solforge 0.2.3 → 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.
- package/LICENSE +2 -2
- package/README.md +323 -364
- package/cli.cjs +126 -69
- package/package.json +1 -1
- package/scripts/install.sh +112 -0
- package/scripts/postinstall.cjs +66 -58
- package/server/methods/program/get-token-accounts-by-owner.ts +7 -2
- package/server/ws-server.ts +4 -1
- package/src/api-server-entry.ts +91 -91
- package/src/cli/commands/rpc-start.ts +4 -1
- package/src/cli/main.ts +39 -14
- package/src/cli/run-solforge.ts +20 -6
- package/src/commands/add-program.ts +324 -328
- package/src/commands/init.ts +106 -106
- package/src/commands/list.ts +125 -125
- package/src/commands/mint.ts +246 -246
- package/src/commands/start.ts +834 -831
- package/src/commands/status.ts +80 -80
- package/src/commands/stop.ts +381 -382
- package/src/config/manager.ts +149 -149
- package/src/gui/public/app.css +1556 -1
- package/src/gui/public/build/main.css +1569 -1
- package/src/gui/server.ts +20 -21
- package/src/gui/src/app.tsx +56 -37
- package/src/gui/src/components/airdrop-mint-form.tsx +17 -11
- package/src/gui/src/components/clone-program-modal.tsx +6 -6
- package/src/gui/src/components/clone-token-modal.tsx +7 -7
- package/src/gui/src/components/modal.tsx +13 -11
- package/src/gui/src/components/programs-panel.tsx +27 -15
- package/src/gui/src/components/status-panel.tsx +31 -17
- package/src/gui/src/components/tokens-panel.tsx +25 -19
- package/src/gui/src/index.css +491 -463
- package/src/index.ts +161 -146
- package/src/rpc/start.ts +1 -1
- package/src/services/api-server.ts +470 -473
- package/src/services/port-manager.ts +167 -167
- package/src/services/process-registry.ts +143 -143
- package/src/services/program-cloner.ts +312 -312
- package/src/services/token-cloner.ts +799 -797
- package/src/services/validator.ts +288 -288
- package/src/types/config.ts +71 -71
- package/src/utils/shell.ts +75 -75
- package/src/utils/token-loader.ts +77 -77
|
@@ -1,295 +1,295 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
LocalnetConfig,
|
|
6
|
+
OperationResult,
|
|
7
|
+
ValidatorState,
|
|
8
|
+
ValidatorStatus,
|
|
9
9
|
} from "../types/config.js";
|
|
10
10
|
|
|
11
11
|
export class ValidatorService {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
}
|