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