movehat 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +236 -0
  2. package/bin/movehat.js +21 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +93 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/compile.d.ts +2 -0
  8. package/dist/commands/compile.d.ts.map +1 -0
  9. package/dist/commands/compile.js +71 -0
  10. package/dist/commands/compile.js.map +1 -0
  11. package/dist/commands/fork/create.d.ts +11 -0
  12. package/dist/commands/fork/create.d.ts.map +1 -0
  13. package/dist/commands/fork/create.js +56 -0
  14. package/dist/commands/fork/create.js.map +1 -0
  15. package/dist/commands/fork/fund.d.ts +12 -0
  16. package/dist/commands/fork/fund.d.ts.map +1 -0
  17. package/dist/commands/fork/fund.js +42 -0
  18. package/dist/commands/fork/fund.js.map +1 -0
  19. package/dist/commands/fork/list.d.ts +5 -0
  20. package/dist/commands/fork/list.d.ts.map +1 -0
  21. package/dist/commands/fork/list.js +61 -0
  22. package/dist/commands/fork/list.js.map +1 -0
  23. package/dist/commands/fork/serve.d.ts +10 -0
  24. package/dist/commands/fork/serve.d.ts.map +1 -0
  25. package/dist/commands/fork/serve.js +64 -0
  26. package/dist/commands/fork/serve.js.map +1 -0
  27. package/dist/commands/fork/view-resource.d.ts +11 -0
  28. package/dist/commands/fork/view-resource.d.ts.map +1 -0
  29. package/dist/commands/fork/view-resource.js +34 -0
  30. package/dist/commands/fork/view-resource.js.map +1 -0
  31. package/dist/commands/init.d.ts +2 -0
  32. package/dist/commands/init.d.ts.map +1 -0
  33. package/dist/commands/init.js +90 -0
  34. package/dist/commands/init.js.map +1 -0
  35. package/dist/commands/run.d.ts +2 -0
  36. package/dist/commands/run.d.ts.map +1 -0
  37. package/dist/commands/run.js +51 -0
  38. package/dist/commands/run.js.map +1 -0
  39. package/dist/commands/test.d.ts +2 -0
  40. package/dist/commands/test.d.ts.map +1 -0
  41. package/dist/commands/test.js +35 -0
  42. package/dist/commands/test.js.map +1 -0
  43. package/dist/core/config.d.ts +15 -0
  44. package/dist/core/config.d.ts.map +1 -0
  45. package/dist/core/config.js +121 -0
  46. package/dist/core/config.js.map +1 -0
  47. package/dist/core/contract.d.ts +20 -0
  48. package/dist/core/contract.d.ts.map +1 -0
  49. package/dist/core/contract.js +59 -0
  50. package/dist/core/contract.js.map +1 -0
  51. package/dist/core/deployments.d.ts +32 -0
  52. package/dist/core/deployments.d.ts.map +1 -0
  53. package/dist/core/deployments.js +122 -0
  54. package/dist/core/deployments.js.map +1 -0
  55. package/dist/core/shell.d.ts +25 -0
  56. package/dist/core/shell.d.ts.map +1 -0
  57. package/dist/core/shell.js +56 -0
  58. package/dist/core/shell.js.map +1 -0
  59. package/dist/errors.d.ts +12 -0
  60. package/dist/errors.d.ts.map +1 -0
  61. package/dist/errors.js +24 -0
  62. package/dist/errors.js.map +1 -0
  63. package/dist/fork/api.d.ts +33 -0
  64. package/dist/fork/api.d.ts.map +1 -0
  65. package/dist/fork/api.js +98 -0
  66. package/dist/fork/api.js.map +1 -0
  67. package/dist/fork/manager.d.ts +52 -0
  68. package/dist/fork/manager.d.ts.map +1 -0
  69. package/dist/fork/manager.js +221 -0
  70. package/dist/fork/manager.js.map +1 -0
  71. package/dist/fork/server.d.ts +55 -0
  72. package/dist/fork/server.d.ts.map +1 -0
  73. package/dist/fork/server.js +274 -0
  74. package/dist/fork/server.js.map +1 -0
  75. package/dist/fork/storage.d.ts +63 -0
  76. package/dist/fork/storage.d.ts.map +1 -0
  77. package/dist/fork/storage.js +183 -0
  78. package/dist/fork/storage.js.map +1 -0
  79. package/dist/fork/test.d.ts +75 -0
  80. package/dist/fork/test.d.ts.map +1 -0
  81. package/dist/fork/test.js +157 -0
  82. package/dist/fork/test.js.map +1 -0
  83. package/dist/helpers/assertions.d.ts +7 -0
  84. package/dist/helpers/assertions.d.ts.map +1 -0
  85. package/dist/helpers/assertions.js +17 -0
  86. package/dist/helpers/assertions.js.map +1 -0
  87. package/dist/helpers/banner.d.ts +3 -0
  88. package/dist/helpers/banner.d.ts.map +1 -0
  89. package/dist/helpers/banner.js +38 -0
  90. package/dist/helpers/banner.js.map +1 -0
  91. package/dist/helpers/index.d.ts +11 -0
  92. package/dist/helpers/index.d.ts.map +1 -0
  93. package/dist/helpers/index.js +7 -0
  94. package/dist/helpers/index.js.map +1 -0
  95. package/dist/helpers/setup.d.ts +10 -0
  96. package/dist/helpers/setup.d.ts.map +1 -0
  97. package/dist/helpers/setup.js +28 -0
  98. package/dist/helpers/setup.js.map +1 -0
  99. package/dist/index.d.ts +11 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +12 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/runtime.d.ts +26 -0
  104. package/dist/runtime.d.ts.map +1 -0
  105. package/dist/runtime.js +247 -0
  106. package/dist/runtime.js.map +1 -0
  107. package/dist/templates/.env.example +9 -0
  108. package/dist/templates/.mocharc.json +8 -0
  109. package/dist/templates/README.md +92 -0
  110. package/dist/templates/move/Counter.move +64 -0
  111. package/dist/templates/move/Move.toml +16 -0
  112. package/dist/templates/movehat.config.ts +37 -0
  113. package/dist/templates/package.json +24 -0
  114. package/dist/templates/scripts/deploy-counter.ts +48 -0
  115. package/dist/templates/tests/Counter.test.ts +75 -0
  116. package/dist/templates/tsconfig.json +15 -0
  117. package/dist/templates/types/movehat.d.ts +104 -0
  118. package/dist/types/config.d.ts +35 -0
  119. package/dist/types/config.d.ts.map +1 -0
  120. package/dist/types/config.js +2 -0
  121. package/dist/types/config.js.map +1 -0
  122. package/dist/types/fork.d.ts +37 -0
  123. package/dist/types/fork.d.ts.map +1 -0
  124. package/dist/types/fork.js +5 -0
  125. package/dist/types/fork.js.map +1 -0
  126. package/dist/types/runtime.d.ts +28 -0
  127. package/dist/types/runtime.d.ts.map +1 -0
  128. package/dist/types/runtime.js +2 -0
  129. package/dist/types/runtime.js.map +1 -0
  130. package/package.json +66 -0
  131. package/src/cli.ts +106 -0
  132. package/src/commands/compile.ts +84 -0
  133. package/src/commands/fork/create.ts +70 -0
  134. package/src/commands/fork/fund.ts +57 -0
  135. package/src/commands/fork/list.ts +67 -0
  136. package/src/commands/fork/serve.ts +77 -0
  137. package/src/commands/fork/view-resource.ts +46 -0
  138. package/src/commands/init.ts +150 -0
  139. package/src/commands/run.ts +59 -0
  140. package/src/commands/test.ts +42 -0
  141. package/src/core/config.ts +151 -0
  142. package/src/core/contract.ts +97 -0
  143. package/src/core/deployments.ts +164 -0
  144. package/src/core/shell.ts +66 -0
  145. package/src/errors.ts +21 -0
  146. package/src/fork/api.ts +117 -0
  147. package/src/fork/manager.ts +264 -0
  148. package/src/fork/server.ts +311 -0
  149. package/src/fork/storage.ts +224 -0
  150. package/src/fork/test.ts +195 -0
  151. package/src/helpers/assertions.ts +29 -0
  152. package/src/helpers/banner.ts +47 -0
  153. package/src/helpers/index.ts +26 -0
  154. package/src/helpers/setup.ts +49 -0
  155. package/src/index.ts +17 -0
  156. package/src/runtime.ts +322 -0
  157. package/src/templates/.env.example +9 -0
  158. package/src/templates/.mocharc.json +8 -0
  159. package/src/templates/README.md +92 -0
  160. package/src/templates/move/Counter.move +64 -0
  161. package/src/templates/move/Move.toml +16 -0
  162. package/src/templates/movehat.config.ts +37 -0
  163. package/src/templates/package.json +24 -0
  164. package/src/templates/scripts/deploy-counter.ts +48 -0
  165. package/src/templates/tests/Counter.test.ts +75 -0
  166. package/src/templates/tsconfig.json +15 -0
  167. package/src/templates/types/movehat.d.ts +104 -0
  168. package/src/types/config.ts +36 -0
  169. package/src/types/fork.ts +41 -0
  170. package/src/types/runtime.ts +49 -0
@@ -0,0 +1,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
+ }