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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export interface DeploymentInfo {
|
|
5
|
+
address: string;
|
|
6
|
+
moduleName: string;
|
|
7
|
+
network: string;
|
|
8
|
+
deployer: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
txHash?: string;
|
|
11
|
+
blockNumber?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates that a name is safe for use in file paths
|
|
16
|
+
* Only allows alphanumeric characters, hyphens, and underscores
|
|
17
|
+
* Prevents path traversal attacks
|
|
18
|
+
*/
|
|
19
|
+
export function validateSafeName(name: string, type: "network" | "module"): void {
|
|
20
|
+
if (!name || typeof name !== "string") {
|
|
21
|
+
throw new Error(`Invalid ${type} name: must be a non-empty string`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check for path traversal sequences
|
|
25
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid ${type} name: "${name}"\n` +
|
|
28
|
+
`Path traversal sequences are not allowed.\n` +
|
|
29
|
+
`Use only alphanumeric characters, hyphens, and underscores.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Only allow alphanumeric, hyphens, underscores
|
|
34
|
+
const safePattern = /^[a-zA-Z0-9_-]+$/;
|
|
35
|
+
if (!safePattern.test(name)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid ${type} name: "${name}"\n` +
|
|
38
|
+
`Only alphanumeric characters, hyphens (-), and underscores (_) are allowed.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Additional check: prevent starting with dot (hidden files)
|
|
43
|
+
if (name.startsWith(".")) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Invalid ${type} name: "${name}"\n` +
|
|
46
|
+
`Names cannot start with a dot (.) to prevent hidden file creation.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the deployments directory path
|
|
53
|
+
*/
|
|
54
|
+
function getDeploymentsDir(): string {
|
|
55
|
+
return join(process.cwd(), "deployments");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the network-specific deployments directory
|
|
60
|
+
*/
|
|
61
|
+
function getNetworkDeploymentsDir(network: string): string {
|
|
62
|
+
// Validate network name to prevent path traversal
|
|
63
|
+
validateSafeName(network, "network");
|
|
64
|
+
|
|
65
|
+
const deploymentsDir = getDeploymentsDir();
|
|
66
|
+
const networkDir = join(deploymentsDir, network);
|
|
67
|
+
|
|
68
|
+
// Create directories if they don't exist
|
|
69
|
+
if (!existsSync(deploymentsDir)) {
|
|
70
|
+
mkdirSync(deploymentsDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
if (!existsSync(networkDir)) {
|
|
73
|
+
mkdirSync(networkDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return networkDir;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Save a deployment
|
|
81
|
+
*/
|
|
82
|
+
export function saveDeployment(deployment: DeploymentInfo): void {
|
|
83
|
+
// Validate both network and module name
|
|
84
|
+
validateSafeName(deployment.network, "network");
|
|
85
|
+
validateSafeName(deployment.moduleName, "module");
|
|
86
|
+
|
|
87
|
+
const networkDir = getNetworkDeploymentsDir(deployment.network);
|
|
88
|
+
const filePath = join(networkDir, `${deployment.moduleName}.json`);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
writeFileSync(filePath, JSON.stringify(deployment, null, 2), "utf-8");
|
|
92
|
+
console.log(
|
|
93
|
+
`💾 Deployment saved: deployments/${deployment.network}/${deployment.moduleName}.json`
|
|
94
|
+
);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(
|
|
97
|
+
`Failed to save deployment for ${deployment.moduleName} on ${deployment.network} at ${filePath}:`,
|
|
98
|
+
error
|
|
99
|
+
);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load a deployment
|
|
106
|
+
*/
|
|
107
|
+
export function loadDeployment(network: string, moduleName: string): DeploymentInfo | null {
|
|
108
|
+
// Validate both network and module name
|
|
109
|
+
validateSafeName(network, "network");
|
|
110
|
+
validateSafeName(moduleName, "module");
|
|
111
|
+
|
|
112
|
+
const networkDir = getNetworkDeploymentsDir(network);
|
|
113
|
+
const filePath = join(networkDir, `${moduleName}.json`);
|
|
114
|
+
|
|
115
|
+
if (!existsSync(filePath)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const content = readFileSync(filePath, "utf-8");
|
|
121
|
+
return JSON.parse(content) as DeploymentInfo;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Failed to load deployment for ${moduleName} on ${network}:`, error);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all deployments for a network
|
|
130
|
+
*/
|
|
131
|
+
export function getAllDeployments(network: string): Record<string, DeploymentInfo> {
|
|
132
|
+
// Validate network name
|
|
133
|
+
validateSafeName(network, "network");
|
|
134
|
+
|
|
135
|
+
const networkDir = getNetworkDeploymentsDir(network);
|
|
136
|
+
|
|
137
|
+
if (!existsSync(networkDir)) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const files = readdirSync(networkDir).filter((f: string) => f.endsWith(".json"));
|
|
142
|
+
|
|
143
|
+
const deployments: Record<string, DeploymentInfo> = {};
|
|
144
|
+
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const moduleName = file.replace(".json", "");
|
|
147
|
+
// loadDeployment will validate moduleName internally
|
|
148
|
+
const deployment = loadDeployment(network, moduleName);
|
|
149
|
+
if (deployment) {
|
|
150
|
+
deployments[moduleName] = deployment;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return deployments;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get deployed address for a module
|
|
159
|
+
*/
|
|
160
|
+
export function getDeployedAddress(network: string, moduleName: string): string | null {
|
|
161
|
+
// Validation happens in loadDeployment
|
|
162
|
+
const deployment = loadDeployment(network, moduleName);
|
|
163
|
+
return deployment ? deployment.address : null;
|
|
164
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes a shell argument to prevent command injection
|
|
3
|
+
* Wraps the argument in single quotes and escapes any single quotes within
|
|
4
|
+
*
|
|
5
|
+
* @param arg - The argument to escape
|
|
6
|
+
* @returns The escaped argument safe for shell execution
|
|
7
|
+
*/
|
|
8
|
+
export function escapeShellArg(arg: string): string {
|
|
9
|
+
if (typeof arg !== "string") {
|
|
10
|
+
throw new Error("Shell argument must be a string");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Wrap in single quotes and escape any single quotes by replacing them with '\''
|
|
14
|
+
// This technique works on both Unix and Windows (Git Bash, WSL)
|
|
15
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates that a path is safe (no command injection characters)
|
|
20
|
+
* and returns the escaped version
|
|
21
|
+
*
|
|
22
|
+
* @param path - The path to validate and escape
|
|
23
|
+
* @param name - Name for error messages (e.g., "package directory")
|
|
24
|
+
* @returns The escaped path safe for shell execution
|
|
25
|
+
*/
|
|
26
|
+
export function validateAndEscapePath(path: string, name: string = "path"): string {
|
|
27
|
+
if (!path || typeof path !== "string") {
|
|
28
|
+
throw new Error(`Invalid ${name}: must be a non-empty string`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for obvious command injection attempts
|
|
32
|
+
const dangerousChars = /[;&|`$(){}[\]<>]/;
|
|
33
|
+
if (dangerousChars.test(path)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Invalid ${name}: "${path}"\n` +
|
|
36
|
+
`Path contains potentially dangerous characters.\n` +
|
|
37
|
+
`Allowed characters: letters, numbers, ., -, _, /, \\, spaces`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Escape for shell safety
|
|
42
|
+
return escapeShellArg(path);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validates that a profile name is safe
|
|
47
|
+
*
|
|
48
|
+
* @param profile - The profile name to validate
|
|
49
|
+
* @returns The escaped profile name
|
|
50
|
+
*/
|
|
51
|
+
export function validateAndEscapeProfile(profile: string): string {
|
|
52
|
+
if (!profile || typeof profile !== "string") {
|
|
53
|
+
throw new Error("Invalid profile name: must be a non-empty string");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Profile names should only contain alphanumeric, hyphens, underscores
|
|
57
|
+
const safePattern = /^[a-zA-Z0-9_-]+$/;
|
|
58
|
+
if (!safePattern.test(profile)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Invalid profile name: "${profile}"\n` +
|
|
61
|
+
`Only alphanumeric characters, hyphens (-), and underscores (_) are allowed.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return escapeShellArg(profile);
|
|
66
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error thrown when attempting to deploy a module that is already deployed
|
|
3
|
+
*/
|
|
4
|
+
export class ModuleAlreadyDeployedError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public readonly moduleName: string,
|
|
8
|
+
public readonly network: string,
|
|
9
|
+
public readonly address: string,
|
|
10
|
+
public readonly timestamp: number,
|
|
11
|
+
public readonly txHash?: string
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ModuleAlreadyDeployedError';
|
|
15
|
+
|
|
16
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
17
|
+
if (Error.captureStackTrace) {
|
|
18
|
+
Error.captureStackTrace(this, ModuleAlreadyDeployedError);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/fork/api.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import type { LedgerInfo, AccountData, AccountResource } from '../types/fork.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client for interacting with Movement/Aptos-compatible JSON API
|
|
8
|
+
*/
|
|
9
|
+
export class MovementApiClient {
|
|
10
|
+
private nodeUrl: string;
|
|
11
|
+
|
|
12
|
+
constructor(nodeUrl: string) {
|
|
13
|
+
// Remove trailing slash
|
|
14
|
+
let normalized = nodeUrl.replace(/\/$/, '');
|
|
15
|
+
|
|
16
|
+
// If URL already ends with /v1, use as is
|
|
17
|
+
// Otherwise, assume it's the base URL
|
|
18
|
+
if (!normalized.endsWith('/v1')) {
|
|
19
|
+
// Base URL without /v1, we'll add it in requests
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.nodeUrl = normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Make a GET request to the API
|
|
27
|
+
*/
|
|
28
|
+
private async get<T>(path: string): Promise<T> {
|
|
29
|
+
const fullUrl = `${this.nodeUrl}${path}`;
|
|
30
|
+
const parsedUrl = new URL(fullUrl);
|
|
31
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
32
|
+
const client = isHttps ? https : http;
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const req = client.get(fullUrl, (res) => {
|
|
36
|
+
let data = '';
|
|
37
|
+
|
|
38
|
+
res.on('data', (chunk) => {
|
|
39
|
+
data += chunk;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
if (res.statusCode !== 200) {
|
|
44
|
+
reject(new Error(`API request failed with status ${res.statusCode}: ${data}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(data);
|
|
50
|
+
resolve(parsed);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
reject(new Error(`Failed to parse JSON response: ${err}`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
req.on('error', (err) => {
|
|
58
|
+
reject(new Error(`API request failed: ${err.message}`));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
req.end();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build API path with proper prefix
|
|
67
|
+
*/
|
|
68
|
+
private apiPath(suffix: string): string {
|
|
69
|
+
// If nodeUrl already ends with /v1, just add the suffix
|
|
70
|
+
// Otherwise add /v1 prefix
|
|
71
|
+
return this.nodeUrl.endsWith('/v1') ? suffix : `/v1${suffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get ledger information
|
|
76
|
+
*/
|
|
77
|
+
async getLedgerInfo(): Promise<LedgerInfo> {
|
|
78
|
+
return this.get<LedgerInfo>(this.apiPath('/'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get account information
|
|
83
|
+
*/
|
|
84
|
+
async getAccount(address: string): Promise<AccountData> {
|
|
85
|
+
// Normalize address (ensure 0x prefix and lowercase)
|
|
86
|
+
const normalizedAddress = address.toLowerCase().startsWith('0x')
|
|
87
|
+
? address.toLowerCase()
|
|
88
|
+
: `0x${address.toLowerCase()}`;
|
|
89
|
+
|
|
90
|
+
return this.get<AccountData>(this.apiPath(`/accounts/${normalizedAddress}`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get a specific account resource
|
|
95
|
+
*/
|
|
96
|
+
async getAccountResource(address: string, resourceType: string): Promise<any> {
|
|
97
|
+
const normalizedAddress = address.toLowerCase().startsWith('0x')
|
|
98
|
+
? address.toLowerCase()
|
|
99
|
+
: `0x${address.toLowerCase()}`;
|
|
100
|
+
|
|
101
|
+
// URL encode the resource type
|
|
102
|
+
const encodedType = encodeURIComponent(resourceType);
|
|
103
|
+
|
|
104
|
+
return this.get<any>(this.apiPath(`/accounts/${normalizedAddress}/resource/${encodedType}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all resources for an account
|
|
109
|
+
*/
|
|
110
|
+
async getAccountResources(address: string): Promise<AccountResource[]> {
|
|
111
|
+
const normalizedAddress = address.toLowerCase().startsWith('0x')
|
|
112
|
+
? address.toLowerCase()
|
|
113
|
+
: `0x${address.toLowerCase()}`;
|
|
114
|
+
|
|
115
|
+
return this.get<AccountResource[]>(this.apiPath(`/accounts/${normalizedAddress}/resources`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { MovementApiClient } from './api.js';
|
|
2
|
+
import { ForkStorage } from './storage.js';
|
|
3
|
+
import type { ForkMetadata, AccountState } from '../types/fork.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manager for fork operations
|
|
7
|
+
* Orchestrates API client and storage
|
|
8
|
+
*/
|
|
9
|
+
export class ForkManager {
|
|
10
|
+
private storage: ForkStorage;
|
|
11
|
+
private apiClient: MovementApiClient | null = null;
|
|
12
|
+
private metadata: ForkMetadata | null = null;
|
|
13
|
+
|
|
14
|
+
constructor(forkPath: string) {
|
|
15
|
+
this.storage = new ForkStorage(forkPath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize a new fork from a network
|
|
20
|
+
*/
|
|
21
|
+
async initialize(nodeUrl: string, networkName: string = 'custom'): Promise<void> {
|
|
22
|
+
// Create API client
|
|
23
|
+
this.apiClient = new MovementApiClient(nodeUrl);
|
|
24
|
+
|
|
25
|
+
// Fetch network info
|
|
26
|
+
const ledgerInfo = await this.apiClient.getLedgerInfo();
|
|
27
|
+
|
|
28
|
+
// Create fork structure
|
|
29
|
+
this.storage.initialize();
|
|
30
|
+
|
|
31
|
+
// Save metadata
|
|
32
|
+
this.metadata = {
|
|
33
|
+
network: networkName,
|
|
34
|
+
nodeUrl,
|
|
35
|
+
chainId: ledgerInfo.chain_id,
|
|
36
|
+
ledgerVersion: ledgerInfo.ledger_version,
|
|
37
|
+
timestamp: ledgerInfo.ledger_timestamp,
|
|
38
|
+
epoch: ledgerInfo.epoch,
|
|
39
|
+
blockHeight: ledgerInfo.block_height,
|
|
40
|
+
createdAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.storage.saveMetadata(this.metadata);
|
|
44
|
+
|
|
45
|
+
console.log(`✓ Fork initialized at ledger version ${ledgerInfo.ledger_version}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load an existing fork
|
|
50
|
+
*/
|
|
51
|
+
load(): void {
|
|
52
|
+
if (!this.storage.exists()) {
|
|
53
|
+
throw new Error('Fork does not exist. Run `initialize()` first.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.metadata = this.storage.loadMetadata();
|
|
57
|
+
this.apiClient = new MovementApiClient(this.metadata.nodeUrl);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get fork metadata
|
|
62
|
+
*/
|
|
63
|
+
getMetadata(): ForkMetadata {
|
|
64
|
+
if (!this.metadata) {
|
|
65
|
+
this.metadata = this.storage.loadMetadata();
|
|
66
|
+
}
|
|
67
|
+
return this.metadata;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get account state (with lazy loading)
|
|
72
|
+
*/
|
|
73
|
+
async getAccount(address: string): Promise<AccountState> {
|
|
74
|
+
// Normalize address
|
|
75
|
+
const normalizedAddress = this.normalizeAddress(address);
|
|
76
|
+
|
|
77
|
+
// Check cache first
|
|
78
|
+
let accountState = this.storage.getAccount(normalizedAddress);
|
|
79
|
+
|
|
80
|
+
if (!accountState) {
|
|
81
|
+
// Fetch from network
|
|
82
|
+
if (!this.apiClient) {
|
|
83
|
+
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(` Fetching account ${normalizedAddress} from network...`);
|
|
87
|
+
const accountData = await this.apiClient.getAccount(normalizedAddress);
|
|
88
|
+
|
|
89
|
+
accountState = {
|
|
90
|
+
sequenceNumber: accountData.sequence_number,
|
|
91
|
+
authenticationKey: accountData.authentication_key,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Cache it
|
|
95
|
+
this.storage.saveAccount(normalizedAddress, accountState);
|
|
96
|
+
console.log(` ✓ Cached account ${normalizedAddress}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return accountState;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a specific resource (with lazy loading)
|
|
104
|
+
*/
|
|
105
|
+
async getResource(address: string, resourceType: string): Promise<any> {
|
|
106
|
+
const normalizedAddress = this.normalizeAddress(address);
|
|
107
|
+
|
|
108
|
+
// Check cache first
|
|
109
|
+
let resource = this.storage.getResource(normalizedAddress, resourceType);
|
|
110
|
+
|
|
111
|
+
if (!resource) {
|
|
112
|
+
// Fetch from network
|
|
113
|
+
if (!this.apiClient) {
|
|
114
|
+
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(` Fetching resource ${resourceType} for ${normalizedAddress}...`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const resourceData = await this.apiClient.getAccountResource(normalizedAddress, resourceType);
|
|
121
|
+
resource = resourceData.data;
|
|
122
|
+
|
|
123
|
+
// Cache it
|
|
124
|
+
this.storage.saveResource(normalizedAddress, resourceType, resource);
|
|
125
|
+
console.log(` ✓ Cached resource ${resourceType}`);
|
|
126
|
+
} catch (error: any) {
|
|
127
|
+
if (error.message.includes('404')) {
|
|
128
|
+
throw new Error(`Resource ${resourceType} not found for account ${normalizedAddress}`);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return resource;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get all resources for an account (with lazy loading)
|
|
139
|
+
*/
|
|
140
|
+
async getAllResources(address: string): Promise<Record<string, any>> {
|
|
141
|
+
const normalizedAddress = this.normalizeAddress(address);
|
|
142
|
+
|
|
143
|
+
// Check if we have any cached resources
|
|
144
|
+
let resources = this.storage.getAllResources(normalizedAddress);
|
|
145
|
+
|
|
146
|
+
// If no cached resources, fetch all from network
|
|
147
|
+
if (Object.keys(resources).length === 0) {
|
|
148
|
+
if (!this.apiClient) {
|
|
149
|
+
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(` Fetching all resources for ${normalizedAddress}...`);
|
|
153
|
+
const resourcesList = await this.apiClient.getAccountResources(normalizedAddress);
|
|
154
|
+
|
|
155
|
+
resources = {};
|
|
156
|
+
for (const resource of resourcesList) {
|
|
157
|
+
resources[resource.type] = resource.data;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Cache them
|
|
161
|
+
this.storage.saveAllResources(normalizedAddress, resources);
|
|
162
|
+
console.log(` ✓ Cached ${Object.keys(resources).length} resources`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return resources;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Set a resource value (for testing/mocking)
|
|
170
|
+
*/
|
|
171
|
+
async setResource(address: string, resourceType: string, data: any): Promise<void> {
|
|
172
|
+
const normalizedAddress = this.normalizeAddress(address);
|
|
173
|
+
this.storage.saveResource(normalizedAddress, resourceType, data);
|
|
174
|
+
console.log(` ✓ Updated resource ${resourceType} for ${normalizedAddress}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fund an account with coins (adds to existing balance)
|
|
179
|
+
*/
|
|
180
|
+
async fundAccount(address: string, amount: number, coinType: string = '0x1::aptos_coin::AptosCoin'): Promise<void> {
|
|
181
|
+
const normalizedAddress = this.normalizeAddress(address);
|
|
182
|
+
const resourceType = `0x1::coin::CoinStore<${coinType}>`;
|
|
183
|
+
|
|
184
|
+
// Try to get existing coin store
|
|
185
|
+
let coinStore: any;
|
|
186
|
+
try {
|
|
187
|
+
coinStore = await this.getResource(normalizedAddress, resourceType);
|
|
188
|
+
} catch (error: any) {
|
|
189
|
+
// Only catch "not found" errors, rethrow others (network, API, etc.)
|
|
190
|
+
if (!error.message || !error.message.includes('not found')) {
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If doesn't exist, create new one
|
|
195
|
+
coinStore = {
|
|
196
|
+
coin: { value: '0' },
|
|
197
|
+
deposit_events: {
|
|
198
|
+
counter: '0',
|
|
199
|
+
guid: {
|
|
200
|
+
id: {
|
|
201
|
+
addr: normalizedAddress,
|
|
202
|
+
creation_num: '0',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
withdraw_events: {
|
|
207
|
+
counter: '0',
|
|
208
|
+
guid: {
|
|
209
|
+
id: {
|
|
210
|
+
addr: normalizedAddress,
|
|
211
|
+
creation_num: '1',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
frozen: false,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add to existing balance (instead of replacing it)
|
|
220
|
+
const currentBalance = BigInt(coinStore.coin.value ?? '0');
|
|
221
|
+
const newBalance = currentBalance + BigInt(amount);
|
|
222
|
+
coinStore.coin.value = newBalance.toString();
|
|
223
|
+
|
|
224
|
+
// Save
|
|
225
|
+
await this.setResource(normalizedAddress, resourceType, coinStore);
|
|
226
|
+
|
|
227
|
+
// Also ensure account exists
|
|
228
|
+
let account = this.storage.getAccount(normalizedAddress);
|
|
229
|
+
if (!account) {
|
|
230
|
+
account = {
|
|
231
|
+
sequenceNumber: '0',
|
|
232
|
+
authenticationKey: normalizedAddress.padEnd(66, '0'),
|
|
233
|
+
};
|
|
234
|
+
this.storage.saveAccount(normalizedAddress, account);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(` ✓ Funded ${normalizedAddress} with ${amount} coins`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Normalize address format
|
|
242
|
+
*/
|
|
243
|
+
private normalizeAddress(address: string): string {
|
|
244
|
+
let normalized = address.toLowerCase();
|
|
245
|
+
|
|
246
|
+
if (!normalized.startsWith('0x')) {
|
|
247
|
+
normalized = `0x${normalized}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Pad to 66 characters (0x + 64 hex chars)
|
|
251
|
+
if (normalized.length < 66) {
|
|
252
|
+
normalized = '0x' + normalized.slice(2).padStart(64, '0');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return normalized;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* List all accounts in the fork
|
|
260
|
+
*/
|
|
261
|
+
listAccounts(): string[] {
|
|
262
|
+
return this.storage.listAccounts();
|
|
263
|
+
}
|
|
264
|
+
}
|