solforge 0.2.0 → 0.2.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/docs/API.md +379 -0
  3. package/docs/CONFIGURATION.md +407 -0
  4. package/package.json +67 -45
  5. package/src/api-server-entry.ts +109 -0
  6. package/src/commands/add-program.ts +337 -0
  7. package/src/commands/init.ts +122 -0
  8. package/src/commands/list.ts +136 -0
  9. package/src/commands/mint.ts +288 -0
  10. package/src/commands/start.ts +877 -0
  11. package/src/commands/status.ts +99 -0
  12. package/src/commands/stop.ts +406 -0
  13. package/src/config/manager.ts +157 -0
  14. package/src/gui/public/build/main.css +1 -0
  15. package/src/gui/public/build/main.js +303 -0
  16. package/src/gui/public/build/main.js.txt +231 -0
  17. package/src/index.ts +188 -0
  18. package/src/services/api-server.ts +485 -0
  19. package/src/services/port-manager.ts +177 -0
  20. package/src/services/process-registry.ts +154 -0
  21. package/src/services/program-cloner.ts +317 -0
  22. package/src/services/token-cloner.ts +809 -0
  23. package/src/services/validator.ts +295 -0
  24. package/src/types/config.ts +110 -0
  25. package/src/utils/shell.ts +110 -0
  26. package/src/utils/token-loader.ts +115 -0
  27. package/.agi/agi.sqlite +0 -0
  28. package/.claude/settings.local.json +0 -9
  29. package/.github/workflows/release-binaries.yml +0 -133
  30. package/.tmp/.787ebcdbf7b8fde8-00000000.hm +0 -0
  31. package/.tmp/.bffe6efebdf8aedc-00000000.hm +0 -0
  32. package/AGENTS.md +0 -271
  33. package/CLAUDE.md +0 -106
  34. package/PROJECT_STRUCTURE.md +0 -124
  35. package/SOLANA_KIT_GUIDE.md +0 -251
  36. package/SOLFORGE.md +0 -119
  37. package/biome.json +0 -34
  38. package/bun.lock +0 -743
  39. package/drizzle/0000_friendly_millenium_guard.sql +0 -53
  40. package/drizzle/0001_stale_sentinels.sql +0 -2
  41. package/drizzle/meta/0000_snapshot.json +0 -329
  42. package/drizzle/meta/0001_snapshot.json +0 -345
  43. package/drizzle/meta/_journal.json +0 -20
  44. package/drizzle.config.ts +0 -12
  45. package/index.ts +0 -21
  46. package/mint.sh +0 -47
  47. package/postcss.config.js +0 -6
  48. package/rpc-server.ts.backup +0 -519
  49. package/sf.config.json +0 -38
  50. package/tailwind.config.js +0 -27
  51. package/test-client.ts +0 -120
  52. package/tmp/inspect-html.ts +0 -4
  53. package/tmp/response-test.ts +0 -5
  54. package/tmp/test-html.ts +0 -5
  55. package/tmp/test-server.ts +0 -13
  56. package/tsconfig.json +0 -29
@@ -0,0 +1,809 @@
1
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
2
+ import { resolve, join } from "path";
3
+ import { Keypair, PublicKey } from "@solana/web3.js";
4
+ import chalk from "chalk";
5
+ import { runCommand } from "../utils/shell.js";
6
+ import type { TokenConfig } from "../types/config.js";
7
+
8
+ // Metaplex Token Metadata Program ID
9
+ const METADATA_PROGRAM_ID = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
10
+
11
+ export interface ClonedToken {
12
+ config: TokenConfig;
13
+ mintAuthorityPath: string;
14
+ modifiedAccountPath: string;
15
+ metadataAccountPath?: string; // Path to cloned metadata account
16
+ mintAuthority: {
17
+ publicKey: string;
18
+ secretKey: number[];
19
+ };
20
+ }
21
+
22
+ export class TokenCloner {
23
+ private workDir: string;
24
+
25
+ constructor(workDir: string = ".solforge") {
26
+ this.workDir = workDir;
27
+ // Ensure work directory exists
28
+ if (!existsSync(this.workDir)) {
29
+ mkdirSync(this.workDir, { recursive: true });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Clone tokens by fetching account data, modifying mint authority, and preparing for validator
35
+ */
36
+ async cloneTokens(
37
+ tokens: TokenConfig[],
38
+ rpcUrl: string = "https://api.mainnet-beta.solana.com",
39
+ debug: boolean = false
40
+ ): Promise<ClonedToken[]> {
41
+ const clonedTokens: ClonedToken[] = [];
42
+
43
+ // Generate one shared mint authority for all tokens
44
+ const sharedMintAuthorityPath = join(
45
+ this.workDir,
46
+ "shared-mint-authority.json"
47
+ );
48
+ let sharedMintAuthority: { publicKey: string; secretKey: number[] };
49
+
50
+ if (existsSync(sharedMintAuthorityPath)) {
51
+ // Use existing shared mint authority
52
+ console.log(chalk.gray(`🔑 Using existing shared mint authority...`));
53
+ const fileContent = JSON.parse(
54
+ readFileSync(sharedMintAuthorityPath, "utf8")
55
+ );
56
+
57
+ // Handle both old format {publicKey, secretKey} and new format [secretKey array]
58
+ if (Array.isArray(fileContent)) {
59
+ // New format: file contains just the secret key array
60
+ const keypair = Keypair.fromSecretKey(new Uint8Array(fileContent));
61
+ sharedMintAuthority = {
62
+ publicKey: keypair.publicKey.toBase58(),
63
+ secretKey: Array.from(keypair.secretKey),
64
+ };
65
+
66
+ // Verify consistency with metadata if it exists
67
+ const metadataPath = join(
68
+ this.workDir,
69
+ "shared-mint-authority-meta.json"
70
+ );
71
+ if (existsSync(metadataPath)) {
72
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
73
+ if (metadata.publicKey !== sharedMintAuthority.publicKey) {
74
+ console.log(chalk.yellow(`⚠️ Public key mismatch detected!`));
75
+ console.log(chalk.gray(` Expected: ${metadata.publicKey}`));
76
+ console.log(chalk.gray(` Got: ${sharedMintAuthority.publicKey}`));
77
+ console.log(
78
+ chalk.gray(
79
+ ` Using the original saved public key for consistency`
80
+ )
81
+ );
82
+ sharedMintAuthority.publicKey = metadata.publicKey;
83
+ }
84
+ }
85
+ } else {
86
+ // Old format: file contains {publicKey, secretKey}
87
+ sharedMintAuthority = fileContent;
88
+ }
89
+ } else {
90
+ // Generate new shared mint authority
91
+ console.log(
92
+ chalk.gray(`🔑 Generating shared mint authority for all tokens...`)
93
+ );
94
+ const keypair = Keypair.generate();
95
+ sharedMintAuthority = {
96
+ publicKey: keypair.publicKey.toBase58(),
97
+ secretKey: Array.from(keypair.secretKey),
98
+ };
99
+
100
+ // Ensure work directory exists
101
+ if (!existsSync(this.workDir)) {
102
+ mkdirSync(this.workDir, { recursive: true });
103
+ }
104
+
105
+ // Save shared mint authority in the format expected by spl-token (just the secret key array)
106
+ // But also save a metadata file for verification
107
+ writeFileSync(
108
+ sharedMintAuthorityPath,
109
+ JSON.stringify(sharedMintAuthority.secretKey)
110
+ );
111
+
112
+ // Save metadata for debugging and verification
113
+ const metadataPath = join(
114
+ this.workDir,
115
+ "shared-mint-authority-meta.json"
116
+ );
117
+ writeFileSync(
118
+ metadataPath,
119
+ JSON.stringify(
120
+ {
121
+ publicKey: sharedMintAuthority.publicKey,
122
+ savedAt: new Date().toISOString(),
123
+ },
124
+ null,
125
+ 2
126
+ )
127
+ );
128
+ }
129
+
130
+ console.log(
131
+ chalk.gray(` 📋 Shared mint authority: ${sharedMintAuthority.publicKey}`)
132
+ );
133
+
134
+ for (const token of tokens) {
135
+ console.log(
136
+ chalk.cyan(`🪙 Cloning ${token.symbol} (${token.mainnetMint})...`)
137
+ );
138
+
139
+ try {
140
+ const cloned = await this.cloneToken(
141
+ token,
142
+ rpcUrl,
143
+ debug,
144
+ sharedMintAuthority,
145
+ sharedMintAuthorityPath
146
+ );
147
+ clonedTokens.push(cloned);
148
+ console.log(chalk.green(` ✅ ${token.symbol} cloned successfully`));
149
+ } catch (error) {
150
+ console.error(chalk.red(` ❌ Failed to clone ${token.symbol}:`));
151
+ console.error(
152
+ chalk.red(
153
+ ` ${error instanceof Error ? error.message : String(error)}`
154
+ )
155
+ );
156
+ throw error; // Stop on first failure
157
+ }
158
+ }
159
+
160
+ return clonedTokens;
161
+ }
162
+
163
+ /**
164
+ * Clone a single token
165
+ */
166
+ private async cloneToken(
167
+ token: TokenConfig,
168
+ rpcUrl: string,
169
+ debug: boolean = false,
170
+ sharedMintAuthority: { publicKey: string; secretKey: number[] },
171
+ sharedMintAuthorityPath: string
172
+ ): Promise<ClonedToken> {
173
+ const tokenDir = join(this.workDir, `token-${token.symbol.toLowerCase()}`);
174
+ if (!existsSync(tokenDir)) {
175
+ mkdirSync(tokenDir, { recursive: true });
176
+ }
177
+
178
+ const originalAccountPath = join(tokenDir, "original.json");
179
+ const modifiedAccountPath = join(tokenDir, "modified.json");
180
+ const metadataAccountPath = join(tokenDir, "metadata.json");
181
+
182
+ // Step 1: Fetch original account data from mainnet
183
+ console.log(chalk.gray(` 📥 Fetching account data from mainnet...`));
184
+ if (debug) {
185
+ console.log(chalk.gray(` 🔗 Using RPC: ${rpcUrl}`));
186
+ }
187
+
188
+ const fetchResult = await runCommand(
189
+ "solana",
190
+ [
191
+ "account",
192
+ token.mainnetMint,
193
+ "--output",
194
+ "json-compact",
195
+ "--output-file",
196
+ originalAccountPath,
197
+ "--url",
198
+ rpcUrl,
199
+ ],
200
+ { silent: !debug, debug }
201
+ );
202
+
203
+ if (!fetchResult.success) {
204
+ console.error(
205
+ chalk.red(` ❌ Failed to fetch account data for ${token.symbol}`)
206
+ );
207
+ throw new Error(
208
+ `Failed to fetch account: ${fetchResult.stderr || fetchResult.stdout}`
209
+ );
210
+ }
211
+
212
+ // Step 2: Clone metadata if requested
213
+ let hasMetadata = false;
214
+ if (token.cloneMetadata !== false) {
215
+ // Default to true
216
+ try {
217
+ hasMetadata = await this.cloneTokenMetadata(
218
+ token,
219
+ rpcUrl,
220
+ metadataAccountPath,
221
+ debug
222
+ );
223
+ if (hasMetadata) {
224
+ console.log(chalk.gray(` 📋 Token metadata cloned successfully`));
225
+ }
226
+ } catch (error) {
227
+ console.log(
228
+ chalk.yellow(
229
+ ` ⚠️ Failed to clone metadata: ${
230
+ error instanceof Error ? error.message : String(error)
231
+ }`
232
+ )
233
+ );
234
+ // Don't fail the entire token cloning if metadata fails
235
+ }
236
+ }
237
+
238
+ // Step 3: Modify mint authority
239
+ console.log(chalk.gray(` 🔧 Modifying mint authority...`));
240
+ await this.modifyMintAuthority(
241
+ originalAccountPath,
242
+ modifiedAccountPath,
243
+ sharedMintAuthority
244
+ );
245
+
246
+ const result: ClonedToken = {
247
+ config: token,
248
+ mintAuthorityPath: sharedMintAuthorityPath,
249
+ modifiedAccountPath,
250
+ mintAuthority: sharedMintAuthority,
251
+ };
252
+
253
+ // Add metadata path if we successfully cloned metadata
254
+ if (hasMetadata) {
255
+ result.metadataAccountPath = metadataAccountPath;
256
+ }
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * Clone token metadata from Metaplex Token Metadata program
263
+ */
264
+ private async cloneTokenMetadata(
265
+ token: TokenConfig,
266
+ rpcUrl: string,
267
+ metadataAccountPath: string,
268
+ debug: boolean = false
269
+ ): Promise<boolean> {
270
+ try {
271
+ // Derive metadata account address
272
+ const metadataAddress = await this.deriveMetadataAddress(
273
+ token.mainnetMint
274
+ );
275
+
276
+ if (debug) {
277
+ console.log(chalk.gray(` 🔍 Metadata PDA: ${metadataAddress}`));
278
+ }
279
+
280
+ // Fetch metadata account data
281
+ const fetchResult = await runCommand(
282
+ "solana",
283
+ [
284
+ "account",
285
+ metadataAddress,
286
+ "--output",
287
+ "json-compact",
288
+ "--output-file",
289
+ metadataAccountPath,
290
+ "--url",
291
+ rpcUrl,
292
+ ],
293
+ { silent: !debug, debug }
294
+ );
295
+
296
+ if (!fetchResult.success) {
297
+ if (debug) {
298
+ console.log(
299
+ chalk.gray(` ℹ️ No metadata account found for ${token.symbol}`)
300
+ );
301
+ }
302
+ return false;
303
+ }
304
+
305
+ return true;
306
+ } catch (error) {
307
+ throw new Error(
308
+ `Failed to clone metadata: ${
309
+ error instanceof Error ? error.message : String(error)
310
+ }`
311
+ );
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Derive the metadata account address for a given mint
317
+ * PDA: ["metadata", metadata_program_id, mint_address]
318
+ */
319
+ private async deriveMetadataAddress(mintAddress: string): Promise<string> {
320
+ try {
321
+ // Use web3.js to derive the actual PDA
322
+ const metadataProgram = new PublicKey(METADATA_PROGRAM_ID);
323
+ const mint = new PublicKey(mintAddress);
324
+
325
+ const [metadataAddress] = PublicKey.findProgramAddressSync(
326
+ [Buffer.from("metadata"), metadataProgram.toBuffer(), mint.toBuffer()],
327
+ metadataProgram
328
+ );
329
+
330
+ return metadataAddress.toBase58();
331
+ } catch (error) {
332
+ throw new Error(
333
+ `Failed to derive metadata PDA: ${
334
+ error instanceof Error ? error.message : String(error)
335
+ }`
336
+ );
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Modify the mint account data to use a new mint authority
342
+ */
343
+ private async modifyMintAuthority(
344
+ originalPath: string,
345
+ modifiedPath: string,
346
+ mintAuthority: { publicKey: string; secretKey: number[] }
347
+ ): Promise<void> {
348
+ // Read original account data
349
+ const originalData = JSON.parse(readFileSync(originalPath, "utf8"));
350
+
351
+ // Decode the base64 account data
352
+ const accountData = Buffer.from(originalData.account.data[0], "base64");
353
+
354
+ // Convert to mutable array for modification
355
+ const dataArray = new Uint8Array(accountData);
356
+
357
+ // Check if mint authority is set to None (fixed supply)
358
+ const mintAuthorityOption = dataArray[0];
359
+ const hasFixedSupply = mintAuthorityOption === 0;
360
+
361
+ if (hasFixedSupply) {
362
+ console.log(
363
+ chalk.yellow(` ⚠️ Token has fixed supply, making it mintable...`)
364
+ );
365
+
366
+ // Set mint_authority_option to Some (1)
367
+ dataArray[0] = 1;
368
+
369
+ // Set the mint authority (bytes 4-35)
370
+ const keypair = Keypair.fromSecretKey(
371
+ new Uint8Array(mintAuthority.secretKey)
372
+ );
373
+ const newAuthorityBytes = keypair.publicKey.toBuffer();
374
+
375
+ for (let i = 0; i < 32; i++) {
376
+ const byte = newAuthorityBytes[i];
377
+ if (byte !== undefined) {
378
+ dataArray[4 + i] = byte;
379
+ }
380
+ }
381
+ } else {
382
+ console.log(chalk.gray(` 🔄 Updating existing mint authority...`));
383
+
384
+ // Create mint authority public key from the keypair
385
+ const keypair = Keypair.fromSecretKey(
386
+ new Uint8Array(mintAuthority.secretKey)
387
+ );
388
+ const newAuthorityBytes = keypair.publicKey.toBuffer();
389
+
390
+ // Replace mint authority (starts at byte 4, 32 bytes long)
391
+ for (let i = 0; i < 32; i++) {
392
+ const byte = newAuthorityBytes[i];
393
+ if (byte !== undefined) {
394
+ dataArray[4 + i] = byte;
395
+ }
396
+ }
397
+ }
398
+
399
+ // Convert back to base64
400
+ const modifiedData = Buffer.from(dataArray).toString("base64");
401
+
402
+ // Create modified account data
403
+ const modifiedAccountData = {
404
+ ...originalData,
405
+ account: {
406
+ ...originalData.account,
407
+ data: [modifiedData, "base64"],
408
+ },
409
+ };
410
+
411
+ // Handle rentEpoch properly (needs to be a number, not string)
412
+ let jsonString = JSON.stringify(modifiedAccountData, null, 2);
413
+ jsonString = jsonString.replace(
414
+ /"rentEpoch":\s*\d+/g,
415
+ '"rentEpoch": 18446744073709551615'
416
+ );
417
+
418
+ // Save modified account
419
+ writeFileSync(modifiedPath, jsonString);
420
+ }
421
+
422
+ /**
423
+ * Get validator arguments for cloned tokens
424
+ */
425
+ getValidatorArgs(clonedTokens: ClonedToken[]): string[] {
426
+ const args: string[] = [];
427
+
428
+ for (const token of clonedTokens) {
429
+ // Add the modified mint account
430
+ args.push(
431
+ "--account",
432
+ token.config.mainnetMint,
433
+ token.modifiedAccountPath
434
+ );
435
+
436
+ // Add metadata account if it exists
437
+ if (token.metadataAccountPath && existsSync(token.metadataAccountPath)) {
438
+ try {
439
+ // Derive metadata address for this token
440
+ const metadataAddress = this.deriveMetadataAddressSync(
441
+ token.config.mainnetMint
442
+ );
443
+ args.push("--account", metadataAddress, token.metadataAccountPath);
444
+ console.log(
445
+ chalk.gray(
446
+ ` 📋 Adding metadata account for ${token.config.symbol}`
447
+ )
448
+ );
449
+ } catch (error) {
450
+ console.log(
451
+ chalk.yellow(
452
+ `⚠️ Failed to add metadata account for ${token.config.symbol}: ${
453
+ error instanceof Error ? error.message : String(error)
454
+ }`
455
+ )
456
+ );
457
+ }
458
+ }
459
+ }
460
+
461
+ return args;
462
+ }
463
+
464
+ /**
465
+ * Synchronous version of deriveMetadataAddress for use in getValidatorArgs
466
+ */
467
+ private deriveMetadataAddressSync(mintAddress: string): string {
468
+ try {
469
+ // Use web3.js to derive the actual PDA
470
+ const metadataProgram = new PublicKey(METADATA_PROGRAM_ID);
471
+ const mint = new PublicKey(mintAddress);
472
+
473
+ const [metadataAddress] = PublicKey.findProgramAddressSync(
474
+ [Buffer.from("metadata"), metadataProgram.toBuffer(), mint.toBuffer()],
475
+ metadataProgram
476
+ );
477
+
478
+ return metadataAddress.toBase58();
479
+ } catch (error) {
480
+ throw new Error(
481
+ `Failed to derive metadata PDA: ${
482
+ error instanceof Error ? error.message : String(error)
483
+ }`
484
+ );
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Mint initial tokens to mint authority, then to recipients after validator is running
490
+ */
491
+ async mintTokensToRecipients(
492
+ clonedTokens: ClonedToken[],
493
+ rpcUrl: string,
494
+ debug: boolean = false
495
+ ): Promise<void> {
496
+ for (const token of clonedTokens) {
497
+ console.log(chalk.cyan(`💰 Processing ${token.config.symbol}...`));
498
+
499
+ // First, mint the default amount to the mint authority
500
+ try {
501
+ await this.mintToMintAuthority(token, rpcUrl, debug);
502
+ console.log(
503
+ chalk.green(
504
+ ` ✅ Minted ${token.config.mintAmount} ${token.config.symbol} to mint authority`
505
+ )
506
+ );
507
+ } catch (error) {
508
+ console.error(chalk.red(` ❌ Failed to mint to mint authority:`));
509
+ console.error(
510
+ chalk.red(
511
+ ` ${error instanceof Error ? error.message : String(error)}`
512
+ )
513
+ );
514
+ continue; // Skip recipients if mint authority minting failed
515
+ }
516
+
517
+ // Then mint to recipients if any are configured
518
+ if (token.config.recipients.length > 0) {
519
+ console.log(
520
+ chalk.gray(
521
+ ` 🔄 Minting to ${token.config.recipients.length} recipients...`
522
+ )
523
+ );
524
+
525
+ for (const recipient of token.config.recipients) {
526
+ try {
527
+ await this.mintToRecipient(
528
+ token,
529
+ recipient.wallet,
530
+ recipient.amount,
531
+ rpcUrl
532
+ );
533
+ console.log(
534
+ chalk.green(
535
+ ` ✅ Minted ${recipient.amount} ${
536
+ token.config.symbol
537
+ } to ${recipient.wallet.slice(0, 8)}...`
538
+ )
539
+ );
540
+ } catch (error) {
541
+ console.error(
542
+ chalk.red(
543
+ ` ❌ Failed to mint to ${recipient.wallet.slice(0, 8)}...:`
544
+ )
545
+ );
546
+ console.error(
547
+ chalk.red(
548
+ ` ${error instanceof Error ? error.message : String(error)}`
549
+ )
550
+ );
551
+ }
552
+ }
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Mint initial tokens to the mint authority account
559
+ */
560
+ private async mintToMintAuthority(
561
+ token: ClonedToken,
562
+ rpcUrl: string,
563
+ debug: boolean = false
564
+ ): Promise<void> {
565
+ console.log(
566
+ chalk.gray(
567
+ ` 🔄 Minting ${token.config.mintAmount} tokens to mint authority...`
568
+ )
569
+ );
570
+
571
+ // Check if associated token account already exists
572
+ if (debug) {
573
+ console.log(
574
+ chalk.gray(` 🔍 Checking if token account already exists...`)
575
+ );
576
+ }
577
+
578
+ const checkAccountsResult = await runCommand(
579
+ "spl-token",
580
+ [
581
+ "accounts",
582
+ "--owner",
583
+ token.mintAuthority.publicKey,
584
+ "--url",
585
+ rpcUrl,
586
+ "--output",
587
+ "json",
588
+ ],
589
+ { silent: true }
590
+ );
591
+
592
+ let tokenAccountAddress = "";
593
+
594
+ if (checkAccountsResult.success && checkAccountsResult.stdout) {
595
+ try {
596
+ const accountsData = JSON.parse(checkAccountsResult.stdout);
597
+
598
+ // Look for existing token account for this mint
599
+ for (const account of accountsData.accounts || []) {
600
+ if (account.mint === token.config.mainnetMint) {
601
+ tokenAccountAddress = account.address;
602
+ break;
603
+ }
604
+ }
605
+
606
+ if (tokenAccountAddress) {
607
+ if (debug) {
608
+ console.log(
609
+ chalk.gray(
610
+ ` ℹ️ Token account already exists: ${tokenAccountAddress}`
611
+ )
612
+ );
613
+ }
614
+ }
615
+ } catch (error) {
616
+ if (debug) {
617
+ console.log(
618
+ chalk.gray(` ℹ️ No existing accounts found or parsing error`)
619
+ );
620
+ }
621
+ }
622
+ }
623
+
624
+ if (!tokenAccountAddress) {
625
+ // Account doesn't exist, create it
626
+ if (debug) {
627
+ console.log(
628
+ chalk.gray(` 🔧 Creating token account for mint authority...`)
629
+ );
630
+ }
631
+
632
+ const createAccountResult = await runCommand(
633
+ "spl-token",
634
+ [
635
+ "create-account",
636
+ token.config.mainnetMint,
637
+ "--owner",
638
+ token.mintAuthority.publicKey,
639
+ "--fee-payer",
640
+ token.mintAuthorityPath,
641
+ "--url",
642
+ rpcUrl,
643
+ ],
644
+ { silent: !debug, debug }
645
+ );
646
+
647
+ if (!createAccountResult.success) {
648
+ console.error(chalk.red(` ❌ Failed to create token account:`));
649
+ console.error(
650
+ chalk.red(
651
+ ` Command: spl-token create-account ${token.config.mainnetMint} --owner ${token.mintAuthority.publicKey} --fee-payer ${token.mintAuthorityPath} --url ${rpcUrl}`
652
+ )
653
+ );
654
+ console.error(
655
+ chalk.red(` Exit code: ${createAccountResult.exitCode}`)
656
+ );
657
+ console.error(chalk.red(` Stderr: ${createAccountResult.stderr}`));
658
+ if (createAccountResult.stdout) {
659
+ console.error(
660
+ chalk.red(` Stdout: ${createAccountResult.stdout}`)
661
+ );
662
+ }
663
+ throw new Error(
664
+ `Failed to create token account: ${createAccountResult.stderr}`
665
+ );
666
+ } else {
667
+ if (debug) {
668
+ console.log(chalk.gray(` ✅ Token account created successfully`));
669
+ }
670
+ // Extract token account address from create-account output
671
+ const match = createAccountResult.stdout.match(
672
+ /Creating account (\S+)/
673
+ );
674
+ tokenAccountAddress = match?.[1] || "";
675
+ }
676
+ }
677
+
678
+ if (!tokenAccountAddress) {
679
+ throw new Error("Failed to determine token account address");
680
+ }
681
+
682
+ if (debug) {
683
+ console.log(chalk.gray(` 📍 Token account: ${tokenAccountAddress}`));
684
+ }
685
+
686
+ // Mint tokens to the specific token account
687
+ const mintResult = await runCommand(
688
+ "spl-token",
689
+ [
690
+ "mint",
691
+ token.config.mainnetMint,
692
+ token.config.mintAmount.toString(),
693
+ tokenAccountAddress,
694
+ "--mint-authority",
695
+ token.mintAuthorityPath,
696
+ "--fee-payer",
697
+ token.mintAuthorityPath,
698
+ "--url",
699
+ rpcUrl,
700
+ ],
701
+ { silent: !debug, debug }
702
+ );
703
+
704
+ if (!mintResult.success) {
705
+ throw new Error(`Failed to mint tokens: ${mintResult.stderr}`);
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Mint tokens to a specific recipient
711
+ */
712
+ private async mintToRecipient(
713
+ token: ClonedToken,
714
+ recipientAddress: string,
715
+ amount: number,
716
+ rpcUrl: string
717
+ ): Promise<void> {
718
+ console.log(
719
+ chalk.gray(` 🔄 Minting ${amount} tokens to ${recipientAddress}...`)
720
+ );
721
+
722
+ // Check if associated token account already exists
723
+ const checkAccountsResult = await runCommand(
724
+ "spl-token",
725
+ [
726
+ "accounts",
727
+ "--owner",
728
+ recipientAddress,
729
+ "--url",
730
+ rpcUrl,
731
+ "--output",
732
+ "json",
733
+ ],
734
+ { silent: true }
735
+ );
736
+
737
+ let tokenAccountAddress = "";
738
+
739
+ if (checkAccountsResult.success && checkAccountsResult.stdout) {
740
+ try {
741
+ const accountsData = JSON.parse(checkAccountsResult.stdout);
742
+
743
+ // Look for existing token account for this mint
744
+ for (const account of accountsData.accounts || []) {
745
+ if (account.mint === token.config.mainnetMint) {
746
+ tokenAccountAddress = account.address;
747
+ break;
748
+ }
749
+ }
750
+ } catch (error) {
751
+ // No existing accounts found or parsing error, will create new account
752
+ }
753
+ }
754
+
755
+ if (!tokenAccountAddress) {
756
+ // Account doesn't exist, create it
757
+ const createAccountResult = await runCommand(
758
+ "spl-token",
759
+ [
760
+ "create-account",
761
+ token.config.mainnetMint,
762
+ "--owner",
763
+ recipientAddress,
764
+ "--fee-payer",
765
+ token.mintAuthorityPath,
766
+ "--url",
767
+ rpcUrl,
768
+ ],
769
+ { silent: true }
770
+ );
771
+
772
+ if (!createAccountResult.success) {
773
+ throw new Error(
774
+ `Failed to create token account: ${createAccountResult.stderr}`
775
+ );
776
+ }
777
+
778
+ // Extract token account address from create-account output
779
+ const match = createAccountResult.stdout.match(/Creating account (\S+)/);
780
+ tokenAccountAddress = match?.[1] || "";
781
+ }
782
+
783
+ if (!tokenAccountAddress) {
784
+ throw new Error("Failed to determine token account address");
785
+ }
786
+
787
+ // Mint tokens to the specific token account
788
+ const mintResult = await runCommand(
789
+ "spl-token",
790
+ [
791
+ "mint",
792
+ token.config.mainnetMint,
793
+ amount.toString(),
794
+ tokenAccountAddress,
795
+ "--mint-authority",
796
+ token.mintAuthorityPath,
797
+ "--fee-payer",
798
+ token.mintAuthorityPath,
799
+ "--url",
800
+ rpcUrl,
801
+ ],
802
+ { silent: true }
803
+ );
804
+
805
+ if (!mintResult.success) {
806
+ throw new Error(`Failed to mint tokens: ${mintResult.stderr}`);
807
+ }
808
+ }
809
+ }