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