solforge 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +323 -364
  3. package/cli.cjs +126 -69
  4. package/package.json +1 -1
  5. package/scripts/install.sh +112 -0
  6. package/scripts/postinstall.cjs +66 -58
  7. package/server/methods/program/get-token-accounts-by-owner.ts +7 -2
  8. package/server/ws-server.ts +4 -1
  9. package/src/api-server-entry.ts +91 -91
  10. package/src/cli/commands/rpc-start.ts +4 -1
  11. package/src/cli/main.ts +39 -14
  12. package/src/cli/run-solforge.ts +20 -6
  13. package/src/commands/add-program.ts +324 -328
  14. package/src/commands/init.ts +106 -106
  15. package/src/commands/list.ts +125 -125
  16. package/src/commands/mint.ts +246 -246
  17. package/src/commands/start.ts +834 -831
  18. package/src/commands/status.ts +80 -80
  19. package/src/commands/stop.ts +381 -382
  20. package/src/config/manager.ts +149 -149
  21. package/src/gui/public/app.css +1556 -1
  22. package/src/gui/public/build/main.css +1569 -1
  23. package/src/gui/server.ts +20 -21
  24. package/src/gui/src/app.tsx +56 -37
  25. package/src/gui/src/components/airdrop-mint-form.tsx +17 -11
  26. package/src/gui/src/components/clone-program-modal.tsx +6 -6
  27. package/src/gui/src/components/clone-token-modal.tsx +7 -7
  28. package/src/gui/src/components/modal.tsx +13 -11
  29. package/src/gui/src/components/programs-panel.tsx +27 -15
  30. package/src/gui/src/components/status-panel.tsx +31 -17
  31. package/src/gui/src/components/tokens-panel.tsx +25 -19
  32. package/src/gui/src/index.css +491 -463
  33. package/src/index.ts +161 -146
  34. package/src/rpc/start.ts +1 -1
  35. package/src/services/api-server.ts +470 -473
  36. package/src/services/port-manager.ts +167 -167
  37. package/src/services/process-registry.ts +143 -143
  38. package/src/services/program-cloner.ts +312 -312
  39. package/src/services/token-cloner.ts +799 -797
  40. package/src/services/validator.ts +288 -288
  41. package/src/types/config.ts +71 -71
  42. package/src/utils/shell.ts +75 -75
  43. package/src/utils/token-loader.ts +77 -77
@@ -2,61 +2,61 @@ import { z } from "zod";
2
2
 
3
3
  // Token configuration schema
4
4
  export const TokenConfigSchema = z.object({
5
- symbol: z.string().min(1, "Token symbol is required"),
6
- mainnetMint: z.string().min(1, "Mainnet mint address is required"),
7
- mintAuthority: z.string().optional(), // Path to keypair file
8
- mintAmount: z
9
- .number()
10
- .positive("Mint amount must be positive")
11
- .default(1000000), // Amount to mint to mint authority
12
- recipients: z
13
- .array(
14
- z.object({
15
- wallet: z.string().min(1, "Wallet address is required"),
16
- amount: z.number().positive("Amount must be positive"),
17
- })
18
- )
19
- .default([]),
20
- cloneMetadata: z.boolean().default(true), // Whether to clone token metadata
5
+ symbol: z.string().min(1, "Token symbol is required"),
6
+ mainnetMint: z.string().min(1, "Mainnet mint address is required"),
7
+ mintAuthority: z.string().optional(), // Path to keypair file
8
+ mintAmount: z
9
+ .number()
10
+ .positive("Mint amount must be positive")
11
+ .default(1000000), // Amount to mint to mint authority
12
+ recipients: z
13
+ .array(
14
+ z.object({
15
+ wallet: z.string().min(1, "Wallet address is required"),
16
+ amount: z.number().positive("Amount must be positive"),
17
+ }),
18
+ )
19
+ .default([]),
20
+ cloneMetadata: z.boolean().default(true), // Whether to clone token metadata
21
21
  });
22
22
 
23
23
  // Program configuration schema
24
24
  export const ProgramConfigSchema = z.object({
25
- name: z.string().optional(),
26
- mainnetProgramId: z.string().min(1, "Program ID is required"),
27
- deployPath: z.string().optional(), // Optional path to local .so file
28
- upgradeable: z.boolean().default(false),
29
- cluster: z
30
- .enum(["mainnet-beta", "devnet", "testnet"])
31
- .default("mainnet-beta"),
32
- dependencies: z.array(z.string()).default([]), // Other program IDs this program depends on
25
+ name: z.string().optional(),
26
+ mainnetProgramId: z.string().min(1, "Program ID is required"),
27
+ deployPath: z.string().optional(), // Optional path to local .so file
28
+ upgradeable: z.boolean().default(false),
29
+ cluster: z
30
+ .enum(["mainnet-beta", "devnet", "testnet"])
31
+ .default("mainnet-beta"),
32
+ dependencies: z.array(z.string()).default([]), // Other program IDs this program depends on
33
33
  });
34
34
 
35
35
  // Localnet configuration schema
36
36
  export const LocalnetConfigSchema = z.object({
37
- airdropAmount: z.number().positive().default(100),
38
- faucetAccounts: z.array(z.string()).default([]),
39
- logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
40
- port: z.number().int().min(1000).max(65535).default(8899),
41
- faucetPort: z.number().int().min(1000).max(65535).default(9900),
42
- reset: z.boolean().default(false),
43
- quiet: z.boolean().default(false),
44
- ledgerPath: z.string().optional(),
45
- bindAddress: z.string().default("127.0.0.1"),
46
- limitLedgerSize: z.number().int().positive().default(100000),
47
- rpc: z
48
- .string()
49
- .url("RPC must be a valid URL")
50
- .default("https://api.mainnet-beta.solana.com"),
37
+ airdropAmount: z.number().positive().default(100),
38
+ faucetAccounts: z.array(z.string()).default([]),
39
+ logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
40
+ port: z.number().int().min(1000).max(65535).default(8899),
41
+ faucetPort: z.number().int().min(1000).max(65535).default(9900),
42
+ reset: z.boolean().default(false),
43
+ quiet: z.boolean().default(false),
44
+ ledgerPath: z.string().optional(),
45
+ bindAddress: z.string().default("127.0.0.1"),
46
+ limitLedgerSize: z.number().int().positive().default(100000),
47
+ rpc: z
48
+ .string()
49
+ .url("RPC must be a valid URL")
50
+ .default("https://api.mainnet-beta.solana.com"),
51
51
  });
52
52
 
53
53
  // Complete configuration schema
54
54
  export const ConfigSchema = z.object({
55
- name: z.string().default("solforge-localnet"),
56
- description: z.string().optional(),
57
- tokens: z.array(TokenConfigSchema).default([]),
58
- programs: z.array(ProgramConfigSchema).default([]),
59
- localnet: LocalnetConfigSchema.default({}),
55
+ name: z.string().default("solforge-localnet"),
56
+ description: z.string().optional(),
57
+ tokens: z.array(TokenConfigSchema).default([]),
58
+ programs: z.array(ProgramConfigSchema).default([]),
59
+ localnet: LocalnetConfigSchema.default({}),
60
60
  });
61
61
 
62
62
  // Inferred TypeScript types
@@ -67,44 +67,44 @@ export type Config = z.infer<typeof ConfigSchema>;
67
67
 
68
68
  // Validator status types
69
69
  export type ValidatorStatus =
70
- | "stopped"
71
- | "starting"
72
- | "running"
73
- | "stopping"
74
- | "error";
70
+ | "stopped"
71
+ | "starting"
72
+ | "running"
73
+ | "stopping"
74
+ | "error";
75
75
 
76
76
  export interface ValidatorState {
77
- status: ValidatorStatus;
78
- pid?: number;
79
- port: number;
80
- faucetPort: number;
81
- rpcUrl: string;
82
- wsUrl: string;
83
- startTime?: Date;
84
- logs: string[];
85
- error?: string;
77
+ status: ValidatorStatus;
78
+ pid?: number;
79
+ port: number;
80
+ faucetPort: number;
81
+ rpcUrl: string;
82
+ wsUrl: string;
83
+ startTime?: Date;
84
+ logs: string[];
85
+ error?: string;
86
86
  }
87
87
 
88
88
  // Operation result types
89
89
  export interface OperationResult<T = any> {
90
- success: boolean;
91
- data?: T;
92
- error?: string;
93
- details?: any;
90
+ success: boolean;
91
+ data?: T;
92
+ error?: string;
93
+ details?: any;
94
94
  }
95
95
 
96
96
  export interface CloneResult {
97
- type: "program" | "token";
98
- id: string;
99
- name?: string;
100
- success: boolean;
101
- error?: string;
97
+ type: "program" | "token";
98
+ id: string;
99
+ name?: string;
100
+ success: boolean;
101
+ error?: string;
102
102
  }
103
103
 
104
104
  export interface ValidationResult {
105
- valid: boolean;
106
- errors: Array<{
107
- path: string;
108
- message: string;
109
- }>;
105
+ valid: boolean;
106
+ errors: Array<{
107
+ path: string;
108
+ message: string;
109
+ }>;
110
110
  }
@@ -2,109 +2,109 @@ import { $ } from "bun";
2
2
  import chalk from "chalk";
3
3
 
4
4
  export interface CommandResult {
5
- success: boolean;
6
- stdout: string;
7
- stderr: string;
8
- exitCode: number;
5
+ success: boolean;
6
+ stdout: string;
7
+ stderr: string;
8
+ exitCode: number;
9
9
  }
10
10
 
11
11
  /**
12
12
  * Execute a shell command and return the result
13
13
  */
14
14
  export async function runCommand(
15
- command: string,
16
- args: string[] = [],
17
- options: {
18
- silent?: boolean;
19
- jsonOutput?: boolean;
20
- debug?: boolean;
21
- } = {}
15
+ command: string,
16
+ args: string[] = [],
17
+ options: {
18
+ silent?: boolean;
19
+ jsonOutput?: boolean;
20
+ debug?: boolean;
21
+ } = {},
22
22
  ): Promise<CommandResult> {
23
- const { silent = false, jsonOutput = false, debug = false } = options;
23
+ const { silent = false, jsonOutput = false, debug = false } = options;
24
24
 
25
- try {
26
- if (!silent || debug) {
27
- console.log(chalk.gray(`$ ${command} ${args.join(" ")}`));
28
- }
25
+ try {
26
+ if (!silent || debug) {
27
+ console.log(chalk.gray(`$ ${command} ${args.join(" ")}`));
28
+ }
29
29
 
30
- const result = await $`${command} ${args}`.quiet();
30
+ const result = await $`${command} ${args}`.quiet();
31
31
 
32
- const stdout = result.stdout.toString();
33
- const stderr = result.stderr.toString();
34
- const exitCode = result.exitCode;
35
- const success = exitCode === 0;
32
+ const stdout = result.stdout.toString();
33
+ const stderr = result.stderr.toString();
34
+ const exitCode = result.exitCode;
35
+ const success = exitCode === 0;
36
36
 
37
- if (debug) {
38
- console.log(chalk.gray(`Exit code: ${exitCode}`));
39
- if (stdout) {
40
- console.log(chalk.gray(`Stdout: ${stdout}`));
41
- }
42
- if (stderr) {
43
- console.log(chalk.gray(`Stderr: ${stderr}`));
44
- }
45
- }
37
+ if (debug) {
38
+ console.log(chalk.gray(`Exit code: ${exitCode}`));
39
+ if (stdout) {
40
+ console.log(chalk.gray(`Stdout: ${stdout}`));
41
+ }
42
+ if (stderr) {
43
+ console.log(chalk.gray(`Stderr: ${stderr}`));
44
+ }
45
+ }
46
46
 
47
- if (!silent && !success) {
48
- console.error(chalk.red(`Command failed with exit code ${exitCode}`));
49
- if (stderr) {
50
- console.error(chalk.red(`Error: ${stderr}`));
51
- }
52
- }
47
+ if (!silent && !success) {
48
+ console.error(chalk.red(`Command failed with exit code ${exitCode}`));
49
+ if (stderr) {
50
+ console.error(chalk.red(`Error: ${stderr}`));
51
+ }
52
+ }
53
53
 
54
- // If JSON output is expected, try to parse it
55
- let parsedOutput = stdout;
56
- if (jsonOutput && success && stdout.trim()) {
57
- try {
58
- parsedOutput = JSON.parse(stdout);
59
- } catch (e) {
60
- // If JSON parsing fails, keep original stdout
61
- console.warn(
62
- chalk.yellow("Warning: Expected JSON output but got invalid JSON")
63
- );
64
- }
65
- }
54
+ // If JSON output is expected, try to parse it
55
+ let parsedOutput = stdout;
56
+ if (jsonOutput && success && stdout.trim()) {
57
+ try {
58
+ parsedOutput = JSON.parse(stdout);
59
+ } catch (e) {
60
+ // If JSON parsing fails, keep original stdout
61
+ console.warn(
62
+ chalk.yellow("Warning: Expected JSON output but got invalid JSON"),
63
+ );
64
+ }
65
+ }
66
66
 
67
- return {
68
- success,
69
- stdout: parsedOutput,
70
- stderr,
71
- exitCode,
72
- };
73
- } catch (error) {
74
- const errorMessage = error instanceof Error ? error.message : String(error);
67
+ return {
68
+ success,
69
+ stdout: parsedOutput,
70
+ stderr,
71
+ exitCode,
72
+ };
73
+ } catch (error) {
74
+ const errorMessage = error instanceof Error ? error.message : String(error);
75
75
 
76
- if (!silent) {
77
- console.error(chalk.red(`Command execution failed: ${errorMessage}`));
78
- }
76
+ if (!silent) {
77
+ console.error(chalk.red(`Command execution failed: ${errorMessage}`));
78
+ }
79
79
 
80
- return {
81
- success: false,
82
- stdout: "",
83
- stderr: errorMessage,
84
- exitCode: 1,
85
- };
86
- }
80
+ return {
81
+ success: false,
82
+ stdout: "",
83
+ stderr: errorMessage,
84
+ exitCode: 1,
85
+ };
86
+ }
87
87
  }
88
88
 
89
89
  /**
90
90
  * Check if a command exists in PATH
91
91
  */
92
92
  export async function commandExists(command: string): Promise<boolean> {
93
- const result = await runCommand("which", [command], { silent: true });
94
- return result.success;
93
+ const result = await runCommand("which", [command], { silent: true });
94
+ return result.success;
95
95
  }
96
96
 
97
97
  /**
98
98
  * Check if solana CLI tools are available
99
99
  */
100
100
  export async function checkSolanaTools(): Promise<{
101
- solana: boolean;
102
- splToken: boolean;
101
+ solana: boolean;
102
+ splToken: boolean;
103
103
  }> {
104
- const [solana, splToken] = await Promise.all([
105
- commandExists("solana"),
106
- commandExists("spl-token"),
107
- ]);
104
+ const [solana, splToken] = await Promise.all([
105
+ commandExists("solana"),
106
+ commandExists("spl-token"),
107
+ ]);
108
108
 
109
- return { solana, splToken };
109
+ return { solana, splToken };
110
110
  }
@@ -1,17 +1,17 @@
1
+ import { Keypair } from "@solana/web3.js";
1
2
  import { existsSync, readFileSync } from "fs";
2
3
  import { join } from "path";
3
- import { Keypair } from "@solana/web3.js";
4
4
  import type { TokenConfig } from "../types/config.js";
5
5
 
6
6
  export interface ClonedToken {
7
- config: TokenConfig;
8
- mintAuthorityPath: string;
9
- modifiedAccountPath: string;
10
- metadataAccountPath?: string;
11
- mintAuthority: {
12
- publicKey: string;
13
- secretKey: number[];
14
- };
7
+ config: TokenConfig;
8
+ mintAuthorityPath: string;
9
+ modifiedAccountPath: string;
10
+ metadataAccountPath?: string;
11
+ mintAuthority: {
12
+ publicKey: string;
13
+ secretKey: number[];
14
+ };
15
15
  }
16
16
 
17
17
  /**
@@ -19,97 +19,97 @@ export interface ClonedToken {
19
19
  * This ensures consistent token loading across CLI and API server.
20
20
  */
21
21
  export async function loadClonedTokens(
22
- tokenConfigs: TokenConfig[],
23
- workDir: string = ".solforge"
22
+ tokenConfigs: TokenConfig[],
23
+ workDir: string = ".solforge",
24
24
  ): Promise<ClonedToken[]> {
25
- const clonedTokens: ClonedToken[] = [];
25
+ const clonedTokens: ClonedToken[] = [];
26
26
 
27
- // Load shared mint authority
28
- const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
29
- let sharedMintAuthority: { publicKey: string; secretKey: number[] } | null =
30
- null;
27
+ // Load shared mint authority
28
+ const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
29
+ let sharedMintAuthority: { publicKey: string; secretKey: number[] } | null =
30
+ null;
31
31
 
32
- if (existsSync(sharedMintAuthorityPath)) {
33
- try {
34
- const fileContent = JSON.parse(
35
- readFileSync(sharedMintAuthorityPath, "utf8")
36
- );
32
+ if (existsSync(sharedMintAuthorityPath)) {
33
+ try {
34
+ const fileContent = JSON.parse(
35
+ readFileSync(sharedMintAuthorityPath, "utf8"),
36
+ );
37
37
 
38
- if (Array.isArray(fileContent)) {
39
- // New format: file contains just the secret key array
40
- const keypair = Keypair.fromSecretKey(new Uint8Array(fileContent));
41
- sharedMintAuthority = {
42
- publicKey: keypair.publicKey.toBase58(),
43
- secretKey: Array.from(keypair.secretKey),
44
- };
38
+ if (Array.isArray(fileContent)) {
39
+ // New format: file contains just the secret key array
40
+ const keypair = Keypair.fromSecretKey(new Uint8Array(fileContent));
41
+ sharedMintAuthority = {
42
+ publicKey: keypair.publicKey.toBase58(),
43
+ secretKey: Array.from(keypair.secretKey),
44
+ };
45
45
 
46
- // Check metadata for consistency
47
- const metadataPath = join(workDir, "shared-mint-authority-meta.json");
48
- if (existsSync(metadataPath)) {
49
- const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
50
- if (metadata.publicKey !== sharedMintAuthority.publicKey) {
51
- sharedMintAuthority.publicKey = metadata.publicKey;
52
- }
53
- }
54
- } else {
55
- // Old format: file contains {publicKey, secretKey}
56
- sharedMintAuthority = fileContent;
57
- }
58
- } catch (error) {
59
- console.error("Failed to load shared mint authority:", error);
60
- return [];
61
- }
62
- }
46
+ // Check metadata for consistency
47
+ const metadataPath = join(workDir, "shared-mint-authority-meta.json");
48
+ if (existsSync(metadataPath)) {
49
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
50
+ if (metadata.publicKey !== sharedMintAuthority.publicKey) {
51
+ sharedMintAuthority.publicKey = metadata.publicKey;
52
+ }
53
+ }
54
+ } else {
55
+ // Old format: file contains {publicKey, secretKey}
56
+ sharedMintAuthority = fileContent;
57
+ }
58
+ } catch (error) {
59
+ console.error("Failed to load shared mint authority:", error);
60
+ return [];
61
+ }
62
+ }
63
63
 
64
- if (!sharedMintAuthority) {
65
- return [];
66
- }
64
+ if (!sharedMintAuthority) {
65
+ return [];
66
+ }
67
67
 
68
- // Load each token that has been cloned
69
- for (const tokenConfig of tokenConfigs) {
70
- const tokenDir = join(workDir, `token-${tokenConfig.symbol.toLowerCase()}`);
71
- const modifiedAccountPath = join(tokenDir, "modified.json");
72
- const metadataAccountPath = join(tokenDir, "metadata.json");
68
+ // Load each token that has been cloned
69
+ for (const tokenConfig of tokenConfigs) {
70
+ const tokenDir = join(workDir, `token-${tokenConfig.symbol.toLowerCase()}`);
71
+ const modifiedAccountPath = join(tokenDir, "modified.json");
72
+ const metadataAccountPath = join(tokenDir, "metadata.json");
73
73
 
74
- // Check if this token has already been cloned
75
- if (existsSync(modifiedAccountPath)) {
76
- const clonedToken: ClonedToken = {
77
- config: tokenConfig,
78
- mintAuthorityPath: sharedMintAuthorityPath,
79
- modifiedAccountPath,
80
- mintAuthority: sharedMintAuthority,
81
- };
74
+ // Check if this token has already been cloned
75
+ if (existsSync(modifiedAccountPath)) {
76
+ const clonedToken: ClonedToken = {
77
+ config: tokenConfig,
78
+ mintAuthorityPath: sharedMintAuthorityPath,
79
+ modifiedAccountPath,
80
+ mintAuthority: sharedMintAuthority,
81
+ };
82
82
 
83
- // Add metadata path if it exists
84
- if (existsSync(metadataAccountPath)) {
85
- clonedToken.metadataAccountPath = metadataAccountPath;
86
- }
83
+ // Add metadata path if it exists
84
+ if (existsSync(metadataAccountPath)) {
85
+ clonedToken.metadataAccountPath = metadataAccountPath;
86
+ }
87
87
 
88
- clonedTokens.push(clonedToken);
89
- }
90
- }
88
+ clonedTokens.push(clonedToken);
89
+ }
90
+ }
91
91
 
92
- return clonedTokens;
92
+ return clonedTokens;
93
93
  }
94
94
 
95
95
  /**
96
96
  * Find a cloned token by its mint address
97
97
  */
98
98
  export function findTokenByMint(
99
- clonedTokens: ClonedToken[],
100
- mintAddress: string
99
+ clonedTokens: ClonedToken[],
100
+ mintAddress: string,
101
101
  ): ClonedToken | undefined {
102
- return clonedTokens.find((token) => token.config.mainnetMint === mintAddress);
102
+ return clonedTokens.find((token) => token.config.mainnetMint === mintAddress);
103
103
  }
104
104
 
105
105
  /**
106
106
  * Find a cloned token by its symbol
107
107
  */
108
108
  export function findTokenBySymbol(
109
- clonedTokens: ClonedToken[],
110
- symbol: string
109
+ clonedTokens: ClonedToken[],
110
+ symbol: string,
111
111
  ): ClonedToken | undefined {
112
- return clonedTokens.find(
113
- (token) => token.config.symbol.toLowerCase() === symbol.toLowerCase()
114
- );
112
+ return clonedTokens.find(
113
+ (token) => token.config.symbol.toLowerCase() === symbol.toLowerCase(),
114
+ );
115
115
  }