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,311 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { ForkManager } from './manager.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fork Server - Serves fork data via Movement/Aptos RPC API
|
|
7
|
+
* Emulates a Movement L1 node using local fork storage
|
|
8
|
+
*/
|
|
9
|
+
export class ForkServer {
|
|
10
|
+
private server: http.Server | null = null;
|
|
11
|
+
private forkManager: ForkManager;
|
|
12
|
+
private port: number;
|
|
13
|
+
|
|
14
|
+
constructor(forkPath: string, port: number = 8080) {
|
|
15
|
+
this.forkManager = new ForkManager(forkPath);
|
|
16
|
+
this.port = port;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Start the fork server
|
|
21
|
+
*/
|
|
22
|
+
async start(): Promise<void> {
|
|
23
|
+
// Load fork metadata
|
|
24
|
+
this.forkManager.load();
|
|
25
|
+
const metadata = this.forkManager.getMetadata();
|
|
26
|
+
|
|
27
|
+
console.log(`\nStarting Fork Server...`);
|
|
28
|
+
console.log(` Network: ${metadata.network}`);
|
|
29
|
+
console.log(` Chain ID: ${metadata.chainId}`);
|
|
30
|
+
console.log(` Ledger Version: ${metadata.ledgerVersion}`);
|
|
31
|
+
console.log(` Forked at: ${metadata.createdAt}`);
|
|
32
|
+
|
|
33
|
+
this.server = http.createServer((req, res) => {
|
|
34
|
+
this.handleRequest(req, res).catch((error) => {
|
|
35
|
+
// Log full error server-side for diagnostics
|
|
36
|
+
console.error(`Error handling request:`, error);
|
|
37
|
+
|
|
38
|
+
// Only send response if headers haven't been sent yet
|
|
39
|
+
if (!res.headersSent) {
|
|
40
|
+
// Add CORS headers (same as in handleRequest)
|
|
41
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
42
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
43
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
44
|
+
|
|
45
|
+
// Send generic error response (no internal details exposed)
|
|
46
|
+
this.sendJSON(res, 500, {
|
|
47
|
+
message: 'Internal server error',
|
|
48
|
+
error_code: 'internal_error',
|
|
49
|
+
vm_error_code: null
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
// Handle server errors (port in use, permission denied, etc.)
|
|
57
|
+
const onError = (error: NodeJS.ErrnoException) => {
|
|
58
|
+
if (error.code === 'EADDRINUSE') {
|
|
59
|
+
reject(new Error(`Port ${this.port} is already in use. Please use a different port with --port <number>`));
|
|
60
|
+
} else if (error.code === 'EACCES') {
|
|
61
|
+
reject(new Error(`Permission denied to bind to port ${this.port}. Try using a port above 1024 or run with appropriate permissions.`));
|
|
62
|
+
} else {
|
|
63
|
+
reject(new Error(`Failed to start server: ${error.message}`));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Listen for errors during startup
|
|
68
|
+
this.server!.once('error', onError);
|
|
69
|
+
|
|
70
|
+
this.server!.listen(this.port, () => {
|
|
71
|
+
// Remove error listener after successful start
|
|
72
|
+
this.server!.removeListener('error', onError);
|
|
73
|
+
|
|
74
|
+
console.log(`\nFork Server listening on http://localhost:${this.port}`);
|
|
75
|
+
console.log(` Ledger Info: http://localhost:${this.port}/v1/`);
|
|
76
|
+
console.log(`\nPress Ctrl+C to stop`);
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Stop the fork server
|
|
84
|
+
*/
|
|
85
|
+
stop(): Promise<void> {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
if (this.server) {
|
|
88
|
+
this.server.close(() => {
|
|
89
|
+
console.log('\nFork Server stopped');
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sanitize pathname for error messages to prevent log injection
|
|
100
|
+
*/
|
|
101
|
+
private sanitizePathname(pathname: string): string {
|
|
102
|
+
// Remove control characters and newlines
|
|
103
|
+
const sanitized = pathname.replace(/[\x00-\x1F\x7F]/g, '');
|
|
104
|
+
// Truncate to reasonable length
|
|
105
|
+
return sanitized.length > 100 ? sanitized.substring(0, 100) + '...' : sanitized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle incoming HTTP requests
|
|
110
|
+
*/
|
|
111
|
+
private async handleRequest(
|
|
112
|
+
req: http.IncomingMessage,
|
|
113
|
+
res: http.ServerResponse
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const url = new URL(req.url || '/', `http://localhost:${this.port}`);
|
|
116
|
+
const pathname = url.pathname;
|
|
117
|
+
|
|
118
|
+
// Log request
|
|
119
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${pathname}`);
|
|
120
|
+
|
|
121
|
+
// CORS headers
|
|
122
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
123
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
124
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
125
|
+
|
|
126
|
+
// Handle OPTIONS for CORS preflight
|
|
127
|
+
if (req.method === 'OPTIONS') {
|
|
128
|
+
res.writeHead(200);
|
|
129
|
+
res.end();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Route requests
|
|
135
|
+
if (pathname === '/v1' || pathname === '/v1/') {
|
|
136
|
+
await this.handleLedgerInfo(res);
|
|
137
|
+
} else if (pathname.match(/^\/v1\/accounts\/0x[a-fA-F0-9]{1,64}$/)) {
|
|
138
|
+
const address = pathname.split('/').pop()!;
|
|
139
|
+
await this.handleGetAccount(address, res);
|
|
140
|
+
} else if (pathname.match(/^\/v1\/accounts\/0x[a-fA-F0-9]{1,64}\/resource\/.+$/)) {
|
|
141
|
+
const parts = pathname.split('/');
|
|
142
|
+
const accountIndex = parts.indexOf('accounts') + 1;
|
|
143
|
+
const resourceIndex = parts.indexOf('resource') + 1;
|
|
144
|
+
const address = parts[accountIndex];
|
|
145
|
+
const resourceType = decodeURIComponent(parts.slice(resourceIndex).join('/'));
|
|
146
|
+
await this.handleGetResource(address, resourceType, res);
|
|
147
|
+
} else {
|
|
148
|
+
// Use regex capture for resources endpoint
|
|
149
|
+
const resourcesMatch = pathname.match(/^\/v1\/accounts\/(0x[a-fA-F0-9]{1,64})\/resources$/);
|
|
150
|
+
if (resourcesMatch) {
|
|
151
|
+
const address = resourcesMatch[1];
|
|
152
|
+
await this.handleGetResources(address, res);
|
|
153
|
+
} else {
|
|
154
|
+
// Sanitize pathname to prevent log injection
|
|
155
|
+
const safePath = this.sanitizePathname(pathname);
|
|
156
|
+
this.send404(res, `Endpoint not found: ${safePath}`, 'endpoint_not_found');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
// Log full error server-side for diagnostics
|
|
161
|
+
console.error('Error handling request:', error);
|
|
162
|
+
|
|
163
|
+
// Send generic error to client (don't expose internal details)
|
|
164
|
+
this.sendError(res, 500, 'Internal server error');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handle GET /v1/ - Ledger info
|
|
170
|
+
*/
|
|
171
|
+
private async handleLedgerInfo(res: http.ServerResponse): Promise<void> {
|
|
172
|
+
const metadata = this.forkManager.getMetadata();
|
|
173
|
+
|
|
174
|
+
const ledgerInfo = {
|
|
175
|
+
chain_id: metadata.chainId,
|
|
176
|
+
epoch: metadata.epoch,
|
|
177
|
+
ledger_version: metadata.ledgerVersion,
|
|
178
|
+
oldest_ledger_version: "0",
|
|
179
|
+
ledger_timestamp: metadata.timestamp,
|
|
180
|
+
node_role: "full_node",
|
|
181
|
+
oldest_block_height: "0",
|
|
182
|
+
block_height: metadata.blockHeight,
|
|
183
|
+
git_hash: "movehat-fork"
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.sendJSON(res, 200, ledgerInfo, {
|
|
187
|
+
'x-aptos-chain-id': String(metadata.chainId),
|
|
188
|
+
'x-aptos-ledger-version': metadata.ledgerVersion,
|
|
189
|
+
'x-aptos-ledger-oldest-version': '0',
|
|
190
|
+
'x-aptos-ledger-timestampusec': metadata.timestamp,
|
|
191
|
+
'x-aptos-epoch': metadata.epoch,
|
|
192
|
+
'x-aptos-block-height': metadata.blockHeight,
|
|
193
|
+
'x-aptos-oldest-block-height': '0'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle GET /v1/accounts/:address
|
|
199
|
+
*/
|
|
200
|
+
private async handleGetAccount(
|
|
201
|
+
address: string,
|
|
202
|
+
res: http.ServerResponse
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
try {
|
|
205
|
+
const account = await this.forkManager.getAccount(address);
|
|
206
|
+
|
|
207
|
+
this.sendJSON(res, 200, {
|
|
208
|
+
sequence_number: account.sequenceNumber,
|
|
209
|
+
authentication_key: account.authenticationKey
|
|
210
|
+
});
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
if (error.message.includes('not found')) {
|
|
213
|
+
this.send404(res, `Account not found: ${address}`);
|
|
214
|
+
} else {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Handle GET /v1/accounts/:address/resource/:resourceType
|
|
222
|
+
*/
|
|
223
|
+
private async handleGetResource(
|
|
224
|
+
address: string,
|
|
225
|
+
resourceType: string,
|
|
226
|
+
res: http.ServerResponse
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
try {
|
|
229
|
+
const resource = await this.forkManager.getResource(address, resourceType);
|
|
230
|
+
|
|
231
|
+
this.sendJSON(res, 200, {
|
|
232
|
+
type: resourceType,
|
|
233
|
+
data: resource
|
|
234
|
+
});
|
|
235
|
+
} catch (error: any) {
|
|
236
|
+
if (error.message.includes('not found')) {
|
|
237
|
+
this.send404(res, `Resource not found: ${resourceType}`, 'resource_not_found');
|
|
238
|
+
} else {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Handle GET /v1/accounts/:address/resources
|
|
246
|
+
*/
|
|
247
|
+
private async handleGetResources(
|
|
248
|
+
address: string,
|
|
249
|
+
res: http.ServerResponse
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
try {
|
|
252
|
+
const resources = await this.forkManager.getAllResources(address);
|
|
253
|
+
|
|
254
|
+
// Convert to array format expected by Aptos API
|
|
255
|
+
const resourcesArray = Object.entries(resources).map(([type, data]) => ({
|
|
256
|
+
type,
|
|
257
|
+
data
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
this.sendJSON(res, 200, resourcesArray);
|
|
261
|
+
} catch (error: any) {
|
|
262
|
+
if (error.message.includes('not found')) {
|
|
263
|
+
this.send404(res, `Account not found: ${address}`);
|
|
264
|
+
} else {
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Send JSON response
|
|
272
|
+
*/
|
|
273
|
+
private sendJSON(
|
|
274
|
+
res: http.ServerResponse,
|
|
275
|
+
status: number,
|
|
276
|
+
data: any,
|
|
277
|
+
extraHeaders: Record<string, string> = {}
|
|
278
|
+
): void {
|
|
279
|
+
const body = JSON.stringify(data, null, 2);
|
|
280
|
+
|
|
281
|
+
res.writeHead(status, {
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
'Content-Length': Buffer.byteLength(body),
|
|
284
|
+
...extraHeaders
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
res.end(body);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Send 404 error
|
|
292
|
+
*/
|
|
293
|
+
private send404(res: http.ServerResponse, message: string, errorCode: string = 'account_not_found'): void {
|
|
294
|
+
this.sendJSON(res, 404, {
|
|
295
|
+
message,
|
|
296
|
+
error_code: errorCode,
|
|
297
|
+
vm_error_code: null
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Send error response
|
|
303
|
+
*/
|
|
304
|
+
private sendError(res: http.ServerResponse, status: number, message: string): void {
|
|
305
|
+
this.sendJSON(res, status, {
|
|
306
|
+
message,
|
|
307
|
+
error_code: 'internal_error',
|
|
308
|
+
vm_error_code: null
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { ForkMetadata, AccountState } from '../types/fork.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize address to create a safe filename
|
|
7
|
+
* Prevents path traversal by encoding the address
|
|
8
|
+
*/
|
|
9
|
+
function sanitizeAddressForFilename(address: string): string {
|
|
10
|
+
// Remove any path separators and normalize
|
|
11
|
+
const normalized = address.toLowerCase().replace(/^0x/, '');
|
|
12
|
+
|
|
13
|
+
// Validate that it's a valid hex string
|
|
14
|
+
if (!/^[0-9a-f]+$/.test(normalized)) {
|
|
15
|
+
throw new Error(`Invalid address format: ${address}. Expected hexadecimal string.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Encode to prevent any path traversal
|
|
19
|
+
// Use the normalized hex string directly as it's safe
|
|
20
|
+
const safe = `0x${normalized}`;
|
|
21
|
+
|
|
22
|
+
// Validate no path separators in result
|
|
23
|
+
if (safe.includes('/') || safe.includes('\\') || safe.includes('..')) {
|
|
24
|
+
throw new Error(`Address contains invalid characters: ${address}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return safe;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Storage system for fork state
|
|
32
|
+
* Manages the file structure and I/O for fork data
|
|
33
|
+
*/
|
|
34
|
+
export class ForkStorage {
|
|
35
|
+
private forkPath: string;
|
|
36
|
+
|
|
37
|
+
constructor(forkPath: string) {
|
|
38
|
+
this.forkPath = forkPath;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get safe resource file path for an address
|
|
43
|
+
* Prevents path traversal attacks
|
|
44
|
+
*/
|
|
45
|
+
private getResourceFilePath(address: string): string {
|
|
46
|
+
const safeFilename = sanitizeAddressForFilename(address);
|
|
47
|
+
return join(this.forkPath, 'resources', `${safeFilename}.json`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialize fork directory structure
|
|
52
|
+
*/
|
|
53
|
+
initialize(): void {
|
|
54
|
+
// Create main fork directory
|
|
55
|
+
if (!existsSync(this.forkPath)) {
|
|
56
|
+
mkdirSync(this.forkPath, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create subdirectories
|
|
60
|
+
const resourcesDir = join(this.forkPath, 'resources');
|
|
61
|
+
if (!existsSync(resourcesDir)) {
|
|
62
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cacheDir = join(this.forkPath, 'cache');
|
|
66
|
+
if (!existsSync(cacheDir)) {
|
|
67
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create .gitignore for cache
|
|
71
|
+
const gitignorePath = join(cacheDir, '.gitignore');
|
|
72
|
+
if (!existsSync(gitignorePath)) {
|
|
73
|
+
writeFileSync(gitignorePath, '*\n!.gitignore\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Initialize accounts.json if it doesn't exist
|
|
77
|
+
const accountsPath = join(this.forkPath, 'accounts.json');
|
|
78
|
+
if (!existsSync(accountsPath)) {
|
|
79
|
+
writeFileSync(accountsPath, JSON.stringify({}, null, 2));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if fork exists
|
|
85
|
+
*/
|
|
86
|
+
exists(): boolean {
|
|
87
|
+
return existsSync(this.forkPath) && existsSync(join(this.forkPath, 'metadata.json'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Save fork metadata
|
|
92
|
+
*/
|
|
93
|
+
saveMetadata(metadata: ForkMetadata): void {
|
|
94
|
+
const metadataPath = join(this.forkPath, 'metadata.json');
|
|
95
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load fork metadata
|
|
100
|
+
*/
|
|
101
|
+
loadMetadata(): ForkMetadata {
|
|
102
|
+
const metadataPath = join(this.forkPath, 'metadata.json');
|
|
103
|
+
|
|
104
|
+
if (!existsSync(metadataPath)) {
|
|
105
|
+
throw new Error(`Fork metadata not found at ${metadataPath}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const data = readFileSync(metadataPath, 'utf-8');
|
|
109
|
+
return JSON.parse(data);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get account state
|
|
114
|
+
*/
|
|
115
|
+
getAccount(address: string): AccountState | null {
|
|
116
|
+
const accountsPath = join(this.forkPath, 'accounts.json');
|
|
117
|
+
|
|
118
|
+
if (!existsSync(accountsPath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const accounts = JSON.parse(readFileSync(accountsPath, 'utf-8'));
|
|
123
|
+
return accounts[address] || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Save account state
|
|
128
|
+
*/
|
|
129
|
+
saveAccount(address: string, state: AccountState): void {
|
|
130
|
+
const accountsPath = join(this.forkPath, 'accounts.json');
|
|
131
|
+
|
|
132
|
+
let accounts: Record<string, AccountState> = {};
|
|
133
|
+
if (existsSync(accountsPath)) {
|
|
134
|
+
accounts = JSON.parse(readFileSync(accountsPath, 'utf-8'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
accounts[address] = state;
|
|
138
|
+
writeFileSync(accountsPath, JSON.stringify(accounts, null, 2));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get resource for an account
|
|
143
|
+
*/
|
|
144
|
+
getResource(address: string, resourceType: string): any | null {
|
|
145
|
+
const resourceFilePath = this.getResourceFilePath(address);
|
|
146
|
+
|
|
147
|
+
if (!existsSync(resourceFilePath)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const resources = JSON.parse(readFileSync(resourceFilePath, 'utf-8'));
|
|
152
|
+
return resources[resourceType] || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get all resources for an account
|
|
157
|
+
*/
|
|
158
|
+
getAllResources(address: string): Record<string, any> {
|
|
159
|
+
const resourceFilePath = this.getResourceFilePath(address);
|
|
160
|
+
|
|
161
|
+
if (!existsSync(resourceFilePath)) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return JSON.parse(readFileSync(resourceFilePath, 'utf-8'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Save resource for an account
|
|
170
|
+
*/
|
|
171
|
+
saveResource(address: string, resourceType: string, data: any): void {
|
|
172
|
+
const resourceFilePath = this.getResourceFilePath(address);
|
|
173
|
+
|
|
174
|
+
// Ensure resources directory exists
|
|
175
|
+
const resourcesDir = join(this.forkPath, 'resources');
|
|
176
|
+
if (!existsSync(resourcesDir)) {
|
|
177
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let resources: Record<string, any> = {};
|
|
181
|
+
if (existsSync(resourceFilePath)) {
|
|
182
|
+
resources = JSON.parse(readFileSync(resourceFilePath, 'utf-8'));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
resources[resourceType] = data;
|
|
186
|
+
writeFileSync(resourceFilePath, JSON.stringify(resources, null, 2));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Save all resources for an account
|
|
191
|
+
*/
|
|
192
|
+
saveAllResources(address: string, resources: Record<string, any>): void {
|
|
193
|
+
const resourceFilePath = this.getResourceFilePath(address);
|
|
194
|
+
|
|
195
|
+
// Ensure resources directory exists
|
|
196
|
+
const resourcesDir = join(this.forkPath, 'resources');
|
|
197
|
+
if (!existsSync(resourcesDir)) {
|
|
198
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
writeFileSync(resourceFilePath, JSON.stringify(resources, null, 2));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if resource is cached
|
|
206
|
+
*/
|
|
207
|
+
hasResource(address: string, resourceType: string): boolean {
|
|
208
|
+
return this.getResource(address, resourceType) !== null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* List all accounts in the fork
|
|
213
|
+
*/
|
|
214
|
+
listAccounts(): string[] {
|
|
215
|
+
const accountsPath = join(this.forkPath, 'accounts.json');
|
|
216
|
+
|
|
217
|
+
if (!existsSync(accountsPath)) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const accounts = JSON.parse(readFileSync(accountsPath, 'utf-8'));
|
|
222
|
+
return Object.keys(accounts);
|
|
223
|
+
}
|
|
224
|
+
}
|