solforge 0.2.4 → 0.2.5

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 (42) hide show
  1. package/README.md +471 -79
  2. package/cli.cjs +106 -78
  3. package/package.json +1 -1
  4. package/scripts/install.sh +1 -1
  5. package/scripts/postinstall.cjs +66 -58
  6. package/server/methods/program/get-token-accounts-by-owner.ts +7 -2
  7. package/server/ws-server.ts +4 -1
  8. package/src/api-server-entry.ts +91 -91
  9. package/src/cli/commands/rpc-start.ts +4 -1
  10. package/src/cli/main.ts +7 -3
  11. package/src/cli/run-solforge.ts +20 -6
  12. package/src/commands/add-program.ts +324 -328
  13. package/src/commands/init.ts +106 -106
  14. package/src/commands/list.ts +125 -125
  15. package/src/commands/mint.ts +246 -246
  16. package/src/commands/start.ts +834 -831
  17. package/src/commands/status.ts +80 -80
  18. package/src/commands/stop.ts +381 -382
  19. package/src/config/manager.ts +149 -149
  20. package/src/gui/public/app.css +1556 -1
  21. package/src/gui/public/build/main.css +1569 -1
  22. package/src/gui/server.ts +20 -21
  23. package/src/gui/src/app.tsx +56 -37
  24. package/src/gui/src/components/airdrop-mint-form.tsx +17 -11
  25. package/src/gui/src/components/clone-program-modal.tsx +6 -6
  26. package/src/gui/src/components/clone-token-modal.tsx +7 -7
  27. package/src/gui/src/components/modal.tsx +13 -11
  28. package/src/gui/src/components/programs-panel.tsx +27 -15
  29. package/src/gui/src/components/status-panel.tsx +31 -17
  30. package/src/gui/src/components/tokens-panel.tsx +25 -19
  31. package/src/gui/src/index.css +491 -463
  32. package/src/index.ts +161 -146
  33. package/src/rpc/start.ts +1 -1
  34. package/src/services/api-server.ts +470 -473
  35. package/src/services/port-manager.ts +167 -167
  36. package/src/services/process-registry.ts +143 -143
  37. package/src/services/program-cloner.ts +312 -312
  38. package/src/services/token-cloner.ts +799 -797
  39. package/src/services/validator.ts +288 -288
  40. package/src/types/config.ts +71 -71
  41. package/src/utils/shell.ts +75 -75
  42. package/src/utils/token-loader.ts +77 -77
@@ -1,295 +1,295 @@
1
- import { spawn, ChildProcess } from "child_process";
1
+ import { type ChildProcess, spawn } from "child_process";
2
2
  import { existsSync } from "fs";
3
3
  import { join } from "path";
4
4
  import type {
5
- ValidatorState,
6
- ValidatorStatus,
7
- LocalnetConfig,
8
- OperationResult,
5
+ LocalnetConfig,
6
+ OperationResult,
7
+ ValidatorState,
8
+ ValidatorStatus,
9
9
  } from "../types/config.js";
10
10
 
11
11
  export class ValidatorService {
12
- private process: ChildProcess | null = null;
13
- private state: ValidatorState;
14
- private config: LocalnetConfig;
15
-
16
- constructor(config: LocalnetConfig) {
17
- this.config = config;
18
- this.state = {
19
- status: "stopped",
20
- port: config.port,
21
- faucetPort: config.faucetPort,
22
- rpcUrl: `http://${config.bindAddress}:${config.port}`,
23
- wsUrl: `ws://${config.bindAddress}:${config.port}`,
24
- logs: [],
25
- };
26
- }
27
-
28
- /**
29
- * Start the validator with the given configuration
30
- */
31
- async start(
32
- programs: string[] = [],
33
- tokens: string[] = []
34
- ): Promise<OperationResult<ValidatorState>> {
35
- if (this.state.status === "running") {
36
- return {
37
- success: false,
38
- error: "Validator is already running",
39
- data: this.state,
40
- };
41
- }
42
-
43
- try {
44
- this.updateStatus("starting");
45
-
46
- const args = this.buildValidatorArgs(programs, tokens);
47
-
48
- this.process = spawn("solana-test-validator", args, {
49
- stdio: ["pipe", "pipe", "pipe"],
50
- detached: false,
51
- });
52
-
53
- this.state.pid = this.process.pid;
54
- this.state.startTime = new Date();
55
-
56
- // Handle process events
57
- this.setupProcessHandlers();
58
-
59
- // Wait for validator to be ready
60
- await this.waitForReady();
61
-
62
- this.updateStatus("running");
63
-
64
- return {
65
- success: true,
66
- data: this.state,
67
- };
68
- } catch (error) {
69
- this.updateStatus("error");
70
- this.state.error = error instanceof Error ? error.message : String(error);
71
-
72
- return {
73
- success: false,
74
- error: this.state.error,
75
- data: this.state,
76
- };
77
- }
78
- }
79
-
80
- /**
81
- * Stop the validator
82
- */
83
- async stop(): Promise<OperationResult<ValidatorState>> {
84
- if (this.state.status === "stopped") {
85
- return {
86
- success: true,
87
- data: this.state,
88
- };
89
- }
90
-
91
- try {
92
- this.updateStatus("stopping");
93
-
94
- if (this.process) {
95
- this.process.kill("SIGTERM");
96
-
97
- // Wait for graceful shutdown
98
- await new Promise<void>((resolve) => {
99
- const timeout = setTimeout(() => {
100
- if (this.process) {
101
- this.process.kill("SIGKILL");
102
- }
103
- resolve();
104
- }, 5000);
105
-
106
- this.process?.on("exit", () => {
107
- clearTimeout(timeout);
108
- resolve();
109
- });
110
- });
111
- }
112
-
113
- this.cleanup();
114
- this.updateStatus("stopped");
115
-
116
- return {
117
- success: true,
118
- data: this.state,
119
- };
120
- } catch (error) {
121
- return {
122
- success: false,
123
- error: error instanceof Error ? error.message : String(error),
124
- data: this.state,
125
- };
126
- }
127
- }
128
-
129
- /**
130
- * Get current validator state
131
- */
132
- getState(): ValidatorState {
133
- return { ...this.state };
134
- }
135
-
136
- /**
137
- * Check if validator is running
138
- */
139
- isRunning(): boolean {
140
- return this.state.status === "running";
141
- }
142
-
143
- /**
144
- * Get recent logs
145
- */
146
- getLogs(count = 100): string[] {
147
- return this.state.logs.slice(-count);
148
- }
149
-
150
- /**
151
- * Build validator arguments based on configuration
152
- */
153
- private buildValidatorArgs(
154
- programs: string[] = [],
155
- tokens: string[] = []
156
- ): string[] {
157
- const args: string[] = [];
158
-
159
- // Basic configuration
160
- args.push("--rpc-port", this.config.port.toString());
161
- args.push("--faucet-port", this.config.faucetPort.toString());
162
- args.push("--bind-address", this.config.bindAddress);
163
-
164
- if (this.config.reset) {
165
- args.push("--reset");
166
- }
167
-
168
- if (this.config.quiet) {
169
- args.push("--quiet");
170
- }
171
-
172
- if (this.config.ledgerPath) {
173
- args.push("--ledger", this.config.ledgerPath);
174
- }
175
-
176
- // Set log level
177
- args.push("--log");
178
-
179
- // Clone programs
180
- for (const programId of programs) {
181
- args.push("--clone", programId);
182
- }
183
-
184
- // Clone tokens (these would be mint addresses)
185
- for (const tokenMint of tokens) {
186
- args.push("--clone", tokenMint);
187
- }
188
-
189
- // Always specify mainnet as the source for cloning
190
- if (programs.length > 0 || tokens.length > 0) {
191
- args.push("--url", "https://api.mainnet-beta.solana.com");
192
- }
193
-
194
- return args;
195
- }
196
-
197
- /**
198
- * Setup process event handlers
199
- */
200
- private setupProcessHandlers(): void {
201
- if (!this.process) return;
202
-
203
- this.process.stdout?.on("data", (data: Buffer) => {
204
- const log = data.toString().trim();
205
- this.addLog(`[STDOUT] ${log}`);
206
- });
207
-
208
- this.process.stderr?.on("data", (data: Buffer) => {
209
- const log = data.toString().trim();
210
- this.addLog(`[STDERR] ${log}`);
211
- });
212
-
213
- this.process.on("error", (error) => {
214
- this.addLog(`[ERROR] ${error.message}`);
215
- this.updateStatus("error");
216
- this.state.error = error.message;
217
- });
218
-
219
- this.process.on("exit", (code, signal) => {
220
- this.addLog(`[EXIT] Process exited with code ${code}, signal ${signal}`);
221
- this.cleanup();
222
-
223
- if (this.state.status !== "stopping") {
224
- this.updateStatus(code === 0 ? "stopped" : "error");
225
- if (code !== 0) {
226
- this.state.error = `Process exited with code ${code}`;
227
- }
228
- }
229
- });
230
- }
231
-
232
- /**
233
- * Wait for validator to be ready
234
- */
235
- private async waitForReady(timeout = 30000): Promise<void> {
236
- const startTime = Date.now();
237
-
238
- while (Date.now() - startTime < timeout) {
239
- try {
240
- // Try to connect to the RPC endpoint
241
- const response = await fetch(this.state.rpcUrl, {
242
- method: "POST",
243
- headers: { "Content-Type": "application/json" },
244
- body: JSON.stringify({
245
- jsonrpc: "2.0",
246
- id: 1,
247
- method: "getHealth",
248
- }),
249
- });
250
-
251
- if (response.ok) {
252
- return; // Validator is ready
253
- }
254
- } catch (error) {
255
- // Continue waiting
256
- }
257
-
258
- await new Promise((resolve) => setTimeout(resolve, 1000));
259
- }
260
-
261
- throw new Error("Validator failed to start within timeout period");
262
- }
263
-
264
- /**
265
- * Update validator status
266
- */
267
- private updateStatus(status: ValidatorStatus): void {
268
- this.state.status = status;
269
- this.addLog(`[STATUS] Validator status changed to: ${status}`);
270
- }
271
-
272
- /**
273
- * Add log entry
274
- */
275
- private addLog(message: string): void {
276
- const timestamp = new Date().toISOString();
277
- const logEntry = `[${timestamp}] ${message}`;
278
- this.state.logs.push(logEntry);
279
-
280
- // Keep only last 1000 log entries
281
- if (this.state.logs.length > 1000) {
282
- this.state.logs = this.state.logs.slice(-1000);
283
- }
284
- }
285
-
286
- /**
287
- * Clean up process references
288
- */
289
- private cleanup(): void {
290
- this.process = null;
291
- this.state.pid = undefined;
292
- this.state.startTime = undefined;
293
- this.state.error = undefined;
294
- }
12
+ private process: ChildProcess | null = null;
13
+ private state: ValidatorState;
14
+ private config: LocalnetConfig;
15
+
16
+ constructor(config: LocalnetConfig) {
17
+ this.config = config;
18
+ this.state = {
19
+ status: "stopped",
20
+ port: config.port,
21
+ faucetPort: config.faucetPort,
22
+ rpcUrl: `http://${config.bindAddress}:${config.port}`,
23
+ wsUrl: `ws://${config.bindAddress}:${config.port}`,
24
+ logs: [],
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Start the validator with the given configuration
30
+ */
31
+ async start(
32
+ programs: string[] = [],
33
+ tokens: string[] = [],
34
+ ): Promise<OperationResult<ValidatorState>> {
35
+ if (this.state.status === "running") {
36
+ return {
37
+ success: false,
38
+ error: "Validator is already running",
39
+ data: this.state,
40
+ };
41
+ }
42
+
43
+ try {
44
+ this.updateStatus("starting");
45
+
46
+ const args = this.buildValidatorArgs(programs, tokens);
47
+
48
+ this.process = spawn("solana-test-validator", args, {
49
+ stdio: ["pipe", "pipe", "pipe"],
50
+ detached: false,
51
+ });
52
+
53
+ this.state.pid = this.process.pid;
54
+ this.state.startTime = new Date();
55
+
56
+ // Handle process events
57
+ this.setupProcessHandlers();
58
+
59
+ // Wait for validator to be ready
60
+ await this.waitForReady();
61
+
62
+ this.updateStatus("running");
63
+
64
+ return {
65
+ success: true,
66
+ data: this.state,
67
+ };
68
+ } catch (error) {
69
+ this.updateStatus("error");
70
+ this.state.error = error instanceof Error ? error.message : String(error);
71
+
72
+ return {
73
+ success: false,
74
+ error: this.state.error,
75
+ data: this.state,
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Stop the validator
82
+ */
83
+ async stop(): Promise<OperationResult<ValidatorState>> {
84
+ if (this.state.status === "stopped") {
85
+ return {
86
+ success: true,
87
+ data: this.state,
88
+ };
89
+ }
90
+
91
+ try {
92
+ this.updateStatus("stopping");
93
+
94
+ if (this.process) {
95
+ this.process.kill("SIGTERM");
96
+
97
+ // Wait for graceful shutdown
98
+ await new Promise<void>((resolve) => {
99
+ const timeout = setTimeout(() => {
100
+ if (this.process) {
101
+ this.process.kill("SIGKILL");
102
+ }
103
+ resolve();
104
+ }, 5000);
105
+
106
+ this.process?.on("exit", () => {
107
+ clearTimeout(timeout);
108
+ resolve();
109
+ });
110
+ });
111
+ }
112
+
113
+ this.cleanup();
114
+ this.updateStatus("stopped");
115
+
116
+ return {
117
+ success: true,
118
+ data: this.state,
119
+ };
120
+ } catch (error) {
121
+ return {
122
+ success: false,
123
+ error: error instanceof Error ? error.message : String(error),
124
+ data: this.state,
125
+ };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Get current validator state
131
+ */
132
+ getState(): ValidatorState {
133
+ return { ...this.state };
134
+ }
135
+
136
+ /**
137
+ * Check if validator is running
138
+ */
139
+ isRunning(): boolean {
140
+ return this.state.status === "running";
141
+ }
142
+
143
+ /**
144
+ * Get recent logs
145
+ */
146
+ getLogs(count = 100): string[] {
147
+ return this.state.logs.slice(-count);
148
+ }
149
+
150
+ /**
151
+ * Build validator arguments based on configuration
152
+ */
153
+ private buildValidatorArgs(
154
+ programs: string[] = [],
155
+ tokens: string[] = [],
156
+ ): string[] {
157
+ const args: string[] = [];
158
+
159
+ // Basic configuration
160
+ args.push("--rpc-port", this.config.port.toString());
161
+ args.push("--faucet-port", this.config.faucetPort.toString());
162
+ args.push("--bind-address", this.config.bindAddress);
163
+
164
+ if (this.config.reset) {
165
+ args.push("--reset");
166
+ }
167
+
168
+ if (this.config.quiet) {
169
+ args.push("--quiet");
170
+ }
171
+
172
+ if (this.config.ledgerPath) {
173
+ args.push("--ledger", this.config.ledgerPath);
174
+ }
175
+
176
+ // Set log level
177
+ args.push("--log");
178
+
179
+ // Clone programs
180
+ for (const programId of programs) {
181
+ args.push("--clone", programId);
182
+ }
183
+
184
+ // Clone tokens (these would be mint addresses)
185
+ for (const tokenMint of tokens) {
186
+ args.push("--clone", tokenMint);
187
+ }
188
+
189
+ // Always specify mainnet as the source for cloning
190
+ if (programs.length > 0 || tokens.length > 0) {
191
+ args.push("--url", "https://api.mainnet-beta.solana.com");
192
+ }
193
+
194
+ return args;
195
+ }
196
+
197
+ /**
198
+ * Setup process event handlers
199
+ */
200
+ private setupProcessHandlers(): void {
201
+ if (!this.process) return;
202
+
203
+ this.process.stdout?.on("data", (data: Buffer) => {
204
+ const log = data.toString().trim();
205
+ this.addLog(`[STDOUT] ${log}`);
206
+ });
207
+
208
+ this.process.stderr?.on("data", (data: Buffer) => {
209
+ const log = data.toString().trim();
210
+ this.addLog(`[STDERR] ${log}`);
211
+ });
212
+
213
+ this.process.on("error", (error) => {
214
+ this.addLog(`[ERROR] ${error.message}`);
215
+ this.updateStatus("error");
216
+ this.state.error = error.message;
217
+ });
218
+
219
+ this.process.on("exit", (code, signal) => {
220
+ this.addLog(`[EXIT] Process exited with code ${code}, signal ${signal}`);
221
+ this.cleanup();
222
+
223
+ if (this.state.status !== "stopping") {
224
+ this.updateStatus(code === 0 ? "stopped" : "error");
225
+ if (code !== 0) {
226
+ this.state.error = `Process exited with code ${code}`;
227
+ }
228
+ }
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Wait for validator to be ready
234
+ */
235
+ private async waitForReady(timeout = 30000): Promise<void> {
236
+ const startTime = Date.now();
237
+
238
+ while (Date.now() - startTime < timeout) {
239
+ try {
240
+ // Try to connect to the RPC endpoint
241
+ const response = await fetch(this.state.rpcUrl, {
242
+ method: "POST",
243
+ headers: { "Content-Type": "application/json" },
244
+ body: JSON.stringify({
245
+ jsonrpc: "2.0",
246
+ id: 1,
247
+ method: "getHealth",
248
+ }),
249
+ });
250
+
251
+ if (response.ok) {
252
+ return; // Validator is ready
253
+ }
254
+ } catch (error) {
255
+ // Continue waiting
256
+ }
257
+
258
+ await new Promise((resolve) => setTimeout(resolve, 1000));
259
+ }
260
+
261
+ throw new Error("Validator failed to start within timeout period");
262
+ }
263
+
264
+ /**
265
+ * Update validator status
266
+ */
267
+ private updateStatus(status: ValidatorStatus): void {
268
+ this.state.status = status;
269
+ this.addLog(`[STATUS] Validator status changed to: ${status}`);
270
+ }
271
+
272
+ /**
273
+ * Add log entry
274
+ */
275
+ private addLog(message: string): void {
276
+ const timestamp = new Date().toISOString();
277
+ const logEntry = `[${timestamp}] ${message}`;
278
+ this.state.logs.push(logEntry);
279
+
280
+ // Keep only last 1000 log entries
281
+ if (this.state.logs.length > 1000) {
282
+ this.state.logs = this.state.logs.slice(-1000);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Clean up process references
288
+ */
289
+ private cleanup(): void {
290
+ this.process = null;
291
+ this.state.pid = undefined;
292
+ this.state.startTime = undefined;
293
+ this.state.error = undefined;
294
+ }
295
295
  }