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
package/src/commands/start.ts
CHANGED
|
@@ -1,877 +1,880 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import ora from "ora";
|
|
3
2
|
import { spawn } from "child_process";
|
|
4
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import ora from "ora";
|
|
5
5
|
import { join } from "path";
|
|
6
|
-
import { runCommand, checkSolanaTools } from "../utils/shell.js";
|
|
7
6
|
import { configManager } from "../config/manager.js";
|
|
8
|
-
import { TokenCloner } from "../services/token-cloner.js";
|
|
9
|
-
import { processRegistry } from "../services/process-registry.js";
|
|
10
7
|
import { portManager } from "../services/port-manager.js";
|
|
11
|
-
|
|
12
|
-
import type { Config, TokenConfig } from "../types/config.js";
|
|
13
|
-
import type { ClonedToken } from "../services/token-cloner.js";
|
|
14
8
|
import type { RunningValidator } from "../services/process-registry.js";
|
|
9
|
+
import { processRegistry } from "../services/process-registry.js";
|
|
10
|
+
import type { ClonedToken } from "../services/token-cloner.js";
|
|
11
|
+
import { TokenCloner } from "../services/token-cloner.js";
|
|
12
|
+
import type { Config, TokenConfig } from "../types/config.js";
|
|
13
|
+
import { checkSolanaTools, runCommand } from "../utils/shell.js";
|
|
15
14
|
|
|
16
15
|
function generateValidatorId(name: string): string {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const timestamp = Date.now();
|
|
17
|
+
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
18
|
+
const safeName = name.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
|
|
19
|
+
return `${safeName}-${timestamp}-${randomSuffix}`;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export async function startCommand(
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
debug: boolean = false,
|
|
24
|
+
network: boolean = false,
|
|
26
25
|
): Promise<void> {
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
26
|
+
// Check prerequisites
|
|
27
|
+
const tools = await checkSolanaTools();
|
|
28
|
+
if (!tools.solana) {
|
|
29
|
+
console.error(chalk.red("❌ solana CLI not found"));
|
|
30
|
+
console.log(
|
|
31
|
+
chalk.yellow(
|
|
32
|
+
"💡 Install it with: sh -c \"$(curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash)\"",
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Load configuration
|
|
39
|
+
let config: Config;
|
|
40
|
+
try {
|
|
41
|
+
await configManager.load("./sf.config.json");
|
|
42
|
+
config = configManager.getConfig();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(chalk.red("❌ Failed to load sf.config.json"));
|
|
45
|
+
console.error(
|
|
46
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.yellow("💡 Run `solforge init` to create a configuration"),
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if validator is already running on configured ports first
|
|
55
|
+
const checkResult = await runCommand(
|
|
56
|
+
"curl",
|
|
57
|
+
[
|
|
58
|
+
"-s",
|
|
59
|
+
"-X",
|
|
60
|
+
"POST",
|
|
61
|
+
`http://127.0.0.1:${config.localnet.port}`,
|
|
62
|
+
"-H",
|
|
63
|
+
"Content-Type: application/json",
|
|
64
|
+
"-d",
|
|
65
|
+
'{"jsonrpc":"2.0","id":1,"method":"getHealth"}',
|
|
66
|
+
],
|
|
67
|
+
{ silent: true, debug: false },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (checkResult.success && checkResult.stdout.includes("ok")) {
|
|
71
|
+
console.log(chalk.yellow("⚠️ Validator is already running"));
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.cyan(`🌐 RPC URL: http://127.0.0.1:${config.localnet.port}`),
|
|
74
|
+
);
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.cyan(
|
|
77
|
+
`💰 Faucet URL: http://127.0.0.1:${config.localnet.faucetPort}`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Clone tokens if needed, even when validator is already running
|
|
82
|
+
const clonedTokens: ClonedToken[] = [];
|
|
83
|
+
if (config.tokens.length > 0) {
|
|
84
|
+
const tokenCloner = new TokenCloner();
|
|
85
|
+
|
|
86
|
+
// Check which tokens are already cloned and which need to be cloned
|
|
87
|
+
const { existingTokens, tokensToClone } = await checkExistingClonedTokens(
|
|
88
|
+
config.tokens,
|
|
89
|
+
tokenCloner,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (existingTokens.length > 0) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.green(
|
|
95
|
+
`📁 Found ${existingTokens.length} already cloned tokens`,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
if (debug) {
|
|
99
|
+
existingTokens.forEach((token: ClonedToken) => {
|
|
100
|
+
console.log(
|
|
101
|
+
chalk.gray(
|
|
102
|
+
` ✓ ${token.config.symbol} (${token.config.mainnetMint})`,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
clonedTokens.push(...existingTokens);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (tokensToClone.length > 0) {
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.yellow(
|
|
113
|
+
`📦 Cloning ${tokensToClone.length} new tokens from mainnet...\n`,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
try {
|
|
117
|
+
const newlyClonedTokens = await tokenCloner.cloneTokens(
|
|
118
|
+
tokensToClone,
|
|
119
|
+
config.localnet.rpc,
|
|
120
|
+
debug,
|
|
121
|
+
);
|
|
122
|
+
clonedTokens.push(...newlyClonedTokens);
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.green(
|
|
125
|
+
`✅ Successfully cloned ${newlyClonedTokens.length} new tokens\n`,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(chalk.red("❌ Failed to clone tokens:"));
|
|
130
|
+
console.error(
|
|
131
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
132
|
+
);
|
|
133
|
+
console.log(
|
|
134
|
+
chalk.yellow(
|
|
135
|
+
"💡 You can start without tokens by removing them from sf.config.json",
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
} else if (existingTokens.length > 0) {
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.green("✅ All tokens already cloned, skipping clone step\n"),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Airdrop SOL to mint authority if tokens were cloned (even when validator already running)
|
|
148
|
+
if (clonedTokens.length > 0) {
|
|
149
|
+
console.log(chalk.yellow("\n💸 Airdropping SOL to mint authority..."));
|
|
150
|
+
const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await airdropSolToMintAuthority(clonedTokens[0], rpcUrl, debug);
|
|
154
|
+
console.log(chalk.green("✅ SOL airdropped successfully!"));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(chalk.red("❌ Failed to airdrop SOL:"));
|
|
157
|
+
console.error(
|
|
158
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
159
|
+
);
|
|
160
|
+
console.log(
|
|
161
|
+
chalk.yellow(
|
|
162
|
+
"💡 You may need to manually airdrop SOL for fee payments",
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Still mint tokens if any were cloned
|
|
169
|
+
if (clonedTokens.length > 0) {
|
|
170
|
+
console.log(chalk.yellow("\n💰 Minting tokens..."));
|
|
171
|
+
const tokenCloner = new TokenCloner();
|
|
172
|
+
const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
|
|
173
|
+
|
|
174
|
+
if (debug) {
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.gray(`🐛 Minting ${clonedTokens.length} tokens to recipients:`),
|
|
177
|
+
);
|
|
178
|
+
clonedTokens.forEach((token, index) => {
|
|
179
|
+
console.log(
|
|
180
|
+
chalk.gray(
|
|
181
|
+
` ${index + 1}. ${token.config.symbol} (${
|
|
182
|
+
token.config.mainnetMint
|
|
183
|
+
}) - ${token.config.mintAmount} tokens`,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
console.log(chalk.gray(`🌐 Using RPC: ${rpcUrl}`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await tokenCloner.mintTokensToRecipients(clonedTokens, rpcUrl, debug);
|
|
192
|
+
console.log(chalk.green("✅ Token minting completed!"));
|
|
193
|
+
|
|
194
|
+
if (debug) {
|
|
195
|
+
console.log(
|
|
196
|
+
chalk.gray(
|
|
197
|
+
"🐛 All tokens have been minted to their respective recipients",
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(chalk.red("❌ Failed to mint tokens:"));
|
|
203
|
+
console.error(
|
|
204
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
205
|
+
);
|
|
206
|
+
console.log(
|
|
207
|
+
chalk.yellow("💡 Validator is still running, you can mint manually"),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Generate unique ID for this validator instance
|
|
215
|
+
const validatorId = generateValidatorId(config.name);
|
|
216
|
+
|
|
217
|
+
// Get available ports (only if validator is not already running)
|
|
218
|
+
const ports = await portManager.getRecommendedPorts(config);
|
|
219
|
+
if (
|
|
220
|
+
ports.rpcPort !== config.localnet.port ||
|
|
221
|
+
ports.faucetPort !== config.localnet.faucetPort
|
|
222
|
+
) {
|
|
223
|
+
console.log(
|
|
224
|
+
chalk.yellow(
|
|
225
|
+
`⚠️ Configured ports not available, using: RPC ${ports.rpcPort}, Faucet ${ports.faucetPort}`,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
// Update config with available ports
|
|
229
|
+
config.localnet.port = ports.rpcPort;
|
|
230
|
+
config.localnet.faucetPort = ports.faucetPort;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(chalk.blue(`🚀 Starting ${config.name} (${validatorId})...\n`));
|
|
234
|
+
console.log(chalk.gray(`📡 RPC Port: ${config.localnet.port}`));
|
|
235
|
+
console.log(chalk.gray(`💰 Faucet Port: ${config.localnet.faucetPort}\n`));
|
|
236
|
+
|
|
237
|
+
// Programs will be cloned automatically by validator using --clone-program flags
|
|
238
|
+
if (config.programs.length > 0) {
|
|
239
|
+
console.log(
|
|
240
|
+
chalk.cyan(
|
|
241
|
+
`🔧 Will clone ${config.programs.length} programs from mainnet during startup\n`,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Clone tokens after programs
|
|
247
|
+
const clonedTokens: ClonedToken[] = [];
|
|
248
|
+
if (config.tokens.length > 0) {
|
|
249
|
+
const tokenCloner = new TokenCloner();
|
|
250
|
+
|
|
251
|
+
// Check which tokens are already cloned and which need to be cloned
|
|
252
|
+
const { existingTokens, tokensToClone } = await checkExistingClonedTokens(
|
|
253
|
+
config.tokens,
|
|
254
|
+
tokenCloner,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (existingTokens.length > 0) {
|
|
258
|
+
console.log(
|
|
259
|
+
chalk.green(`📁 Found ${existingTokens.length} already cloned tokens`),
|
|
260
|
+
);
|
|
261
|
+
if (debug) {
|
|
262
|
+
existingTokens.forEach((token: ClonedToken) => {
|
|
263
|
+
console.log(
|
|
264
|
+
chalk.gray(
|
|
265
|
+
` ✓ ${token.config.symbol} (${token.config.mainnetMint})`,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
clonedTokens.push(...existingTokens);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (tokensToClone.length > 0) {
|
|
274
|
+
console.log(
|
|
275
|
+
chalk.yellow(
|
|
276
|
+
`📦 Cloning ${tokensToClone.length} new tokens from mainnet...\n`,
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
try {
|
|
280
|
+
const newlyClonedTokens = await tokenCloner.cloneTokens(
|
|
281
|
+
tokensToClone,
|
|
282
|
+
config.localnet.rpc,
|
|
283
|
+
debug,
|
|
284
|
+
);
|
|
285
|
+
clonedTokens.push(...newlyClonedTokens);
|
|
286
|
+
console.log(
|
|
287
|
+
chalk.green(
|
|
288
|
+
`✅ Successfully cloned ${newlyClonedTokens.length} new tokens\n`,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(chalk.red("❌ Failed to clone tokens:"));
|
|
293
|
+
console.error(
|
|
294
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
295
|
+
);
|
|
296
|
+
console.log(
|
|
297
|
+
chalk.yellow(
|
|
298
|
+
"💡 You can start without tokens by removing them from sf.config.json",
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
} else if (existingTokens.length > 0) {
|
|
304
|
+
console.log(
|
|
305
|
+
chalk.green("✅ All tokens already cloned, skipping clone step\n"),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Build validator command arguments
|
|
311
|
+
const args = buildValidatorArgs(config, clonedTokens);
|
|
312
|
+
|
|
313
|
+
console.log(chalk.gray("Command to run:"));
|
|
314
|
+
console.log(chalk.gray(`solana-test-validator ${args.join(" ")}\n`));
|
|
315
|
+
|
|
316
|
+
if (debug) {
|
|
317
|
+
console.log(chalk.yellow("🐛 Debug mode enabled"));
|
|
318
|
+
console.log(chalk.gray("Full command details:"));
|
|
319
|
+
console.log(chalk.gray(` Command: solana-test-validator`));
|
|
320
|
+
console.log(chalk.gray(` Arguments: ${JSON.stringify(args, null, 2)}`));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Start the validator
|
|
324
|
+
const spinner = ora("Starting Solana test validator...").start();
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// Start validator in background
|
|
328
|
+
const validatorProcess = await startValidatorInBackground(
|
|
329
|
+
"solana-test-validator",
|
|
330
|
+
args,
|
|
331
|
+
debug,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Wait for validator to be ready
|
|
335
|
+
spinner.text = "Waiting for validator to be ready...";
|
|
336
|
+
await waitForValidatorReady(
|
|
337
|
+
`http://127.0.0.1:${config.localnet.port}`,
|
|
338
|
+
debug,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Find an available port for the API server
|
|
342
|
+
let apiServerPort = 3000;
|
|
343
|
+
while (!(await portManager.isPortAvailable(apiServerPort))) {
|
|
344
|
+
apiServerPort++;
|
|
345
|
+
if (apiServerPort > 3100) {
|
|
346
|
+
throw new Error("Could not find available port for API server");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Start the API server as a background process
|
|
351
|
+
let apiServerPid: number | undefined;
|
|
352
|
+
let apiResult: { success: boolean; error?: string } = {
|
|
353
|
+
success: false,
|
|
354
|
+
error: "Not started",
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const currentDir = process.cwd();
|
|
359
|
+
const testpilotDir = join(__dirname, "..", "..");
|
|
360
|
+
const apiServerScript = join(testpilotDir, "src", "api-server-entry.ts");
|
|
361
|
+
const configPath =
|
|
362
|
+
configManager.getConfigPath() ?? join(currentDir, "sf.config.json");
|
|
363
|
+
const workDir = join(currentDir, ".solforge");
|
|
364
|
+
|
|
365
|
+
// Start API server in background using runCommand with nohup
|
|
366
|
+
const hostFlag = network ? ` --host "0.0.0.0"` : "";
|
|
367
|
+
const apiServerCommand = `nohup bun run "${apiServerScript}" --port ${apiServerPort} --config "${configPath}" --rpc-url "http://127.0.0.1:${config.localnet.port}" --faucet-url "http://127.0.0.1:${config.localnet.faucetPort}" --work-dir "${workDir}"${hostFlag} > /dev/null 2>&1 &`;
|
|
368
|
+
|
|
369
|
+
const startResult = await runCommand("sh", ["-c", apiServerCommand], {
|
|
370
|
+
silent: !debug,
|
|
371
|
+
debug: debug,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (startResult.success) {
|
|
375
|
+
// Wait a moment for the API server to start
|
|
376
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
377
|
+
|
|
378
|
+
// Test if the API server is responding
|
|
379
|
+
try {
|
|
380
|
+
const healthCheckHost = network ? "0.0.0.0" : "127.0.0.1";
|
|
381
|
+
const response = await fetch(
|
|
382
|
+
`http://${healthCheckHost}:${apiServerPort}/api/health`,
|
|
383
|
+
);
|
|
384
|
+
if (response.ok) {
|
|
385
|
+
apiResult = { success: true };
|
|
386
|
+
// Get the PID of the API server process
|
|
387
|
+
const pidResult = await runCommand(
|
|
388
|
+
"pgrep",
|
|
389
|
+
["-f", `api-server-entry.*--port ${apiServerPort}`],
|
|
390
|
+
{ silent: true, debug: false },
|
|
391
|
+
);
|
|
392
|
+
if (pidResult.success && pidResult.stdout.trim()) {
|
|
393
|
+
const pidLine = pidResult.stdout.trim().split("\n")[0];
|
|
394
|
+
if (pidLine) {
|
|
395
|
+
apiServerPid = parseInt(pidLine);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
apiResult = {
|
|
400
|
+
success: false,
|
|
401
|
+
error: `Health check failed: ${response.status}`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
apiResult = {
|
|
406
|
+
success: false,
|
|
407
|
+
error: `Health check failed: ${
|
|
408
|
+
error instanceof Error ? error.message : String(error)
|
|
409
|
+
}`,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
apiResult = {
|
|
414
|
+
success: false,
|
|
415
|
+
error: `Failed to start API server: ${
|
|
416
|
+
startResult.stderr || "Unknown error"
|
|
417
|
+
}`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
apiResult = {
|
|
422
|
+
success: false,
|
|
423
|
+
error: error instanceof Error ? error.message : String(error),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!apiResult.success) {
|
|
428
|
+
console.warn(
|
|
429
|
+
chalk.yellow("⚠️ Failed to start API server:", apiResult.error),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Register the running validator
|
|
434
|
+
const runningValidator: RunningValidator = {
|
|
435
|
+
id: validatorId,
|
|
436
|
+
name: config.name,
|
|
437
|
+
pid: validatorProcess.pid!,
|
|
438
|
+
rpcPort: config.localnet.port,
|
|
439
|
+
faucetPort: config.localnet.faucetPort,
|
|
440
|
+
rpcUrl: `http://127.0.0.1:${config.localnet.port}`,
|
|
441
|
+
faucetUrl: `http://127.0.0.1:${config.localnet.faucetPort}`,
|
|
442
|
+
configPath: configManager.getConfigPath() || "./sf.config.json",
|
|
443
|
+
startTime: new Date(),
|
|
444
|
+
status: "running",
|
|
445
|
+
apiServerPort: apiResult.success ? apiServerPort : undefined,
|
|
446
|
+
apiServerUrl: apiResult.success
|
|
447
|
+
? `http://${network ? "0.0.0.0" : "127.0.0.1"}:${apiServerPort}`
|
|
448
|
+
: undefined,
|
|
449
|
+
apiServerPid: apiResult.success ? apiServerPid : undefined,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
processRegistry.register(runningValidator);
|
|
453
|
+
|
|
454
|
+
// Validator is now ready
|
|
455
|
+
spinner.succeed("Validator started successfully!");
|
|
456
|
+
|
|
457
|
+
console.log(chalk.green("✅ Localnet is running!"));
|
|
458
|
+
console.log(chalk.gray(`🆔 Validator ID: ${validatorId}`));
|
|
459
|
+
console.log(
|
|
460
|
+
chalk.cyan(`🌐 RPC URL: http://127.0.0.1:${config.localnet.port}`),
|
|
461
|
+
);
|
|
462
|
+
console.log(
|
|
463
|
+
chalk.cyan(
|
|
464
|
+
`💰 Faucet URL: http://127.0.0.1:${config.localnet.faucetPort}`,
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
if (apiResult.success) {
|
|
468
|
+
const displayHost = network ? "0.0.0.0" : "127.0.0.1";
|
|
469
|
+
console.log(
|
|
470
|
+
chalk.cyan(`🚀 API Server: http://${displayHost}:${apiServerPort}/api`),
|
|
471
|
+
);
|
|
472
|
+
if (network) {
|
|
473
|
+
console.log(
|
|
474
|
+
chalk.yellow(
|
|
475
|
+
" 🌐 Network mode enabled - API server accessible from other devices",
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Airdrop SOL to mint authority if tokens were cloned
|
|
482
|
+
if (clonedTokens.length > 0) {
|
|
483
|
+
console.log(chalk.yellow("\n💸 Airdropping SOL to mint authority..."));
|
|
484
|
+
const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
await airdropSolToMintAuthority(clonedTokens[0], rpcUrl, debug);
|
|
488
|
+
console.log(chalk.green("✅ SOL airdropped successfully!"));
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error(chalk.red("❌ Failed to airdrop SOL:"));
|
|
491
|
+
console.error(
|
|
492
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
493
|
+
);
|
|
494
|
+
console.log(
|
|
495
|
+
chalk.yellow(
|
|
496
|
+
"💡 You may need to manually airdrop SOL for fee payments",
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Mint tokens if any were cloned
|
|
503
|
+
if (clonedTokens.length > 0) {
|
|
504
|
+
console.log(chalk.yellow("\n💰 Minting tokens..."));
|
|
505
|
+
const tokenCloner = new TokenCloner();
|
|
506
|
+
const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
|
|
507
|
+
|
|
508
|
+
if (debug) {
|
|
509
|
+
console.log(
|
|
510
|
+
chalk.gray(`🐛 Minting ${clonedTokens.length} tokens to recipients:`),
|
|
511
|
+
);
|
|
512
|
+
clonedTokens.forEach((token, index) => {
|
|
513
|
+
console.log(
|
|
514
|
+
chalk.gray(
|
|
515
|
+
` ${index + 1}. ${token.config.symbol} (${
|
|
516
|
+
token.config.mainnetMint
|
|
517
|
+
}) - ${token.config.mintAmount} tokens`,
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
console.log(chalk.gray(`🌐 Using RPC: ${rpcUrl}`));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await tokenCloner.mintTokensToRecipients(clonedTokens, rpcUrl, debug);
|
|
526
|
+
console.log(chalk.green("✅ Token minting completed!"));
|
|
527
|
+
|
|
528
|
+
if (debug) {
|
|
529
|
+
console.log(
|
|
530
|
+
chalk.gray(
|
|
531
|
+
"🐛 All tokens have been minted to their respective recipients",
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.error(chalk.red("❌ Failed to mint tokens:"));
|
|
537
|
+
console.error(
|
|
538
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
539
|
+
);
|
|
540
|
+
console.log(
|
|
541
|
+
chalk.yellow("💡 Validator is still running, you can mint manually"),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (config.tokens.length > 0) {
|
|
547
|
+
console.log(chalk.yellow("\n🪙 Cloned tokens:"));
|
|
548
|
+
config.tokens.forEach((token) => {
|
|
549
|
+
console.log(
|
|
550
|
+
chalk.gray(
|
|
551
|
+
` - ${token.symbol}: ${token.mainnetMint} (${token.mintAmount} minted)`,
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (config.programs.length > 0) {
|
|
558
|
+
console.log(chalk.yellow("\n📦 Cloned programs:"));
|
|
559
|
+
config.programs.forEach((program) => {
|
|
560
|
+
const name =
|
|
561
|
+
program.name || program.mainnetProgramId.slice(0, 8) + "...";
|
|
562
|
+
console.log(chalk.gray(` - ${name}: ${program.mainnetProgramId}`));
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log(chalk.blue("\n💡 Tips:"));
|
|
567
|
+
console.log(
|
|
568
|
+
chalk.gray(" - Run `solforge list` to see all running validators"),
|
|
569
|
+
);
|
|
570
|
+
console.log(
|
|
571
|
+
chalk.gray(" - Run `solforge status` to check validator status"),
|
|
572
|
+
);
|
|
573
|
+
console.log(
|
|
574
|
+
chalk.gray(
|
|
575
|
+
` - Run \`solforge stop ${validatorId}\` to stop this validator`,
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
console.log(
|
|
579
|
+
chalk.gray(" - Run `solforge stop --all` to stop all validators"),
|
|
580
|
+
);
|
|
581
|
+
if (apiResult.success) {
|
|
582
|
+
const endpointHost = network ? "0.0.0.0" : "127.0.0.1";
|
|
583
|
+
console.log(chalk.blue("\n🔌 API Endpoints:"));
|
|
584
|
+
console.log(
|
|
585
|
+
chalk.gray(
|
|
586
|
+
` - GET http://${endpointHost}:${apiServerPort}/api/tokens - List cloned tokens`,
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
console.log(
|
|
590
|
+
chalk.gray(
|
|
591
|
+
` - GET http://${endpointHost}:${apiServerPort}/api/programs - List cloned programs`,
|
|
592
|
+
),
|
|
593
|
+
);
|
|
594
|
+
console.log(
|
|
595
|
+
chalk.gray(
|
|
596
|
+
` - POST http://${endpointHost}:${apiServerPort}/api/tokens/{mintAddress}/mint - Mint tokens`,
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
console.log(
|
|
600
|
+
chalk.gray(
|
|
601
|
+
` - POST http://${endpointHost}:${apiServerPort}/api/airdrop - Airdrop SOL`,
|
|
602
|
+
),
|
|
603
|
+
);
|
|
604
|
+
console.log(
|
|
605
|
+
chalk.gray(
|
|
606
|
+
` - GET http://${endpointHost}:${apiServerPort}/api/wallet/{address}/balances - Get balances`,
|
|
607
|
+
),
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
} catch (error) {
|
|
611
|
+
spinner.fail("Failed to start validator");
|
|
612
|
+
console.error(chalk.red("❌ Unexpected error:"));
|
|
613
|
+
console.error(
|
|
614
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
615
|
+
);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
617
618
|
}
|
|
618
619
|
|
|
619
620
|
function buildValidatorArgs(
|
|
620
|
-
|
|
621
|
-
|
|
621
|
+
config: Config,
|
|
622
|
+
clonedTokens: ClonedToken[] = [],
|
|
622
623
|
): string[] {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
624
|
+
const args: string[] = [];
|
|
625
|
+
|
|
626
|
+
// Basic configuration
|
|
627
|
+
args.push("--rpc-port", config.localnet.port.toString());
|
|
628
|
+
args.push("--faucet-port", config.localnet.faucetPort.toString());
|
|
629
|
+
args.push("--bind-address", config.localnet.bindAddress);
|
|
630
|
+
|
|
631
|
+
if (config.localnet.reset) {
|
|
632
|
+
args.push("--reset");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (config.localnet.quiet) {
|
|
636
|
+
args.push("--quiet");
|
|
637
|
+
} else {
|
|
638
|
+
// Always use quiet mode to prevent log spam in background
|
|
639
|
+
args.push("--quiet");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Add ledger size limit
|
|
643
|
+
args.push("--limit-ledger-size", config.localnet.limitLedgerSize.toString());
|
|
644
|
+
|
|
645
|
+
// Add cloned token accounts (using modified JSON files)
|
|
646
|
+
if (clonedTokens.length > 0) {
|
|
647
|
+
const tokenCloner = new TokenCloner();
|
|
648
|
+
const tokenArgs = tokenCloner.getValidatorArgs(clonedTokens);
|
|
649
|
+
args.push(...tokenArgs);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Clone programs from mainnet using built-in validator flags
|
|
653
|
+
for (const program of config.programs) {
|
|
654
|
+
if (program.upgradeable) {
|
|
655
|
+
args.push("--clone-upgradeable-program", program.mainnetProgramId);
|
|
656
|
+
} else {
|
|
657
|
+
// Use --clone for regular programs (non-upgradeable)
|
|
658
|
+
args.push("--clone", program.mainnetProgramId);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// If we're cloning programs, specify the source cluster
|
|
663
|
+
if (config.programs.length > 0) {
|
|
664
|
+
args.push("--url", config.localnet.rpc);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return args;
|
|
667
668
|
}
|
|
668
669
|
|
|
669
670
|
/**
|
|
670
671
|
* Start the validator in the background
|
|
671
672
|
*/
|
|
672
673
|
async function startValidatorInBackground(
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
674
|
+
command: string,
|
|
675
|
+
args: string[],
|
|
676
|
+
debug: boolean,
|
|
676
677
|
): Promise<{ pid: number }> {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
678
|
+
return new Promise((resolve, reject) => {
|
|
679
|
+
if (debug) {
|
|
680
|
+
console.log(chalk.gray(`Starting ${command} in background...`));
|
|
681
|
+
console.log(chalk.gray(`Command: ${command} ${args.join(" ")}`));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const child = spawn(command, args, {
|
|
685
|
+
detached: true,
|
|
686
|
+
stdio: "ignore", // Always ignore stdio to ensure it runs in background
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
child.on("error", (error) => {
|
|
690
|
+
reject(new Error(`Failed to start validator: ${error.message}`));
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Give the validator a moment to start
|
|
694
|
+
setTimeout(() => {
|
|
695
|
+
if (child.pid) {
|
|
696
|
+
child.unref(); // Allow parent to exit without waiting for child
|
|
697
|
+
if (debug) {
|
|
698
|
+
console.log(
|
|
699
|
+
chalk.gray(`✅ Validator started with PID: ${child.pid}`),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
resolve({ pid: child.pid });
|
|
703
|
+
} else {
|
|
704
|
+
reject(new Error("Validator failed to start"));
|
|
705
|
+
}
|
|
706
|
+
}, 1000);
|
|
707
|
+
});
|
|
707
708
|
}
|
|
708
709
|
|
|
709
710
|
/**
|
|
710
711
|
* Wait for the validator to be ready by polling the RPC endpoint
|
|
711
712
|
*/
|
|
712
713
|
async function waitForValidatorReady(
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
714
|
+
rpcUrl: string,
|
|
715
|
+
debug: boolean,
|
|
716
|
+
maxAttempts: number = 30,
|
|
716
717
|
): Promise<void> {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
718
|
+
let lastError: string = "";
|
|
719
|
+
|
|
720
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
721
|
+
try {
|
|
722
|
+
if (debug) {
|
|
723
|
+
console.log(
|
|
724
|
+
chalk.gray(`Attempt ${attempt}/${maxAttempts}: Checking ${rpcUrl}`),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const result = await runCommand(
|
|
729
|
+
"curl",
|
|
730
|
+
[
|
|
731
|
+
"-s",
|
|
732
|
+
"-X",
|
|
733
|
+
"POST",
|
|
734
|
+
rpcUrl,
|
|
735
|
+
"-H",
|
|
736
|
+
"Content-Type: application/json",
|
|
737
|
+
"-d",
|
|
738
|
+
'{"jsonrpc":"2.0","id":1,"method":"getHealth"}',
|
|
739
|
+
],
|
|
740
|
+
{ silent: true, debug: false },
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
if (result.success && result.stdout.includes("ok")) {
|
|
744
|
+
if (debug) {
|
|
745
|
+
console.log(
|
|
746
|
+
chalk.green(`✅ Validator is ready after ${attempt} attempts`),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Store the last error for better diagnostics
|
|
753
|
+
if (!result.success) {
|
|
754
|
+
lastError = result.stderr || "Unknown error";
|
|
755
|
+
}
|
|
756
|
+
} catch (error) {
|
|
757
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (attempt < maxAttempts) {
|
|
761
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Provide better error message
|
|
766
|
+
let errorMsg = `Validator failed to become ready after ${maxAttempts} attempts`;
|
|
767
|
+
if (lastError.includes("Connection refused")) {
|
|
768
|
+
errorMsg += `\n💡 This usually means:\n - Port ${
|
|
769
|
+
rpcUrl.split(":")[2]
|
|
770
|
+
} is already in use\n - Run 'pkill -f solana-test-validator' to kill existing validators`;
|
|
771
|
+
} else if (lastError) {
|
|
772
|
+
errorMsg += `\nLast error: ${lastError}`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
throw new Error(errorMsg);
|
|
775
776
|
}
|
|
776
777
|
|
|
777
778
|
/**
|
|
778
779
|
* Airdrop SOL to the mint authority for fee payments
|
|
779
780
|
*/
|
|
780
781
|
async function airdropSolToMintAuthority(
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
782
|
+
clonedToken: any,
|
|
783
|
+
rpcUrl: string,
|
|
784
|
+
debug: boolean = false,
|
|
784
785
|
): Promise<void> {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
786
|
+
if (debug) {
|
|
787
|
+
console.log(
|
|
788
|
+
chalk.gray(
|
|
789
|
+
`Airdropping 10 SOL to ${clonedToken.mintAuthority.publicKey}`,
|
|
790
|
+
),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const airdropResult = await runCommand(
|
|
795
|
+
"solana",
|
|
796
|
+
["airdrop", "10", clonedToken.mintAuthority.publicKey, "--url", rpcUrl],
|
|
797
|
+
{ silent: !debug, debug },
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
if (!airdropResult.success) {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`Failed to airdrop SOL: ${airdropResult.stderr || airdropResult.stdout}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (debug) {
|
|
807
|
+
console.log(chalk.gray("SOL airdrop completed"));
|
|
808
|
+
}
|
|
806
809
|
}
|
|
807
810
|
|
|
808
811
|
/**
|
|
809
812
|
* Check for existing cloned tokens and return what's already cloned vs what needs to be cloned
|
|
810
813
|
*/
|
|
811
814
|
async function checkExistingClonedTokens(
|
|
812
|
-
|
|
813
|
-
|
|
815
|
+
tokens: TokenConfig[],
|
|
816
|
+
tokenCloner: TokenCloner,
|
|
814
817
|
): Promise<{ existingTokens: ClonedToken[]; tokensToClone: TokenConfig[] }> {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
818
|
+
const existingTokens: ClonedToken[] = [];
|
|
819
|
+
const tokensToClone: TokenConfig[] = [];
|
|
820
|
+
const workDir = ".solforge";
|
|
821
|
+
|
|
822
|
+
// Check for shared mint authority
|
|
823
|
+
const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
|
|
824
|
+
let sharedMintAuthority: { publicKey: string; secretKey: number[] } | null =
|
|
825
|
+
null;
|
|
826
|
+
|
|
827
|
+
if (existsSync(sharedMintAuthorityPath)) {
|
|
828
|
+
try {
|
|
829
|
+
const fileContent = JSON.parse(
|
|
830
|
+
readFileSync(sharedMintAuthorityPath, "utf8"),
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
if (Array.isArray(fileContent)) {
|
|
834
|
+
// New format: file contains just the secret key array
|
|
835
|
+
const { Keypair } = await import("@solana/web3.js");
|
|
836
|
+
const keypair = Keypair.fromSecretKey(new Uint8Array(fileContent));
|
|
837
|
+
sharedMintAuthority = {
|
|
838
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
839
|
+
secretKey: Array.from(keypair.secretKey),
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// Check metadata for consistency
|
|
843
|
+
const metadataPath = join(workDir, "shared-mint-authority-meta.json");
|
|
844
|
+
if (existsSync(metadataPath)) {
|
|
845
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
846
|
+
if (metadata.publicKey !== sharedMintAuthority.publicKey) {
|
|
847
|
+
sharedMintAuthority.publicKey = metadata.publicKey;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
// Old format: file contains {publicKey, secretKey}
|
|
852
|
+
sharedMintAuthority = fileContent;
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
// If we can't read the shared mint authority, treat all tokens as needing to be cloned
|
|
856
|
+
sharedMintAuthority = null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
for (const token of tokens) {
|
|
861
|
+
const tokenDir = join(workDir, `token-${token.symbol.toLowerCase()}`);
|
|
862
|
+
const modifiedAccountPath = join(tokenDir, "modified.json");
|
|
863
|
+
|
|
864
|
+
// Check if this token has already been cloned
|
|
865
|
+
if (existsSync(modifiedAccountPath) && sharedMintAuthority) {
|
|
866
|
+
// Token appears to be already cloned
|
|
867
|
+
existingTokens.push({
|
|
868
|
+
config: token,
|
|
869
|
+
mintAuthorityPath: sharedMintAuthorityPath,
|
|
870
|
+
modifiedAccountPath,
|
|
871
|
+
mintAuthority: sharedMintAuthority,
|
|
872
|
+
});
|
|
873
|
+
} else {
|
|
874
|
+
// Token needs to be cloned
|
|
875
|
+
tokensToClone.push(token);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return { existingTokens, tokensToClone };
|
|
877
880
|
}
|