solforge 0.1.1 → 0.1.2

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.
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { select } from "@inquirer/prompts";
3
3
  import { processRegistry } from "../services/process-registry.js";
4
+
4
5
  import type { RunningValidator } from "../services/process-registry.js";
5
6
 
6
7
  export async function stopCommand(
@@ -54,9 +55,7 @@ export async function stopCommand(
54
55
  console.log(
55
56
  chalk.gray("💡 Use `solforge stop --all` to stop all validators")
56
57
  );
57
- console.log(
58
- chalk.gray("💡 Use `solforge list` to see running validators")
59
- );
58
+ console.log(chalk.gray("💡 Use `solforge list` to see running validators"));
60
59
  return;
61
60
  }
62
61
 
@@ -109,6 +108,24 @@ async function stopValidator(
109
108
  forceKill: boolean = false
110
109
  ): Promise<{ success: boolean; error?: string }> {
111
110
  try {
111
+ // Stop the API server first if it has a PID
112
+ if (validator.apiServerPid) {
113
+ try {
114
+ process.kill(validator.apiServerPid, "SIGTERM");
115
+ console.log(
116
+ chalk.gray(`📡 Stopped API server (PID: ${validator.apiServerPid})`)
117
+ );
118
+ } catch (error) {
119
+ console.log(
120
+ chalk.yellow(
121
+ `⚠️ Warning: Failed to stop API server: ${
122
+ error instanceof Error ? error.message : String(error)
123
+ }`
124
+ )
125
+ );
126
+ }
127
+ }
128
+
112
129
  // Check if process is still running
113
130
  const isRunning = await processRegistry.isProcessRunning(validator.pid);
114
131
  if (!isRunning) {
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import { resolve } from "path";
7
7
  import { initCommand } from "./commands/init.js";
8
8
  import { statusCommand } from "./commands/status.js";
9
9
  import { startCommand } from "./commands/start.js";
10
- import { transferCommand } from "./commands/transfer.js";
10
+ import { mintCommand } from "./commands/mint.js";
11
11
  import { listCommand } from "./commands/list.js";
12
12
  import { stopCommand, killCommand } from "./commands/stop.js";
13
13
  import { addProgramCommand } from "./commands/add-program.js";
@@ -94,7 +94,7 @@ program
94
94
  await statusCommand();
95
95
  });
96
96
 
97
- program.addCommand(transferCommand);
97
+ program.addCommand(mintCommand);
98
98
 
99
99
  program
100
100
  .command("reset")
@@ -0,0 +1,519 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import { Server } from "http";
4
+ import { spawn, ChildProcess } from "child_process";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { join } from "path";
7
+ import chalk from "chalk";
8
+ import { Connection, PublicKey, Keypair } from "@solana/web3.js";
9
+ import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
10
+ import { runCommand } from "../utils/shell.js";
11
+ import { TokenCloner } from "./token-cloner.js";
12
+ import { ProgramCloner } from "./program-cloner.js";
13
+ import { mintTokenToWallet as mintTokenToWalletShared } from "../commands/mint.js";
14
+ import type { Config } from "../types/config.js";
15
+ import type { ClonedToken } from "./token-cloner.js";
16
+
17
+ export interface APIServerConfig {
18
+ port: number;
19
+ validatorRpcUrl: string;
20
+ validatorFaucetUrl: string;
21
+ config: Config;
22
+ workDir: string;
23
+ }
24
+
25
+ export class APIServer {
26
+ private app: express.Application;
27
+ private server: Server | null = null;
28
+ private config: APIServerConfig;
29
+ private tokenCloner: TokenCloner;
30
+ private programCloner: ProgramCloner;
31
+ private connection: Connection;
32
+
33
+ constructor(config: APIServerConfig) {
34
+ this.config = config;
35
+ this.tokenCloner = new TokenCloner(config.workDir);
36
+ this.programCloner = new ProgramCloner(config.workDir);
37
+ this.connection = new Connection(config.validatorRpcUrl, "confirmed");
38
+
39
+ this.app = express();
40
+ this.setupMiddleware();
41
+ this.setupRoutes();
42
+ }
43
+
44
+ private setupMiddleware(): void {
45
+ this.app.use(cors());
46
+ this.app.use(express.json());
47
+
48
+ // Request logging
49
+ this.app.use((req, res, next) => {
50
+ console.log(chalk.gray(`🌐 API: ${req.method} ${req.path}`));
51
+ next();
52
+ });
53
+ }
54
+
55
+ private setupRoutes(): void {
56
+ const router = express.Router();
57
+
58
+ // Health check
59
+ router.get("/health", (req, res) => {
60
+ res.json({ status: "ok", timestamp: new Date().toISOString() });
61
+ });
62
+
63
+ // Get validator info
64
+ router.get("/validator/info", async (req, res) => {
65
+ try {
66
+ const version = await this.connection.getVersion();
67
+ const blockHeight = await this.connection.getBlockHeight();
68
+ const slotLeader = await this.connection.getSlotLeader();
69
+
70
+ res.json({
71
+ version,
72
+ blockHeight,
73
+ slotLeader: slotLeader.toString(),
74
+ rpcUrl: this.config.validatorRpcUrl,
75
+ faucetUrl: this.config.validatorFaucetUrl,
76
+ });
77
+ } catch (error) {
78
+ res.status(500).json({
79
+ error: "Failed to fetch validator info",
80
+ details: error instanceof Error ? error.message : String(error),
81
+ });
82
+ }
83
+ });
84
+
85
+ // Get all cloned tokens
86
+ router.get("/tokens", async (req, res) => {
87
+ try {
88
+ const clonedTokens = await this.getClonedTokens();
89
+ res.json({
90
+ tokens: clonedTokens.map((token) => ({
91
+ symbol: token.config.symbol,
92
+ mainnetMint: token.config.mainnetMint,
93
+ mintAuthority: token.mintAuthority.publicKey,
94
+ recipients: token.config.recipients,
95
+ cloneMetadata: token.config.cloneMetadata,
96
+ })),
97
+ count: clonedTokens.length,
98
+ });
99
+ } catch (error) {
100
+ res.status(500).json({
101
+ error: "Failed to fetch cloned tokens",
102
+ details: error instanceof Error ? error.message : String(error),
103
+ });
104
+ }
105
+ });
106
+
107
+ // Get all cloned programs
108
+ router.get("/programs", async (req, res) => {
109
+ try {
110
+ const clonedPrograms = await this.getClonedPrograms();
111
+ res.json({
112
+ programs: clonedPrograms,
113
+ count: clonedPrograms.length,
114
+ });
115
+ } catch (error) {
116
+ res.status(500).json({
117
+ error: "Failed to fetch cloned programs",
118
+ details: error instanceof Error ? error.message : String(error),
119
+ });
120
+ }
121
+ });
122
+
123
+ // Mint tokens to a wallet
124
+ router.post("/tokens/:symbol/mint", async (req, res) => {
125
+ try {
126
+ const { symbol } = req.params;
127
+ const { walletAddress, amount } = req.body;
128
+
129
+ if (!walletAddress || !amount) {
130
+ return res.status(400).json({
131
+ error: "Missing required fields: walletAddress and amount",
132
+ });
133
+ }
134
+
135
+ // Validate wallet address
136
+ try {
137
+ new PublicKey(walletAddress);
138
+ } catch {
139
+ return res.status(400).json({
140
+ error: "Invalid wallet address",
141
+ });
142
+ }
143
+
144
+ // Validate amount
145
+ if (!Number.isInteger(amount) || amount <= 0) {
146
+ return res.status(400).json({
147
+ error: "Amount must be a positive integer",
148
+ });
149
+ }
150
+
151
+ const result = await this.mintTokenToWallet(
152
+ symbol,
153
+ walletAddress,
154
+ amount
155
+ );
156
+ res.json(result);
157
+ } catch (error) {
158
+ res.status(500).json({
159
+ error: "Failed to mint tokens",
160
+ details: error instanceof Error ? error.message : String(error),
161
+ });
162
+ }
163
+ });
164
+
165
+ // Get account balances for a wallet
166
+ router.get("/wallet/:address/balances", async (req, res) => {
167
+ try {
168
+ const { address } = req.params;
169
+
170
+ // Validate wallet address
171
+ try {
172
+ new PublicKey(address);
173
+ } catch {
174
+ return res.status(400).json({
175
+ error: "Invalid wallet address",
176
+ });
177
+ }
178
+
179
+ const balances = await this.getWalletBalances(address);
180
+ res.json(balances);
181
+ } catch (error) {
182
+ res.status(500).json({
183
+ error: "Failed to fetch wallet balances",
184
+ details: error instanceof Error ? error.message : String(error),
185
+ });
186
+ }
187
+ });
188
+
189
+ // Airdrop SOL to a wallet
190
+ router.post("/airdrop", async (req, res) => {
191
+ try {
192
+ const { walletAddress, amount } = req.body;
193
+
194
+ if (!walletAddress || !amount) {
195
+ return res.status(400).json({
196
+ error: "Missing required fields: walletAddress and amount",
197
+ });
198
+ }
199
+
200
+ // Validate wallet address
201
+ try {
202
+ new PublicKey(walletAddress);
203
+ } catch {
204
+ return res.status(400).json({
205
+ error: "Invalid wallet address",
206
+ });
207
+ }
208
+
209
+ const result = await this.airdropSol(walletAddress, amount);
210
+ res.json(result);
211
+ } catch (error) {
212
+ res.status(500).json({
213
+ error: "Failed to airdrop SOL",
214
+ details: error instanceof Error ? error.message : String(error),
215
+ });
216
+ }
217
+ });
218
+
219
+ // Get recent transactions
220
+ router.get("/transactions/recent", async (req, res) => {
221
+ try {
222
+ const limit = Math.min(parseInt(req.query.limit as string) || 10, 100);
223
+ const signatures = await this.connection.getSignaturesForAddress(
224
+ new PublicKey("11111111111111111111111111111111"), // System program
225
+ { limit }
226
+ );
227
+
228
+ res.json({
229
+ transactions: signatures,
230
+ count: signatures.length,
231
+ });
232
+ } catch (error) {
233
+ res.status(500).json({
234
+ error: "Failed to fetch recent transactions",
235
+ details: error instanceof Error ? error.message : String(error),
236
+ });
237
+ }
238
+ });
239
+
240
+ this.app.use("/api", router);
241
+
242
+ // 404 handler
243
+ this.app.use("*", (req, res) => {
244
+ res.status(404).json({ error: "Endpoint not found" });
245
+ });
246
+ }
247
+
248
+ private async getClonedTokens(): Promise<ClonedToken[]> {
249
+ const clonedTokens: ClonedToken[] = [];
250
+
251
+ for (const tokenConfig of this.config.config.tokens) {
252
+ const tokenDir = join(
253
+ this.config.workDir,
254
+ `token-${tokenConfig.symbol.toLowerCase()}`
255
+ );
256
+ const modifiedAccountPath = join(tokenDir, "modified.json");
257
+ const sharedMintAuthorityPath = join(
258
+ this.config.workDir,
259
+ "shared-mint-authority.json"
260
+ );
261
+
262
+ if (
263
+ existsSync(modifiedAccountPath) &&
264
+ existsSync(sharedMintAuthorityPath)
265
+ ) {
266
+ try {
267
+ const mintAuthorityData = JSON.parse(
268
+ readFileSync(sharedMintAuthorityPath, "utf8")
269
+ );
270
+ let mintAuthority;
271
+
272
+ if (Array.isArray(mintAuthorityData)) {
273
+ const keypair = Keypair.fromSecretKey(
274
+ new Uint8Array(mintAuthorityData)
275
+ );
276
+ mintAuthority = {
277
+ publicKey: keypair.publicKey.toBase58(),
278
+ secretKey: Array.from(keypair.secretKey),
279
+ };
280
+ } else {
281
+ mintAuthority = mintAuthorityData;
282
+ }
283
+
284
+ clonedTokens.push({
285
+ config: tokenConfig,
286
+ mintAuthorityPath: sharedMintAuthorityPath,
287
+ modifiedAccountPath,
288
+ mintAuthority,
289
+ });
290
+ } catch (error) {
291
+ console.error(
292
+ `Failed to load cloned token ${tokenConfig.symbol}:`,
293
+ error
294
+ );
295
+ }
296
+ }
297
+ }
298
+
299
+ return clonedTokens;
300
+ }
301
+
302
+ private async getClonedPrograms(): Promise<
303
+ Array<{ name?: string; programId: string; filePath?: string }>
304
+ > {
305
+ const clonedPrograms: Array<{
306
+ name?: string;
307
+ programId: string;
308
+ filePath?: string;
309
+ }> = [];
310
+
311
+ for (const programConfig of this.config.config.programs) {
312
+ const programsDir = join(this.config.workDir, "programs");
313
+ const fileName = programConfig.name
314
+ ? `${programConfig.name.toLowerCase().replace(/\s+/g, "-")}.so`
315
+ : `${programConfig.mainnetProgramId}.so`;
316
+ const filePath = join(programsDir, fileName);
317
+
318
+ clonedPrograms.push({
319
+ name: programConfig.name,
320
+ programId: programConfig.mainnetProgramId,
321
+ filePath: existsSync(filePath) ? filePath : undefined,
322
+ });
323
+ }
324
+
325
+ return clonedPrograms;
326
+ }
327
+
328
+ private async mintTokenToWallet(
329
+ symbol: string,
330
+ walletAddress: string,
331
+ amount: number
332
+ ): Promise<any> {
333
+ const clonedTokens = await this.getClonedTokens();
334
+ const token = clonedTokens.find(
335
+ (t) => t.config.symbol.toLowerCase() === symbol.toLowerCase()
336
+ );
337
+
338
+ if (!token) {
339
+ throw new Error(`Token ${symbol} not found in cloned tokens`);
340
+ }
341
+
342
+ // Use the shared minting function from the mint command
343
+ await mintTokenToWalletShared(
344
+ token,
345
+ walletAddress,
346
+ amount,
347
+ this.config.validatorRpcUrl
348
+ );
349
+
350
+ return {
351
+ success: true,
352
+ symbol: token.config.symbol,
353
+ amount,
354
+ walletAddress,
355
+ mintAddress: token.config.mainnetMint,
356
+ };
357
+ }
358
+
359
+ private async getWalletBalances(walletAddress: string): Promise<any> {
360
+ try {
361
+ const publicKey = new PublicKey(walletAddress);
362
+
363
+ // Get SOL balance
364
+ const solBalance = await this.connection.getBalance(publicKey);
365
+
366
+ // Get token accounts
367
+ const tokenAccounts = await this.connection.getTokenAccountsByOwner(
368
+ publicKey,
369
+ {
370
+ programId: TOKEN_PROGRAM_ID,
371
+ }
372
+ );
373
+
374
+ const tokenBalances = [];
375
+ const clonedTokens = await this.getClonedTokens();
376
+
377
+ for (const tokenAccount of tokenAccounts.value) {
378
+ try {
379
+ const accountInfo = await this.connection.getAccountInfo(
380
+ tokenAccount.pubkey
381
+ );
382
+ if (accountInfo) {
383
+ // Parse token account data (simplified)
384
+ const data = accountInfo.data;
385
+ if (data.length >= 32) {
386
+ const mintBytes = data.slice(0, 32);
387
+ const mintAddress = new PublicKey(mintBytes).toBase58();
388
+
389
+ // Find matching cloned token
390
+ const clonedToken = clonedTokens.find(
391
+ (t) => t.config.mainnetMint === mintAddress
392
+ );
393
+
394
+ if (clonedToken) {
395
+ // Get token balance
396
+ const balance = await this.connection.getTokenAccountBalance(
397
+ tokenAccount.pubkey
398
+ );
399
+ tokenBalances.push({
400
+ mint: mintAddress,
401
+ symbol: clonedToken.config.symbol,
402
+ balance: balance.value.amount,
403
+ decimals: balance.value.decimals,
404
+ uiAmount: balance.value.uiAmount,
405
+ });
406
+ }
407
+ }
408
+ }
409
+ } catch (error) {
410
+ // Skip failed token accounts
411
+ continue;
412
+ }
413
+ }
414
+
415
+ return {
416
+ walletAddress,
417
+ solBalance: {
418
+ lamports: solBalance,
419
+ sol: solBalance / 1e9,
420
+ },
421
+ tokenBalances,
422
+ timestamp: new Date().toISOString(),
423
+ };
424
+ } catch (error) {
425
+ throw new Error(
426
+ `Failed to get wallet balances: ${
427
+ error instanceof Error ? error.message : String(error)
428
+ }`
429
+ );
430
+ }
431
+ }
432
+
433
+ private async airdropSol(
434
+ walletAddress: string,
435
+ amount: number
436
+ ): Promise<any> {
437
+ const result = await runCommand(
438
+ "solana",
439
+ [
440
+ "airdrop",
441
+ amount.toString(),
442
+ walletAddress,
443
+ "--url",
444
+ this.config.validatorRpcUrl,
445
+ ],
446
+ { silent: false, debug: false }
447
+ );
448
+
449
+ if (!result.success) {
450
+ throw new Error(`Failed to airdrop SOL: ${result.stderr}`);
451
+ }
452
+
453
+ return {
454
+ success: true,
455
+ amount,
456
+ walletAddress,
457
+ signature:
458
+ result.stdout.match(/Signature: ([A-Za-z0-9]+)/)?.[1] || "unknown",
459
+ };
460
+ }
461
+
462
+ async start(): Promise<{ success: boolean; error?: string }> {
463
+ return new Promise((resolve) => {
464
+ try {
465
+ this.server = this.app.listen(this.config.port, "127.0.0.1", () => {
466
+ console.log(
467
+ chalk.green(
468
+ `🚀 API Server started on http://127.0.0.1:${this.config.port}`
469
+ )
470
+ );
471
+ console.log(
472
+ chalk.gray(
473
+ ` 📋 Endpoints available at http://127.0.0.1:${this.config.port}/api`
474
+ )
475
+ );
476
+ resolve({ success: true });
477
+ });
478
+
479
+ this.server.on("error", (error) => {
480
+ console.error(
481
+ chalk.red(`❌ API Server failed to start: ${error.message}`)
482
+ );
483
+ resolve({ success: false, error: error.message });
484
+ });
485
+ } catch (error) {
486
+ resolve({
487
+ success: false,
488
+ error: error instanceof Error ? error.message : String(error),
489
+ });
490
+ }
491
+ });
492
+ }
493
+
494
+ async stop(): Promise<{ success: boolean; error?: string }> {
495
+ return new Promise((resolve) => {
496
+ if (!this.server) {
497
+ resolve({ success: true });
498
+ return;
499
+ }
500
+
501
+ this.server.close((error) => {
502
+ if (error) {
503
+ resolve({
504
+ success: false,
505
+ error: error instanceof Error ? error.message : String(error),
506
+ });
507
+ } else {
508
+ console.log(chalk.yellow("🛑 API Server stopped"));
509
+ resolve({ success: true });
510
+ }
511
+ this.server = null;
512
+ });
513
+ });
514
+ }
515
+
516
+ isRunning(): boolean {
517
+ return this.server !== null && this.server.listening;
518
+ }
519
+ }
@@ -14,6 +14,9 @@ export interface RunningValidator {
14
14
  configPath: string;
15
15
  startTime: Date;
16
16
  status: "running" | "stopped" | "error";
17
+ apiServerPort?: number;
18
+ apiServerUrl?: string;
19
+ apiServerPid?: number;
17
20
  }
18
21
 
19
22
  export class ProcessRegistry {
@@ -21,11 +24,7 @@ export class ProcessRegistry {
21
24
 
22
25
  constructor() {
23
26
  // Store registry in user's home directory
24
- this.registryPath = join(
25
- homedir(),
26
- ".solforge",
27
- "running-validators.json"
28
- );
27
+ this.registryPath = join(homedir(), ".solforge", "running-validators.json");
29
28
  }
30
29
 
31
30
  /**