solforge 0.2.4 → 0.2.6

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 (82) 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 +69 -61
  6. package/server/lib/base58.ts +1 -1
  7. package/server/methods/account/get-account-info.ts +3 -7
  8. package/server/methods/account/get-balance.ts +3 -7
  9. package/server/methods/account/get-multiple-accounts.ts +2 -1
  10. package/server/methods/account/get-parsed-account-info.ts +3 -7
  11. package/server/methods/account/parsers/index.ts +2 -2
  12. package/server/methods/account/parsers/loader-upgradeable.ts +14 -1
  13. package/server/methods/account/parsers/spl-token.ts +29 -10
  14. package/server/methods/account/request-airdrop.ts +44 -31
  15. package/server/methods/block/get-block.ts +3 -7
  16. package/server/methods/block/get-blocks-with-limit.ts +3 -7
  17. package/server/methods/block/is-blockhash-valid.ts +3 -7
  18. package/server/methods/get-address-lookup-table.ts +3 -7
  19. package/server/methods/program/get-program-accounts.ts +9 -9
  20. package/server/methods/program/get-token-account-balance.ts +3 -7
  21. package/server/methods/program/get-token-accounts-by-delegate.ts +4 -3
  22. package/server/methods/program/get-token-accounts-by-owner.ts +61 -35
  23. package/server/methods/program/get-token-largest-accounts.ts +3 -2
  24. package/server/methods/program/get-token-supply.ts +3 -2
  25. package/server/methods/solforge/index.ts +9 -6
  26. package/server/methods/transaction/get-parsed-transaction.ts +3 -7
  27. package/server/methods/transaction/get-signature-statuses.ts +14 -7
  28. package/server/methods/transaction/get-signatures-for-address.ts +3 -7
  29. package/server/methods/transaction/get-transaction.ts +167 -81
  30. package/server/methods/transaction/send-transaction.ts +29 -16
  31. package/server/methods/transaction/simulate-transaction.ts +3 -2
  32. package/server/rpc-server.ts +47 -34
  33. package/server/types.ts +9 -6
  34. package/server/ws-server.ts +15 -8
  35. package/src/api-server-entry.ts +91 -91
  36. package/src/cli/commands/airdrop.ts +2 -2
  37. package/src/cli/commands/config.ts +2 -2
  38. package/src/cli/commands/mint.ts +3 -3
  39. package/src/cli/commands/program-clone.ts +9 -11
  40. package/src/cli/commands/program-load.ts +3 -3
  41. package/src/cli/commands/rpc-start.ts +8 -5
  42. package/src/cli/commands/token-adopt-authority.ts +1 -1
  43. package/src/cli/commands/token-clone.ts +5 -6
  44. package/src/cli/commands/token-create.ts +5 -5
  45. package/src/cli/main.ts +38 -37
  46. package/src/cli/run-solforge.ts +20 -6
  47. package/src/cli/setup-wizard.ts +8 -6
  48. package/src/commands/add-program.ts +324 -328
  49. package/src/commands/init.ts +106 -106
  50. package/src/commands/list.ts +125 -125
  51. package/src/commands/mint.ts +247 -248
  52. package/src/commands/start.ts +837 -833
  53. package/src/commands/status.ts +80 -80
  54. package/src/commands/stop.ts +381 -382
  55. package/src/config/index.ts +33 -17
  56. package/src/config/manager.ts +150 -150
  57. package/src/db/index.ts +2 -2
  58. package/src/db/tx-store.ts +12 -8
  59. package/src/gui/public/app.css +1556 -1
  60. package/src/gui/public/build/main.css +1569 -1
  61. package/src/gui/server.ts +21 -22
  62. package/src/gui/src/api.ts +1 -1
  63. package/src/gui/src/app.tsx +96 -45
  64. package/src/gui/src/components/airdrop-mint-form.tsx +49 -19
  65. package/src/gui/src/components/clone-program-modal.tsx +31 -12
  66. package/src/gui/src/components/clone-token-modal.tsx +32 -13
  67. package/src/gui/src/components/modal.tsx +18 -11
  68. package/src/gui/src/components/programs-panel.tsx +27 -15
  69. package/src/gui/src/components/status-panel.tsx +32 -18
  70. package/src/gui/src/components/tokens-panel.tsx +25 -19
  71. package/src/gui/src/index.css +491 -463
  72. package/src/index.ts +177 -149
  73. package/src/rpc/start.ts +1 -1
  74. package/src/services/api-server.ts +494 -475
  75. package/src/services/port-manager.ts +164 -167
  76. package/src/services/process-registry.ts +144 -145
  77. package/src/services/program-cloner.ts +312 -312
  78. package/src/services/token-cloner.ts +799 -797
  79. package/src/services/validator.ts +288 -290
  80. package/src/types/config.ts +72 -72
  81. package/src/utils/shell.ts +75 -75
  82. package/src/utils/token-loader.ts +78 -78
@@ -1,317 +1,317 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
- import { join } from "path";
3
- import chalk from "chalk";
4
1
  import { Connection, PublicKey } from "@solana/web3.js";
5
- import { runCommand } from "../utils/shell.js";
2
+ import chalk from "chalk";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
6
5
  import type { ProgramConfig } from "../types/config.js";
6
+ import { runCommand } from "../utils/shell.js";
7
7
 
8
8
  export class ProgramCloner {
9
- private workDir: string;
10
-
11
- constructor(workDir: string = ".solforge") {
12
- this.workDir = workDir;
13
- }
14
-
15
- /**
16
- * Clone programs for validator startup (saved as .so files)
17
- */
18
- async clonePrograms(
19
- programs: ProgramConfig[],
20
- targetCluster: string = "mainnet-beta"
21
- ): Promise<
22
- Array<{
23
- success: boolean;
24
- program: ProgramConfig;
25
- error?: string;
26
- filePath?: string;
27
- }>
28
- > {
29
- console.log(chalk.cyan("\nšŸ”§ Cloning programs from mainnet..."));
30
-
31
- if (!existsSync(this.workDir)) {
32
- mkdirSync(this.workDir, { recursive: true });
33
- }
34
-
35
- const programsDir = join(this.workDir, "programs");
36
- if (!existsSync(programsDir)) {
37
- mkdirSync(programsDir, { recursive: true });
38
- }
39
-
40
- const results = [];
41
-
42
- for (const program of programs) {
43
- console.log(
44
- chalk.gray(
45
- ` šŸ“¦ Processing ${program.name || program.mainnetProgramId}...`
46
- )
47
- );
48
-
49
- try {
50
- // Clone dependencies first
51
- if (program.dependencies && program.dependencies.length > 0) {
52
- console.log(
53
- chalk.gray(
54
- ` šŸ“š Cloning ${program.dependencies.length} dependencies...`
55
- )
56
- );
57
- for (const depId of program.dependencies) {
58
- await this.cloneSingleProgram(depId, programsDir, targetCluster);
59
- }
60
- }
61
-
62
- // Clone the main program
63
- const result = await this.cloneSingleProgram(
64
- program.mainnetProgramId,
65
- programsDir,
66
- targetCluster,
67
- program.name
68
- );
69
-
70
- results.push({
71
- success: true,
72
- program,
73
- filePath: result.filePath,
74
- });
75
-
76
- console.log(chalk.gray(` āœ“ Cloned to ${result.filePath}`));
77
- } catch (error) {
78
- console.error(
79
- chalk.red(` āŒ Failed to clone ${program.mainnetProgramId}`)
80
- );
81
- console.error(
82
- chalk.red(
83
- ` ${error instanceof Error ? error.message : String(error)}`
84
- )
85
- );
86
-
87
- results.push({
88
- success: false,
89
- program,
90
- error: error instanceof Error ? error.message : String(error),
91
- });
92
- }
93
- }
94
-
95
- const successful = results.filter((r) => r.success).length;
96
- console.log(
97
- chalk.cyan(`\nāœ… Cloned ${successful}/${programs.length} programs`)
98
- );
99
-
100
- return results;
101
- }
102
-
103
- /**
104
- * Clone a single program from mainnet
105
- */
106
- private async cloneSingleProgram(
107
- programId: string,
108
- outputDir: string,
109
- cluster: string = "mainnet-beta",
110
- name?: string
111
- ): Promise<{ filePath: string }> {
112
- const fileName = name
113
- ? `${name.toLowerCase().replace(/\s+/g, "-")}.so`
114
- : `${programId}.so`;
115
- const outputPath = join(outputDir, fileName);
116
-
117
- // Skip if already exists
118
- if (existsSync(outputPath)) {
119
- return { filePath: outputPath };
120
- }
121
-
122
- // Use solana account command to fetch program data
123
- const rpcUrl = this.getClusterUrl(cluster);
124
- const accountResult = await runCommand(
125
- "solana",
126
- ["account", programId, "--output", "json", "--url", rpcUrl],
127
- { silent: true }
128
- );
129
-
130
- if (!accountResult.success) {
131
- throw new Error(
132
- `Failed to fetch program account: ${accountResult.stderr}`
133
- );
134
- }
135
-
136
- try {
137
- const accountData = JSON.parse(accountResult.stdout);
138
- const programData = accountData.account.data;
139
-
140
- if (!programData || programData[1] !== "base64") {
141
- throw new Error("Invalid program data format");
142
- }
143
-
144
- // Decode base64 program data
145
- const binaryData = Buffer.from(programData[0], "base64");
146
-
147
- // Write as .so file
148
- writeFileSync(outputPath, binaryData);
149
-
150
- return { filePath: outputPath };
151
- } catch (error) {
152
- throw new Error(
153
- `Failed to process program data: ${
154
- error instanceof Error ? error.message : String(error)
155
- }`
156
- );
157
- }
158
- }
159
-
160
- /**
161
- * Generate validator arguments for cloned programs
162
- */
163
- generateValidatorArgs(
164
- clonedPrograms: Array<{
165
- success: boolean;
166
- program: ProgramConfig;
167
- filePath?: string;
168
- }>
169
- ): string[] {
170
- const args: string[] = [];
171
-
172
- for (const result of clonedPrograms) {
173
- if (result.success && result.filePath) {
174
- args.push("--bpf-program");
175
- args.push(result.program.mainnetProgramId);
176
- args.push(result.filePath);
177
- }
178
- }
179
-
180
- return args;
181
- }
182
-
183
- /**
184
- * Deploy program to running validator (hot deployment)
185
- */
186
- async deployToRunningValidator(
187
- programId: string,
188
- rpcUrl: string,
189
- name?: string
190
- ): Promise<{ success: boolean; deployedAddress?: string; error?: string }> {
191
- try {
192
- console.log(
193
- chalk.cyan(`\nšŸš€ Hot deploying program ${name || programId}...`)
194
- );
195
-
196
- // First, clone the program if we don't have it
197
- const programsDir = join(this.workDir, "programs");
198
- if (!existsSync(programsDir)) {
199
- mkdirSync(programsDir, { recursive: true });
200
- }
201
-
202
- const cloneResult = await this.cloneSingleProgram(
203
- programId,
204
- programsDir,
205
- "mainnet-beta",
206
- name
207
- );
208
-
209
- // Deploy to running validator using solana program deploy
210
- console.log(chalk.gray(" šŸ“¤ Deploying to validator..."));
211
-
212
- const deployResult = await runCommand(
213
- "solana",
214
- [
215
- "program",
216
- "deploy",
217
- cloneResult.filePath,
218
- "--program-id",
219
- programId,
220
- "--url",
221
- rpcUrl,
222
- ],
223
- { silent: false }
224
- );
225
-
226
- if (!deployResult.success) {
227
- return {
228
- success: false,
229
- error: `Deployment failed: ${
230
- deployResult.stderr || deployResult.stdout
231
- }`,
232
- };
233
- }
234
-
235
- console.log(
236
- chalk.green(` āœ… Successfully deployed ${name || programId}`)
237
- );
238
-
239
- return {
240
- success: true,
241
- deployedAddress: programId,
242
- };
243
- } catch (error) {
244
- return {
245
- success: false,
246
- error: error instanceof Error ? error.message : String(error),
247
- };
248
- }
249
- }
250
-
251
- /**
252
- * Get cluster RPC URL
253
- */
254
- private getClusterUrl(cluster: string): string {
255
- switch (cluster) {
256
- case "mainnet-beta":
257
- return "https://api.mainnet-beta.solana.com";
258
- case "devnet":
259
- return "https://api.devnet.solana.com";
260
- case "testnet":
261
- return "https://api.testnet.solana.com";
262
- default:
263
- return cluster; // Assume it's a custom URL
264
- }
265
- }
266
-
267
- /**
268
- * Verify program exists on cluster
269
- */
270
- async verifyProgram(
271
- programId: string,
272
- cluster: string = "mainnet-beta"
273
- ): Promise<boolean> {
274
- try {
275
- const connection = new Connection(this.getClusterUrl(cluster));
276
- const programAccount = await connection.getAccountInfo(
277
- new PublicKey(programId)
278
- );
279
- return programAccount !== null && programAccount.executable;
280
- } catch {
281
- return false;
282
- }
283
- }
284
-
285
- /**
286
- * Get program info from cluster
287
- */
288
- async getProgramInfo(
289
- programId: string,
290
- cluster: string = "mainnet-beta"
291
- ): Promise<{
292
- exists: boolean;
293
- executable?: boolean;
294
- owner?: string;
295
- size?: number;
296
- }> {
297
- try {
298
- const connection = new Connection(this.getClusterUrl(cluster));
299
- const programAccount = await connection.getAccountInfo(
300
- new PublicKey(programId)
301
- );
302
-
303
- if (!programAccount) {
304
- return { exists: false };
305
- }
306
-
307
- return {
308
- exists: true,
309
- executable: programAccount.executable,
310
- owner: programAccount.owner.toBase58(),
311
- size: programAccount.data.length,
312
- };
313
- } catch (error) {
314
- return { exists: false };
315
- }
316
- }
9
+ private workDir: string;
10
+
11
+ constructor(workDir: string = ".solforge") {
12
+ this.workDir = workDir;
13
+ }
14
+
15
+ /**
16
+ * Clone programs for validator startup (saved as .so files)
17
+ */
18
+ async clonePrograms(
19
+ programs: ProgramConfig[],
20
+ targetCluster: string = "mainnet-beta",
21
+ ): Promise<
22
+ Array<{
23
+ success: boolean;
24
+ program: ProgramConfig;
25
+ error?: string;
26
+ filePath?: string;
27
+ }>
28
+ > {
29
+ console.log(chalk.cyan("\nšŸ”§ Cloning programs from mainnet..."));
30
+
31
+ if (!existsSync(this.workDir)) {
32
+ mkdirSync(this.workDir, { recursive: true });
33
+ }
34
+
35
+ const programsDir = join(this.workDir, "programs");
36
+ if (!existsSync(programsDir)) {
37
+ mkdirSync(programsDir, { recursive: true });
38
+ }
39
+
40
+ const results = [];
41
+
42
+ for (const program of programs) {
43
+ console.log(
44
+ chalk.gray(
45
+ ` šŸ“¦ Processing ${program.name || program.mainnetProgramId}...`,
46
+ ),
47
+ );
48
+
49
+ try {
50
+ // Clone dependencies first
51
+ if (program.dependencies && program.dependencies.length > 0) {
52
+ console.log(
53
+ chalk.gray(
54
+ ` šŸ“š Cloning ${program.dependencies.length} dependencies...`,
55
+ ),
56
+ );
57
+ for (const depId of program.dependencies) {
58
+ await this.cloneSingleProgram(depId, programsDir, targetCluster);
59
+ }
60
+ }
61
+
62
+ // Clone the main program
63
+ const result = await this.cloneSingleProgram(
64
+ program.mainnetProgramId,
65
+ programsDir,
66
+ targetCluster,
67
+ program.name,
68
+ );
69
+
70
+ results.push({
71
+ success: true,
72
+ program,
73
+ filePath: result.filePath,
74
+ });
75
+
76
+ console.log(chalk.gray(` āœ“ Cloned to ${result.filePath}`));
77
+ } catch (error) {
78
+ console.error(
79
+ chalk.red(` āŒ Failed to clone ${program.mainnetProgramId}`),
80
+ );
81
+ console.error(
82
+ chalk.red(
83
+ ` ${error instanceof Error ? error.message : String(error)}`,
84
+ ),
85
+ );
86
+
87
+ results.push({
88
+ success: false,
89
+ program,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ });
92
+ }
93
+ }
94
+
95
+ const successful = results.filter((r) => r.success).length;
96
+ console.log(
97
+ chalk.cyan(`\nāœ… Cloned ${successful}/${programs.length} programs`),
98
+ );
99
+
100
+ return results;
101
+ }
102
+
103
+ /**
104
+ * Clone a single program from mainnet
105
+ */
106
+ private async cloneSingleProgram(
107
+ programId: string,
108
+ outputDir: string,
109
+ cluster: string = "mainnet-beta",
110
+ name?: string,
111
+ ): Promise<{ filePath: string }> {
112
+ const fileName = name
113
+ ? `${name.toLowerCase().replace(/\s+/g, "-")}.so`
114
+ : `${programId}.so`;
115
+ const outputPath = join(outputDir, fileName);
116
+
117
+ // Skip if already exists
118
+ if (existsSync(outputPath)) {
119
+ return { filePath: outputPath };
120
+ }
121
+
122
+ // Use solana account command to fetch program data
123
+ const rpcUrl = this.getClusterUrl(cluster);
124
+ const accountResult = await runCommand(
125
+ "solana",
126
+ ["account", programId, "--output", "json", "--url", rpcUrl],
127
+ { silent: true },
128
+ );
129
+
130
+ if (!accountResult.success) {
131
+ throw new Error(
132
+ `Failed to fetch program account: ${accountResult.stderr}`,
133
+ );
134
+ }
135
+
136
+ try {
137
+ const accountData = JSON.parse(accountResult.stdout);
138
+ const programData = accountData.account.data;
139
+
140
+ if (!programData || programData[1] !== "base64") {
141
+ throw new Error("Invalid program data format");
142
+ }
143
+
144
+ // Decode base64 program data
145
+ const binaryData = Buffer.from(programData[0], "base64");
146
+
147
+ // Write as .so file
148
+ writeFileSync(outputPath, binaryData);
149
+
150
+ return { filePath: outputPath };
151
+ } catch (error) {
152
+ throw new Error(
153
+ `Failed to process program data: ${
154
+ error instanceof Error ? error.message : String(error)
155
+ }`,
156
+ );
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Generate validator arguments for cloned programs
162
+ */
163
+ generateValidatorArgs(
164
+ clonedPrograms: Array<{
165
+ success: boolean;
166
+ program: ProgramConfig;
167
+ filePath?: string;
168
+ }>,
169
+ ): string[] {
170
+ const args: string[] = [];
171
+
172
+ for (const result of clonedPrograms) {
173
+ if (result.success && result.filePath) {
174
+ args.push("--bpf-program");
175
+ args.push(result.program.mainnetProgramId);
176
+ args.push(result.filePath);
177
+ }
178
+ }
179
+
180
+ return args;
181
+ }
182
+
183
+ /**
184
+ * Deploy program to running validator (hot deployment)
185
+ */
186
+ async deployToRunningValidator(
187
+ programId: string,
188
+ rpcUrl: string,
189
+ name?: string,
190
+ ): Promise<{ success: boolean; deployedAddress?: string; error?: string }> {
191
+ try {
192
+ console.log(
193
+ chalk.cyan(`\nšŸš€ Hot deploying program ${name || programId}...`),
194
+ );
195
+
196
+ // First, clone the program if we don't have it
197
+ const programsDir = join(this.workDir, "programs");
198
+ if (!existsSync(programsDir)) {
199
+ mkdirSync(programsDir, { recursive: true });
200
+ }
201
+
202
+ const cloneResult = await this.cloneSingleProgram(
203
+ programId,
204
+ programsDir,
205
+ "mainnet-beta",
206
+ name,
207
+ );
208
+
209
+ // Deploy to running validator using solana program deploy
210
+ console.log(chalk.gray(" šŸ“¤ Deploying to validator..."));
211
+
212
+ const deployResult = await runCommand(
213
+ "solana",
214
+ [
215
+ "program",
216
+ "deploy",
217
+ cloneResult.filePath,
218
+ "--program-id",
219
+ programId,
220
+ "--url",
221
+ rpcUrl,
222
+ ],
223
+ { silent: false },
224
+ );
225
+
226
+ if (!deployResult.success) {
227
+ return {
228
+ success: false,
229
+ error: `Deployment failed: ${
230
+ deployResult.stderr || deployResult.stdout
231
+ }`,
232
+ };
233
+ }
234
+
235
+ console.log(
236
+ chalk.green(` āœ… Successfully deployed ${name || programId}`),
237
+ );
238
+
239
+ return {
240
+ success: true,
241
+ deployedAddress: programId,
242
+ };
243
+ } catch (error) {
244
+ return {
245
+ success: false,
246
+ error: error instanceof Error ? error.message : String(error),
247
+ };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Get cluster RPC URL
253
+ */
254
+ private getClusterUrl(cluster: string): string {
255
+ switch (cluster) {
256
+ case "mainnet-beta":
257
+ return "https://api.mainnet-beta.solana.com";
258
+ case "devnet":
259
+ return "https://api.devnet.solana.com";
260
+ case "testnet":
261
+ return "https://api.testnet.solana.com";
262
+ default:
263
+ return cluster; // Assume it's a custom URL
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Verify program exists on cluster
269
+ */
270
+ async verifyProgram(
271
+ programId: string,
272
+ cluster: string = "mainnet-beta",
273
+ ): Promise<boolean> {
274
+ try {
275
+ const connection = new Connection(this.getClusterUrl(cluster));
276
+ const programAccount = await connection.getAccountInfo(
277
+ new PublicKey(programId),
278
+ );
279
+ return programAccount?.executable;
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Get program info from cluster
287
+ */
288
+ async getProgramInfo(
289
+ programId: string,
290
+ cluster: string = "mainnet-beta",
291
+ ): Promise<{
292
+ exists: boolean;
293
+ executable?: boolean;
294
+ owner?: string;
295
+ size?: number;
296
+ }> {
297
+ try {
298
+ const connection = new Connection(this.getClusterUrl(cluster));
299
+ const programAccount = await connection.getAccountInfo(
300
+ new PublicKey(programId),
301
+ );
302
+
303
+ if (!programAccount) {
304
+ return { exists: false };
305
+ }
306
+
307
+ return {
308
+ exists: true,
309
+ executable: programAccount.executable,
310
+ owner: programAccount.owner.toBase58(),
311
+ size: programAccount.data.length,
312
+ };
313
+ } catch (_error) {
314
+ return { exists: false };
315
+ }
316
+ }
317
317
  }