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.
- package/README.md +236 -0
- package/bin/movehat.js +21 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +93 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/compile.d.ts +2 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +71 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/fork/create.d.ts +11 -0
- package/dist/commands/fork/create.d.ts.map +1 -0
- package/dist/commands/fork/create.js +56 -0
- package/dist/commands/fork/create.js.map +1 -0
- package/dist/commands/fork/fund.d.ts +12 -0
- package/dist/commands/fork/fund.d.ts.map +1 -0
- package/dist/commands/fork/fund.js +42 -0
- package/dist/commands/fork/fund.js.map +1 -0
- package/dist/commands/fork/list.d.ts +5 -0
- package/dist/commands/fork/list.d.ts.map +1 -0
- package/dist/commands/fork/list.js +61 -0
- package/dist/commands/fork/list.js.map +1 -0
- package/dist/commands/fork/serve.d.ts +10 -0
- package/dist/commands/fork/serve.d.ts.map +1 -0
- package/dist/commands/fork/serve.js +64 -0
- package/dist/commands/fork/serve.js.map +1 -0
- package/dist/commands/fork/view-resource.d.ts +11 -0
- package/dist/commands/fork/view-resource.d.ts.map +1 -0
- package/dist/commands/fork/view-resource.js +34 -0
- package/dist/commands/fork/view-resource.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +90 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +51 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +35 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +121 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/contract.d.ts +20 -0
- package/dist/core/contract.d.ts.map +1 -0
- package/dist/core/contract.js +59 -0
- package/dist/core/contract.js.map +1 -0
- package/dist/core/deployments.d.ts +32 -0
- package/dist/core/deployments.d.ts.map +1 -0
- package/dist/core/deployments.js +122 -0
- package/dist/core/deployments.js.map +1 -0
- package/dist/core/shell.d.ts +25 -0
- package/dist/core/shell.d.ts.map +1 -0
- package/dist/core/shell.js +56 -0
- package/dist/core/shell.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/fork/api.d.ts +33 -0
- package/dist/fork/api.d.ts.map +1 -0
- package/dist/fork/api.js +98 -0
- package/dist/fork/api.js.map +1 -0
- package/dist/fork/manager.d.ts +52 -0
- package/dist/fork/manager.d.ts.map +1 -0
- package/dist/fork/manager.js +221 -0
- package/dist/fork/manager.js.map +1 -0
- package/dist/fork/server.d.ts +55 -0
- package/dist/fork/server.d.ts.map +1 -0
- package/dist/fork/server.js +274 -0
- package/dist/fork/server.js.map +1 -0
- package/dist/fork/storage.d.ts +63 -0
- package/dist/fork/storage.d.ts.map +1 -0
- package/dist/fork/storage.js +183 -0
- package/dist/fork/storage.js.map +1 -0
- package/dist/fork/test.d.ts +75 -0
- package/dist/fork/test.d.ts.map +1 -0
- package/dist/fork/test.js +157 -0
- package/dist/fork/test.js.map +1 -0
- package/dist/helpers/assertions.d.ts +7 -0
- package/dist/helpers/assertions.d.ts.map +1 -0
- package/dist/helpers/assertions.js +17 -0
- package/dist/helpers/assertions.js.map +1 -0
- package/dist/helpers/banner.d.ts +3 -0
- package/dist/helpers/banner.d.ts.map +1 -0
- package/dist/helpers/banner.js +38 -0
- package/dist/helpers/banner.js.map +1 -0
- package/dist/helpers/index.d.ts +11 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +7 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/helpers/setup.d.ts +10 -0
- package/dist/helpers/setup.d.ts.map +1 -0
- package/dist/helpers/setup.js +28 -0
- package/dist/helpers/setup.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +247 -0
- package/dist/runtime.js.map +1 -0
- package/dist/templates/.env.example +9 -0
- package/dist/templates/.mocharc.json +8 -0
- package/dist/templates/README.md +92 -0
- package/dist/templates/move/Counter.move +64 -0
- package/dist/templates/move/Move.toml +16 -0
- package/dist/templates/movehat.config.ts +37 -0
- package/dist/templates/package.json +24 -0
- package/dist/templates/scripts/deploy-counter.ts +48 -0
- package/dist/templates/tests/Counter.test.ts +75 -0
- package/dist/templates/tsconfig.json +15 -0
- package/dist/templates/types/movehat.d.ts +104 -0
- package/dist/types/config.d.ts +35 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/fork.d.ts +37 -0
- package/dist/types/fork.d.ts.map +1 -0
- package/dist/types/fork.js +5 -0
- package/dist/types/fork.js.map +1 -0
- package/dist/types/runtime.d.ts +28 -0
- package/dist/types/runtime.d.ts.map +1 -0
- package/dist/types/runtime.js +2 -0
- package/dist/types/runtime.js.map +1 -0
- package/package.json +66 -0
- package/src/cli.ts +106 -0
- package/src/commands/compile.ts +84 -0
- package/src/commands/fork/create.ts +70 -0
- package/src/commands/fork/fund.ts +57 -0
- package/src/commands/fork/list.ts +67 -0
- package/src/commands/fork/serve.ts +77 -0
- package/src/commands/fork/view-resource.ts +46 -0
- package/src/commands/init.ts +150 -0
- package/src/commands/run.ts +59 -0
- package/src/commands/test.ts +42 -0
- package/src/core/config.ts +151 -0
- package/src/core/contract.ts +97 -0
- package/src/core/deployments.ts +164 -0
- package/src/core/shell.ts +66 -0
- package/src/errors.ts +21 -0
- package/src/fork/api.ts +117 -0
- package/src/fork/manager.ts +264 -0
- package/src/fork/server.ts +311 -0
- package/src/fork/storage.ts +224 -0
- package/src/fork/test.ts +195 -0
- package/src/helpers/assertions.ts +29 -0
- package/src/helpers/banner.ts +47 -0
- package/src/helpers/index.ts +26 -0
- package/src/helpers/setup.ts +49 -0
- package/src/index.ts +17 -0
- package/src/runtime.ts +322 -0
- package/src/templates/.env.example +9 -0
- package/src/templates/.mocharc.json +8 -0
- package/src/templates/README.md +92 -0
- package/src/templates/move/Counter.move +64 -0
- package/src/templates/move/Move.toml +16 -0
- package/src/templates/movehat.config.ts +37 -0
- package/src/templates/package.json +24 -0
- package/src/templates/scripts/deploy-counter.ts +48 -0
- package/src/templates/tests/Counter.test.ts +75 -0
- package/src/templates/tsconfig.json +15 -0
- package/src/templates/types/movehat.d.ts +104 -0
- package/src/types/config.ts +36 -0
- package/src/types/fork.ts +41 -0
- package/src/types/runtime.ts +49 -0
package/src/fork/test.ts
ADDED
|
@@ -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
|