solforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/package.json +71 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/commands/add-program.ts +337 -0
- package/src/commands/init.ts +122 -0
- package/src/commands/list.ts +136 -0
- package/src/commands/start.ts +735 -0
- package/src/commands/status.ts +99 -0
- package/src/commands/stop.ts +389 -0
- package/src/commands/transfer.ts +259 -0
- package/src/config/manager.ts +157 -0
- package/src/index.ts +107 -0
- package/src/services/port-manager.ts +177 -0
- package/src/services/process-registry.ts +155 -0
- package/src/services/program-cloner.ts +317 -0
- package/src/services/token-cloner.ts +809 -0
- package/src/services/validator.ts +295 -0
- package/src/types/config.ts +110 -0
- package/src/utils/shell.ts +110 -0
- package/tsconfig.json +28 -0
|
@@ -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
|
+
}
|