solforge 0.2.12 → 0.2.14

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 (175) hide show
  1. package/package.json +1 -5
  2. package/start.cjs +19 -23
  3. package/docs/API.md +0 -379
  4. package/docs/CONFIGURATION.md +0 -407
  5. package/docs/bun-single-file-executable.md +0 -585
  6. package/docs/cli-plan.md +0 -154
  7. package/docs/data-indexing-plan.md +0 -214
  8. package/docs/gui-roadmap.md +0 -202
  9. package/scripts/decode-b58.ts +0 -10
  10. package/scripts/install.sh +0 -112
  11. package/server/index.ts +0 -5
  12. package/server/lib/base58.ts +0 -33
  13. package/server/lib/faucet.ts +0 -110
  14. package/server/lib/instruction-parser.ts +0 -328
  15. package/server/lib/parsers/spl-associated-token-account.ts +0 -50
  16. package/server/lib/parsers/spl-token.ts +0 -340
  17. package/server/lib/spl-token.ts +0 -57
  18. package/server/methods/TEMPLATE.md +0 -117
  19. package/server/methods/account/get-account-info.ts +0 -86
  20. package/server/methods/account/get-balance.ts +0 -23
  21. package/server/methods/account/get-multiple-accounts.ts +0 -84
  22. package/server/methods/account/get-parsed-account-info.ts +0 -17
  23. package/server/methods/account/index.ts +0 -12
  24. package/server/methods/account/parsers/index.ts +0 -52
  25. package/server/methods/account/parsers/loader-upgradeable.ts +0 -79
  26. package/server/methods/account/parsers/spl-token.ts +0 -256
  27. package/server/methods/account/parsers/system.ts +0 -4
  28. package/server/methods/account/request-airdrop.ts +0 -271
  29. package/server/methods/admin/adopt-mint-authority.ts +0 -94
  30. package/server/methods/admin/clone-program-accounts.ts +0 -55
  31. package/server/methods/admin/clone-program.ts +0 -152
  32. package/server/methods/admin/clone-token-accounts.ts +0 -117
  33. package/server/methods/admin/clone-token-mint.ts +0 -82
  34. package/server/methods/admin/create-mint.ts +0 -114
  35. package/server/methods/admin/create-token-account.ts +0 -137
  36. package/server/methods/admin/helpers.ts +0 -70
  37. package/server/methods/admin/index.ts +0 -10
  38. package/server/methods/admin/list-mints.ts +0 -21
  39. package/server/methods/admin/load-program.ts +0 -52
  40. package/server/methods/admin/mint-to.ts +0 -266
  41. package/server/methods/block/get-block-height.ts +0 -5
  42. package/server/methods/block/get-block.ts +0 -31
  43. package/server/methods/block/get-blocks-with-limit.ts +0 -19
  44. package/server/methods/block/get-latest-blockhash.ts +0 -12
  45. package/server/methods/block/get-slot.ts +0 -5
  46. package/server/methods/block/index.ts +0 -6
  47. package/server/methods/block/is-blockhash-valid.ts +0 -19
  48. package/server/methods/epoch/get-cluster-nodes.ts +0 -17
  49. package/server/methods/epoch/get-epoch-info.ts +0 -16
  50. package/server/methods/epoch/get-epoch-schedule.ts +0 -15
  51. package/server/methods/epoch/get-highest-snapshot-slot.ts +0 -9
  52. package/server/methods/epoch/get-leader-schedule.ts +0 -8
  53. package/server/methods/epoch/get-max-retransmit-slot.ts +0 -9
  54. package/server/methods/epoch/get-max-shred-insert-slot.ts +0 -9
  55. package/server/methods/epoch/get-slot-leader.ts +0 -6
  56. package/server/methods/epoch/get-slot-leaders.ts +0 -9
  57. package/server/methods/epoch/get-stake-activation.ts +0 -9
  58. package/server/methods/epoch/get-stake-minimum-delegation.ts +0 -9
  59. package/server/methods/epoch/get-vote-accounts.ts +0 -19
  60. package/server/methods/epoch/index.ts +0 -13
  61. package/server/methods/epoch/minimum-ledger-slot.ts +0 -5
  62. package/server/methods/fee/get-fee-calculator-for-blockhash.ts +0 -12
  63. package/server/methods/fee/get-fee-for-message.ts +0 -8
  64. package/server/methods/fee/get-fee-rate-governor.ts +0 -16
  65. package/server/methods/fee/get-fees.ts +0 -14
  66. package/server/methods/fee/get-recent-prioritization-fees.ts +0 -22
  67. package/server/methods/fee/index.ts +0 -5
  68. package/server/methods/get-address-lookup-table.ts +0 -27
  69. package/server/methods/index.ts +0 -265
  70. package/server/methods/performance/get-recent-performance-samples.ts +0 -25
  71. package/server/methods/performance/get-transaction-count.ts +0 -5
  72. package/server/methods/performance/index.ts +0 -2
  73. package/server/methods/program/get-block-commitment.ts +0 -9
  74. package/server/methods/program/get-block-production.ts +0 -14
  75. package/server/methods/program/get-block-time.ts +0 -21
  76. package/server/methods/program/get-blocks.ts +0 -11
  77. package/server/methods/program/get-first-available-block.ts +0 -9
  78. package/server/methods/program/get-genesis-hash.ts +0 -6
  79. package/server/methods/program/get-identity.ts +0 -6
  80. package/server/methods/program/get-inflation-governor.ts +0 -15
  81. package/server/methods/program/get-inflation-rate.ts +0 -10
  82. package/server/methods/program/get-inflation-reward.ts +0 -12
  83. package/server/methods/program/get-largest-accounts.ts +0 -8
  84. package/server/methods/program/get-parsed-program-accounts.ts +0 -12
  85. package/server/methods/program/get-parsed-token-accounts-by-delegate.ts +0 -12
  86. package/server/methods/program/get-parsed-token-accounts-by-owner.ts +0 -12
  87. package/server/methods/program/get-program-accounts.ts +0 -221
  88. package/server/methods/program/get-supply.ts +0 -13
  89. package/server/methods/program/get-token-account-balance.ts +0 -60
  90. package/server/methods/program/get-token-accounts-by-delegate.ts +0 -82
  91. package/server/methods/program/get-token-accounts-by-owner.ts +0 -416
  92. package/server/methods/program/get-token-largest-accounts.ts +0 -81
  93. package/server/methods/program/get-token-supply.ts +0 -39
  94. package/server/methods/program/index.ts +0 -21
  95. package/server/methods/solforge/index.ts +0 -158
  96. package/server/methods/system/get-health.ts +0 -5
  97. package/server/methods/system/get-minimum-balance-for-rent-exemption.ts +0 -13
  98. package/server/methods/system/get-version.ts +0 -9
  99. package/server/methods/system/index.ts +0 -3
  100. package/server/methods/transaction/get-confirmed-transaction.ts +0 -11
  101. package/server/methods/transaction/get-parsed-transaction.ts +0 -17
  102. package/server/methods/transaction/get-signature-statuses.ts +0 -79
  103. package/server/methods/transaction/get-signatures-for-address.ts +0 -41
  104. package/server/methods/transaction/get-transaction.ts +0 -639
  105. package/server/methods/transaction/index.ts +0 -7
  106. package/server/methods/transaction/inner-instructions.test.ts +0 -104
  107. package/server/methods/transaction/send-transaction.ts +0 -469
  108. package/server/methods/transaction/simulate-transaction.ts +0 -57
  109. package/server/rpc-server.ts +0 -521
  110. package/server/types.ts +0 -109
  111. package/server/ws-server.ts +0 -178
  112. package/src/api-server-entry.ts +0 -109
  113. package/src/cli/bootstrap.ts +0 -67
  114. package/src/cli/commands/airdrop.ts +0 -37
  115. package/src/cli/commands/config.ts +0 -39
  116. package/src/cli/commands/mint.ts +0 -187
  117. package/src/cli/commands/program-clone.ts +0 -122
  118. package/src/cli/commands/program-load.ts +0 -64
  119. package/src/cli/commands/rpc-start.ts +0 -49
  120. package/src/cli/commands/token-adopt-authority.ts +0 -37
  121. package/src/cli/commands/token-clone.ts +0 -112
  122. package/src/cli/commands/token-create.ts +0 -81
  123. package/src/cli/main.ts +0 -158
  124. package/src/cli/run-solforge.ts +0 -112
  125. package/src/cli/setup-utils.ts +0 -54
  126. package/src/cli/setup-wizard.ts +0 -258
  127. package/src/cli/utils/args.ts +0 -15
  128. package/src/commands/add-program.ts +0 -333
  129. package/src/commands/init.ts +0 -122
  130. package/src/commands/list.ts +0 -136
  131. package/src/commands/mint.ts +0 -287
  132. package/src/commands/start.ts +0 -881
  133. package/src/commands/status.ts +0 -99
  134. package/src/commands/stop.ts +0 -405
  135. package/src/config/index.ts +0 -146
  136. package/src/config/manager.ts +0 -157
  137. package/src/db/index.ts +0 -83
  138. package/src/db/schema/accounts.ts +0 -23
  139. package/src/db/schema/address-signatures.ts +0 -31
  140. package/src/db/schema/index.ts +0 -6
  141. package/src/db/schema/meta-kv.ts +0 -9
  142. package/src/db/schema/transactions.ts +0 -36
  143. package/src/db/schema/tx-account-states.ts +0 -23
  144. package/src/db/schema/tx-accounts.ts +0 -33
  145. package/src/db/tx-store.ts +0 -264
  146. package/src/gui/public/app.css +0 -1556
  147. package/src/gui/public/build/main.css +0 -1569
  148. package/src/gui/public/build/main.js +0 -303
  149. package/src/gui/public/build/main.js.txt +0 -231
  150. package/src/gui/public/index.html +0 -19
  151. package/src/gui/server.ts +0 -296
  152. package/src/gui/src/api.ts +0 -127
  153. package/src/gui/src/app.tsx +0 -441
  154. package/src/gui/src/components/airdrop-mint-form.tsx +0 -246
  155. package/src/gui/src/components/clone-program-modal.tsx +0 -202
  156. package/src/gui/src/components/clone-token-modal.tsx +0 -230
  157. package/src/gui/src/components/modal.tsx +0 -134
  158. package/src/gui/src/components/programs-panel.tsx +0 -124
  159. package/src/gui/src/components/status-panel.tsx +0 -136
  160. package/src/gui/src/components/tokens-panel.tsx +0 -122
  161. package/src/gui/src/hooks/use-interval.ts +0 -17
  162. package/src/gui/src/index.css +0 -557
  163. package/src/gui/src/main.tsx +0 -17
  164. package/src/index.ts +0 -216
  165. package/src/migrations-bundled.ts +0 -23
  166. package/src/rpc/start.ts +0 -44
  167. package/src/services/api-server.ts +0 -504
  168. package/src/services/port-manager.ts +0 -174
  169. package/src/services/process-registry.ts +0 -153
  170. package/src/services/program-cloner.ts +0 -317
  171. package/src/services/token-cloner.ts +0 -811
  172. package/src/services/validator.ts +0 -293
  173. package/src/types/config.ts +0 -110
  174. package/src/utils/shell.ts +0 -110
  175. package/src/utils/token-loader.ts +0 -115
@@ -1,881 +0,0 @@
1
- import chalk from "chalk";
2
- import { spawn } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import ora from "ora";
5
- import { join } from "node:path";
6
- import { configManager } from "../config/manager.js";
7
- import { portManager } from "../services/port-manager.js";
8
- import type { RunningValidator } from "../services/process-registry.js";
9
- import { processRegistry } from "../services/process-registry.js";
10
- import type { ClonedToken } from "../services/token-cloner.js";
11
- import { TokenCloner } from "../services/token-cloner.js";
12
- import type { Config, TokenConfig } from "../types/config.js";
13
- import { checkSolanaTools, runCommand } from "../utils/shell.js";
14
-
15
- function generateValidatorId(name: string): string {
16
- const timestamp = Date.now();
17
- const randomSuffix = Math.random().toString(36).substring(2, 8);
18
- const safeName = name.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
19
- return `${safeName}-${timestamp}-${randomSuffix}`;
20
- }
21
-
22
- export async function startCommand(
23
- debug: boolean = false,
24
- network: boolean = false,
25
- ): Promise<void> {
26
- // Check prerequisites
27
- const tools = await checkSolanaTools();
28
- if (!tools.solana) {
29
- console.error(chalk.red("āŒ solana CLI not found"));
30
- console.log(
31
- chalk.yellow(
32
- "šŸ’” Install it with: sh -c \"$(curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash)\"",
33
- ),
34
- );
35
- process.exit(1);
36
- }
37
-
38
- // Load configuration
39
- let config: Config;
40
- try {
41
- await configManager.load("./sf.config.json");
42
- config = configManager.getConfig();
43
- } catch (error) {
44
- console.error(chalk.red("āŒ Failed to load sf.config.json"));
45
- console.error(
46
- chalk.red(error instanceof Error ? error.message : String(error)),
47
- );
48
- console.log(
49
- chalk.yellow("šŸ’” Run `solforge init` to create a configuration"),
50
- );
51
- process.exit(1);
52
- }
53
-
54
- // Check if validator is already running on configured ports first
55
- const checkResult = await runCommand(
56
- "curl",
57
- [
58
- "-s",
59
- "-X",
60
- "POST",
61
- `http://127.0.0.1:${config.localnet.port}`,
62
- "-H",
63
- "Content-Type: application/json",
64
- "-d",
65
- '{"jsonrpc":"2.0","id":1,"method":"getHealth"}',
66
- ],
67
- { silent: true, debug: false },
68
- );
69
-
70
- if (checkResult.success && checkResult.stdout.includes("ok")) {
71
- console.log(chalk.yellow("āš ļø Validator is already running"));
72
- console.log(
73
- chalk.cyan(`🌐 RPC URL: http://127.0.0.1:${config.localnet.port}`),
74
- );
75
- console.log(
76
- chalk.cyan(
77
- `šŸ’° Faucet URL: http://127.0.0.1:${config.localnet.faucetPort}`,
78
- ),
79
- );
80
-
81
- // Clone tokens if needed, even when validator is already running
82
- const clonedTokens: ClonedToken[] = [];
83
- if (config.tokens.length > 0) {
84
- const tokenCloner = new TokenCloner();
85
-
86
- // Check which tokens are already cloned and which need to be cloned
87
- const { existingTokens, tokensToClone } = await checkExistingClonedTokens(
88
- config.tokens,
89
- tokenCloner,
90
- );
91
-
92
- if (existingTokens.length > 0) {
93
- console.log(
94
- chalk.green(
95
- `šŸ“ Found ${existingTokens.length} already cloned tokens`,
96
- ),
97
- );
98
- if (debug) {
99
- existingTokens.forEach((token: ClonedToken) => {
100
- console.log(
101
- chalk.gray(
102
- ` āœ“ ${token.config.symbol} (${token.config.mainnetMint})`,
103
- ),
104
- );
105
- });
106
- }
107
- clonedTokens.push(...existingTokens);
108
- }
109
-
110
- if (tokensToClone.length > 0) {
111
- console.log(
112
- chalk.yellow(
113
- `šŸ“¦ Cloning ${tokensToClone.length} new tokens from mainnet...\n`,
114
- ),
115
- );
116
- try {
117
- const newlyClonedTokens = await tokenCloner.cloneTokens(
118
- tokensToClone,
119
- config.localnet.rpc,
120
- debug,
121
- );
122
- clonedTokens.push(...newlyClonedTokens);
123
- console.log(
124
- chalk.green(
125
- `āœ… Successfully cloned ${newlyClonedTokens.length} new tokens\n`,
126
- ),
127
- );
128
- } catch (error) {
129
- console.error(chalk.red("āŒ Failed to clone tokens:"));
130
- console.error(
131
- chalk.red(error instanceof Error ? error.message : String(error)),
132
- );
133
- console.log(
134
- chalk.yellow(
135
- "šŸ’” You can start without tokens by removing them from sf.config.json",
136
- ),
137
- );
138
- process.exit(1);
139
- }
140
- } else if (existingTokens.length > 0) {
141
- console.log(
142
- chalk.green("āœ… All tokens already cloned, skipping clone step\n"),
143
- );
144
- }
145
- }
146
-
147
- // Airdrop SOL to mint authority if tokens were cloned (even when validator already running)
148
- if (clonedTokens.length > 0) {
149
- console.log(chalk.yellow("\nšŸ’ø Airdropping SOL to mint authority..."));
150
- const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
151
-
152
- try {
153
- await airdropSolToMintAuthority(clonedTokens[0], rpcUrl, debug);
154
- console.log(chalk.green("āœ… SOL airdropped successfully!"));
155
- } catch (error) {
156
- console.error(chalk.red("āŒ Failed to airdrop SOL:"));
157
- console.error(
158
- chalk.red(error instanceof Error ? error.message : String(error)),
159
- );
160
- console.log(
161
- chalk.yellow(
162
- "šŸ’” You may need to manually airdrop SOL for fee payments",
163
- ),
164
- );
165
- }
166
- }
167
-
168
- // Still mint tokens if any were cloned
169
- if (clonedTokens.length > 0) {
170
- console.log(chalk.yellow("\nšŸ’° Minting tokens..."));
171
- const tokenCloner = new TokenCloner();
172
- const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
173
-
174
- if (debug) {
175
- console.log(
176
- chalk.gray(`šŸ› Minting ${clonedTokens.length} tokens to recipients:`),
177
- );
178
- clonedTokens.forEach((token, index) => {
179
- console.log(
180
- chalk.gray(
181
- ` ${index + 1}. ${token.config.symbol} (${
182
- token.config.mainnetMint
183
- }) - ${token.config.mintAmount} tokens`,
184
- ),
185
- );
186
- });
187
- console.log(chalk.gray(`🌐 Using RPC: ${rpcUrl}`));
188
- }
189
-
190
- try {
191
- await tokenCloner.mintTokensToRecipients(clonedTokens, rpcUrl, debug);
192
- console.log(chalk.green("āœ… Token minting completed!"));
193
-
194
- if (debug) {
195
- console.log(
196
- chalk.gray(
197
- "šŸ› All tokens have been minted to their respective recipients",
198
- ),
199
- );
200
- }
201
- } catch (error) {
202
- console.error(chalk.red("āŒ Failed to mint tokens:"));
203
- console.error(
204
- chalk.red(error instanceof Error ? error.message : String(error)),
205
- );
206
- console.log(
207
- chalk.yellow("šŸ’” Validator is still running, you can mint manually"),
208
- );
209
- }
210
- }
211
- return;
212
- }
213
-
214
- // Generate unique ID for this validator instance
215
- const validatorId = generateValidatorId(config.name);
216
-
217
- // Get available ports (only if validator is not already running)
218
- const ports = await portManager.getRecommendedPorts(config);
219
- if (
220
- ports.rpcPort !== config.localnet.port ||
221
- ports.faucetPort !== config.localnet.faucetPort
222
- ) {
223
- console.log(
224
- chalk.yellow(
225
- `āš ļø Configured ports not available, using: RPC ${ports.rpcPort}, Faucet ${ports.faucetPort}`,
226
- ),
227
- );
228
- // Update config with available ports
229
- config.localnet.port = ports.rpcPort;
230
- config.localnet.faucetPort = ports.faucetPort;
231
- }
232
-
233
- console.log(chalk.blue(`šŸš€ Starting ${config.name} (${validatorId})...\n`));
234
- console.log(chalk.gray(`šŸ“” RPC Port: ${config.localnet.port}`));
235
- console.log(chalk.gray(`šŸ’° Faucet Port: ${config.localnet.faucetPort}\n`));
236
-
237
- // Programs will be cloned automatically by validator using --clone-program flags
238
- if (config.programs.length > 0) {
239
- console.log(
240
- chalk.cyan(
241
- `šŸ”§ Will clone ${config.programs.length} programs from mainnet during startup\n`,
242
- ),
243
- );
244
- }
245
-
246
- // Clone tokens after programs
247
- const clonedTokens: ClonedToken[] = [];
248
- if (config.tokens.length > 0) {
249
- const tokenCloner = new TokenCloner();
250
-
251
- // Check which tokens are already cloned and which need to be cloned
252
- const { existingTokens, tokensToClone } = await checkExistingClonedTokens(
253
- config.tokens,
254
- tokenCloner,
255
- );
256
-
257
- if (existingTokens.length > 0) {
258
- console.log(
259
- chalk.green(`šŸ“ Found ${existingTokens.length} already cloned tokens`),
260
- );
261
- if (debug) {
262
- existingTokens.forEach((token: ClonedToken) => {
263
- console.log(
264
- chalk.gray(
265
- ` āœ“ ${token.config.symbol} (${token.config.mainnetMint})`,
266
- ),
267
- );
268
- });
269
- }
270
- clonedTokens.push(...existingTokens);
271
- }
272
-
273
- if (tokensToClone.length > 0) {
274
- console.log(
275
- chalk.yellow(
276
- `šŸ“¦ Cloning ${tokensToClone.length} new tokens from mainnet...\n`,
277
- ),
278
- );
279
- try {
280
- const newlyClonedTokens = await tokenCloner.cloneTokens(
281
- tokensToClone,
282
- config.localnet.rpc,
283
- debug,
284
- );
285
- clonedTokens.push(...newlyClonedTokens);
286
- console.log(
287
- chalk.green(
288
- `āœ… Successfully cloned ${newlyClonedTokens.length} new tokens\n`,
289
- ),
290
- );
291
- } catch (error) {
292
- console.error(chalk.red("āŒ Failed to clone tokens:"));
293
- console.error(
294
- chalk.red(error instanceof Error ? error.message : String(error)),
295
- );
296
- console.log(
297
- chalk.yellow(
298
- "šŸ’” You can start without tokens by removing them from sf.config.json",
299
- ),
300
- );
301
- process.exit(1);
302
- }
303
- } else if (existingTokens.length > 0) {
304
- console.log(
305
- chalk.green("āœ… All tokens already cloned, skipping clone step\n"),
306
- );
307
- }
308
- }
309
-
310
- // Build validator command arguments
311
- const args = buildValidatorArgs(config, clonedTokens);
312
-
313
- console.log(chalk.gray("Command to run:"));
314
- console.log(chalk.gray(`solana-test-validator ${args.join(" ")}\n`));
315
-
316
- if (debug) {
317
- console.log(chalk.yellow("šŸ› Debug mode enabled"));
318
- console.log(chalk.gray("Full command details:"));
319
- console.log(chalk.gray(` Command: solana-test-validator`));
320
- console.log(chalk.gray(` Arguments: ${JSON.stringify(args, null, 2)}`));
321
- }
322
-
323
- // Start the validator
324
- const spinner = ora("Starting Solana test validator...").start();
325
-
326
- try {
327
- // Start validator in background
328
- const validatorProcess = await startValidatorInBackground(
329
- "solana-test-validator",
330
- args,
331
- debug,
332
- );
333
-
334
- // Wait for validator to be ready
335
- spinner.text = "Waiting for validator to be ready...";
336
- await waitForValidatorReady(
337
- `http://127.0.0.1:${config.localnet.port}`,
338
- debug,
339
- );
340
-
341
- // Find an available port for the API server
342
- let apiServerPort = 3000;
343
- while (!(await portManager.isPortAvailable(apiServerPort))) {
344
- apiServerPort++;
345
- if (apiServerPort > 3100) {
346
- throw new Error("Could not find available port for API server");
347
- }
348
- }
349
-
350
- // Start the API server as a background process
351
- let apiServerPid: number | undefined;
352
- let apiResult: { success: boolean; error?: string } = {
353
- success: false,
354
- error: "Not started",
355
- };
356
-
357
- try {
358
- const currentDir = process.cwd();
359
- const testpilotDir = join(__dirname, "..", "..");
360
- const apiServerScript = join(testpilotDir, "src", "api-server-entry.ts");
361
- const configPath =
362
- configManager.getConfigPath() ?? join(currentDir, "sf.config.json");
363
- const workDir = join(currentDir, ".solforge");
364
-
365
- // Start API server in background using runCommand with nohup
366
- const hostFlag = network ? ` --host "0.0.0.0"` : "";
367
- const apiServerCommand = `nohup bun run "${apiServerScript}" --port ${apiServerPort} --config "${configPath}" --rpc-url "http://127.0.0.1:${config.localnet.port}" --faucet-url "http://127.0.0.1:${config.localnet.faucetPort}" --work-dir "${workDir}"${hostFlag} > /dev/null 2>&1 &`;
368
-
369
- const startResult = await runCommand("sh", ["-c", apiServerCommand], {
370
- silent: !debug,
371
- debug: debug,
372
- });
373
-
374
- if (startResult.success) {
375
- // Wait a moment for the API server to start
376
- await new Promise((resolve) => setTimeout(resolve, 3000));
377
-
378
- // Test if the API server is responding
379
- try {
380
- const healthCheckHost = network ? "0.0.0.0" : "127.0.0.1";
381
- const response = await fetch(
382
- `http://${healthCheckHost}:${apiServerPort}/api/health`,
383
- );
384
- if (response.ok) {
385
- apiResult = { success: true };
386
- // Get the PID of the API server process
387
- const pidResult = await runCommand(
388
- "pgrep",
389
- ["-f", `api-server-entry.*--port ${apiServerPort}`],
390
- { silent: true, debug: false },
391
- );
392
- if (pidResult.success && pidResult.stdout.trim()) {
393
- const pidLine = pidResult.stdout.trim().split("\n")[0];
394
- if (pidLine) {
395
- apiServerPid = parseInt(pidLine, 10);
396
- }
397
- }
398
- } else {
399
- apiResult = {
400
- success: false,
401
- error: `Health check failed: ${response.status}`,
402
- };
403
- }
404
- } catch (error) {
405
- apiResult = {
406
- success: false,
407
- error: `Health check failed: ${
408
- error instanceof Error ? error.message : String(error)
409
- }`,
410
- };
411
- }
412
- } else {
413
- apiResult = {
414
- success: false,
415
- error: `Failed to start API server: ${
416
- startResult.stderr || "Unknown error"
417
- }`,
418
- };
419
- }
420
- } catch (error) {
421
- apiResult = {
422
- success: false,
423
- error: error instanceof Error ? error.message : String(error),
424
- };
425
- }
426
-
427
- if (!apiResult.success) {
428
- console.warn(
429
- chalk.yellow("āš ļø Failed to start API server:", apiResult.error),
430
- );
431
- }
432
-
433
- // Register the running validator
434
- const runningValidator: RunningValidator = {
435
- id: validatorId,
436
- name: config.name,
437
- pid: validatorProcess.pid,
438
- rpcPort: config.localnet.port,
439
- faucetPort: config.localnet.faucetPort,
440
- rpcUrl: `http://127.0.0.1:${config.localnet.port}`,
441
- faucetUrl: `http://127.0.0.1:${config.localnet.faucetPort}`,
442
- configPath: configManager.getConfigPath() || "./sf.config.json",
443
- startTime: new Date(),
444
- status: "running",
445
- apiServerPort: apiResult.success ? apiServerPort : undefined,
446
- apiServerUrl: apiResult.success
447
- ? `http://${network ? "0.0.0.0" : "127.0.0.1"}:${apiServerPort}`
448
- : undefined,
449
- apiServerPid: apiResult.success ? apiServerPid : undefined,
450
- };
451
-
452
- processRegistry.register(runningValidator);
453
-
454
- // Validator is now ready
455
- spinner.succeed("Validator started successfully!");
456
-
457
- console.log(chalk.green("āœ… Localnet is running!"));
458
- console.log(chalk.gray(`šŸ†” Validator ID: ${validatorId}`));
459
- console.log(
460
- chalk.cyan(`🌐 RPC URL: http://127.0.0.1:${config.localnet.port}`),
461
- );
462
- console.log(
463
- chalk.cyan(
464
- `šŸ’° Faucet URL: http://127.0.0.1:${config.localnet.faucetPort}`,
465
- ),
466
- );
467
- if (apiResult.success) {
468
- const displayHost = network ? "0.0.0.0" : "127.0.0.1";
469
- console.log(
470
- chalk.cyan(`šŸš€ API Server: http://${displayHost}:${apiServerPort}/api`),
471
- );
472
- if (network) {
473
- console.log(
474
- chalk.yellow(
475
- " 🌐 Network mode enabled - API server accessible from other devices",
476
- ),
477
- );
478
- }
479
- }
480
-
481
- // Airdrop SOL to mint authority if tokens were cloned
482
- if (clonedTokens.length > 0) {
483
- console.log(chalk.yellow("\nšŸ’ø Airdropping SOL to mint authority..."));
484
- const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
485
-
486
- try {
487
- await airdropSolToMintAuthority(clonedTokens[0], rpcUrl, debug);
488
- console.log(chalk.green("āœ… SOL airdropped successfully!"));
489
- } catch (error) {
490
- console.error(chalk.red("āŒ Failed to airdrop SOL:"));
491
- console.error(
492
- chalk.red(error instanceof Error ? error.message : String(error)),
493
- );
494
- console.log(
495
- chalk.yellow(
496
- "šŸ’” You may need to manually airdrop SOL for fee payments",
497
- ),
498
- );
499
- }
500
- }
501
-
502
- // Mint tokens if any were cloned
503
- if (clonedTokens.length > 0) {
504
- console.log(chalk.yellow("\nšŸ’° Minting tokens..."));
505
- const tokenCloner = new TokenCloner();
506
- const rpcUrl = `http://127.0.0.1:${config.localnet.port}`;
507
-
508
- if (debug) {
509
- console.log(
510
- chalk.gray(`šŸ› Minting ${clonedTokens.length} tokens to recipients:`),
511
- );
512
- clonedTokens.forEach((token, index) => {
513
- console.log(
514
- chalk.gray(
515
- ` ${index + 1}. ${token.config.symbol} (${
516
- token.config.mainnetMint
517
- }) - ${token.config.mintAmount} tokens`,
518
- ),
519
- );
520
- });
521
- console.log(chalk.gray(`🌐 Using RPC: ${rpcUrl}`));
522
- }
523
-
524
- try {
525
- await tokenCloner.mintTokensToRecipients(clonedTokens, rpcUrl, debug);
526
- console.log(chalk.green("āœ… Token minting completed!"));
527
-
528
- if (debug) {
529
- console.log(
530
- chalk.gray(
531
- "šŸ› All tokens have been minted to their respective recipients",
532
- ),
533
- );
534
- }
535
- } catch (error) {
536
- console.error(chalk.red("āŒ Failed to mint tokens:"));
537
- console.error(
538
- chalk.red(error instanceof Error ? error.message : String(error)),
539
- );
540
- console.log(
541
- chalk.yellow("šŸ’” Validator is still running, you can mint manually"),
542
- );
543
- }
544
- }
545
-
546
- if (config.tokens.length > 0) {
547
- console.log(chalk.yellow("\nšŸŖ™ Cloned tokens:"));
548
- config.tokens.forEach((token) => {
549
- console.log(
550
- chalk.gray(
551
- ` - ${token.symbol}: ${token.mainnetMint} (${token.mintAmount} minted)`,
552
- ),
553
- );
554
- });
555
- }
556
-
557
- if (config.programs.length > 0) {
558
- console.log(chalk.yellow("\nšŸ“¦ Cloned programs:"));
559
- config.programs.forEach((program) => {
560
- const name =
561
- program.name || `${program.mainnetProgramId.slice(0, 8)}...`;
562
- console.log(chalk.gray(` - ${name}: ${program.mainnetProgramId}`));
563
- });
564
- }
565
-
566
- console.log(chalk.blue("\nšŸ’” Tips:"));
567
- console.log(
568
- chalk.gray(" - Run `solforge list` to see all running validators"),
569
- );
570
- console.log(
571
- chalk.gray(" - Run `solforge status` to check validator status"),
572
- );
573
- console.log(
574
- chalk.gray(
575
- ` - Run \`solforge stop ${validatorId}\` to stop this validator`,
576
- ),
577
- );
578
- console.log(
579
- chalk.gray(" - Run `solforge stop --all` to stop all validators"),
580
- );
581
- if (apiResult.success) {
582
- const endpointHost = network ? "0.0.0.0" : "127.0.0.1";
583
- console.log(chalk.blue("\nšŸ”Œ API Endpoints:"));
584
- console.log(
585
- chalk.gray(
586
- ` - GET http://${endpointHost}:${apiServerPort}/api/tokens - List cloned tokens`,
587
- ),
588
- );
589
- console.log(
590
- chalk.gray(
591
- ` - GET http://${endpointHost}:${apiServerPort}/api/programs - List cloned programs`,
592
- ),
593
- );
594
- console.log(
595
- chalk.gray(
596
- ` - POST http://${endpointHost}:${apiServerPort}/api/tokens/{mintAddress}/mint - Mint tokens`,
597
- ),
598
- );
599
- console.log(
600
- chalk.gray(
601
- ` - POST http://${endpointHost}:${apiServerPort}/api/airdrop - Airdrop SOL`,
602
- ),
603
- );
604
- console.log(
605
- chalk.gray(
606
- ` - GET http://${endpointHost}:${apiServerPort}/api/wallet/{address}/balances - Get balances`,
607
- ),
608
- );
609
- }
610
- } catch (error) {
611
- spinner.fail("Failed to start validator");
612
- console.error(chalk.red("āŒ Unexpected error:"));
613
- console.error(
614
- chalk.red(error instanceof Error ? error.message : String(error)),
615
- );
616
- process.exit(1);
617
- }
618
- }
619
-
620
- function buildValidatorArgs(
621
- config: Config,
622
- clonedTokens: ClonedToken[] = [],
623
- ): string[] {
624
- const args: string[] = [];
625
-
626
- // Basic configuration
627
- args.push("--rpc-port", config.localnet.port.toString());
628
- args.push("--faucet-port", config.localnet.faucetPort.toString());
629
- args.push("--bind-address", config.localnet.bindAddress);
630
-
631
- if (config.localnet.reset) {
632
- args.push("--reset");
633
- }
634
-
635
- if (config.localnet.quiet) {
636
- args.push("--quiet");
637
- } else {
638
- // Always use quiet mode to prevent log spam in background
639
- args.push("--quiet");
640
- }
641
-
642
- // Add ledger size limit
643
- args.push("--limit-ledger-size", config.localnet.limitLedgerSize.toString());
644
-
645
- // Add cloned token accounts (using modified JSON files)
646
- if (clonedTokens.length > 0) {
647
- const tokenCloner = new TokenCloner();
648
- const tokenArgs = tokenCloner.getValidatorArgs(clonedTokens);
649
- args.push(...tokenArgs);
650
- }
651
-
652
- // Clone programs from mainnet using built-in validator flags
653
- for (const program of config.programs) {
654
- if (program.upgradeable) {
655
- args.push("--clone-upgradeable-program", program.mainnetProgramId);
656
- } else {
657
- // Use --clone for regular programs (non-upgradeable)
658
- args.push("--clone", program.mainnetProgramId);
659
- }
660
- }
661
-
662
- // If we're cloning programs, specify the source cluster
663
- if (config.programs.length > 0) {
664
- args.push("--url", config.localnet.rpc);
665
- }
666
-
667
- return args;
668
- }
669
-
670
- /**
671
- * Start the validator in the background
672
- */
673
- async function startValidatorInBackground(
674
- command: string,
675
- args: string[],
676
- debug: boolean,
677
- ): Promise<{ pid: number }> {
678
- return new Promise((resolve, reject) => {
679
- if (debug) {
680
- console.log(chalk.gray(`Starting ${command} in background...`));
681
- console.log(chalk.gray(`Command: ${command} ${args.join(" ")}`));
682
- }
683
-
684
- const child = spawn(command, args, {
685
- detached: true,
686
- stdio: "ignore", // Always ignore stdio to ensure it runs in background
687
- });
688
-
689
- child.on("error", (error) => {
690
- reject(new Error(`Failed to start validator: ${error.message}`));
691
- });
692
-
693
- // Give the validator a moment to start
694
- setTimeout(() => {
695
- if (child.pid) {
696
- child.unref(); // Allow parent to exit without waiting for child
697
- if (debug) {
698
- console.log(
699
- chalk.gray(`āœ… Validator started with PID: ${child.pid}`),
700
- );
701
- }
702
- resolve({ pid: child.pid });
703
- } else {
704
- reject(new Error("Validator failed to start"));
705
- }
706
- }, 1000);
707
- });
708
- }
709
-
710
- /**
711
- * Wait for the validator to be ready by polling the RPC endpoint
712
- */
713
- async function waitForValidatorReady(
714
- rpcUrl: string,
715
- debug: boolean,
716
- maxAttempts: number = 30,
717
- ): Promise<void> {
718
- let lastError: string = "";
719
-
720
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
721
- try {
722
- if (debug) {
723
- console.log(
724
- chalk.gray(`Attempt ${attempt}/${maxAttempts}: Checking ${rpcUrl}`),
725
- );
726
- }
727
-
728
- const result = await runCommand(
729
- "curl",
730
- [
731
- "-s",
732
- "-X",
733
- "POST",
734
- rpcUrl,
735
- "-H",
736
- "Content-Type: application/json",
737
- "-d",
738
- '{"jsonrpc":"2.0","id":1,"method":"getHealth"}',
739
- ],
740
- { silent: true, debug: false },
741
- );
742
-
743
- if (result.success && result.stdout.includes("ok")) {
744
- if (debug) {
745
- console.log(
746
- chalk.green(`āœ… Validator is ready after ${attempt} attempts`),
747
- );
748
- }
749
- return;
750
- }
751
-
752
- // Store the last error for better diagnostics
753
- if (!result.success) {
754
- lastError = result.stderr || "Unknown error";
755
- }
756
- } catch (error) {
757
- lastError = error instanceof Error ? error.message : String(error);
758
- }
759
-
760
- if (attempt < maxAttempts) {
761
- await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
762
- }
763
- }
764
-
765
- // Provide better error message
766
- let errorMsg = `Validator failed to become ready after ${maxAttempts} attempts`;
767
- if (lastError.includes("Connection refused")) {
768
- errorMsg += `\nšŸ’” This usually means:\n - Port ${
769
- rpcUrl.split(":")[2]
770
- } is already in use\n - Run 'pkill -f solana-test-validator' to kill existing validators`;
771
- } else if (lastError) {
772
- errorMsg += `\nLast error: ${lastError}`;
773
- }
774
-
775
- throw new Error(errorMsg);
776
- }
777
-
778
- /**
779
- * Airdrop SOL to the mint authority for fee payments
780
- */
781
-
782
- async function airdropSolToMintAuthority(
783
- clonedToken: ClonedToken,
784
- rpcUrl: string,
785
- debug: boolean = false,
786
- ): Promise<void> {
787
- if (debug) {
788
- console.log(
789
- chalk.gray(
790
- `Airdropping 10 SOL to ${clonedToken.mintAuthority.publicKey}`,
791
- ),
792
- );
793
- }
794
-
795
- const airdropResult = await runCommand(
796
- "solana",
797
- ["airdrop", "10", clonedToken.mintAuthority.publicKey, "--url", rpcUrl],
798
- { silent: !debug, debug },
799
- );
800
-
801
- if (!airdropResult.success) {
802
- throw new Error(
803
- `Failed to airdrop SOL: ${airdropResult.stderr || airdropResult.stdout}`,
804
- );
805
- }
806
-
807
- if (debug) {
808
- console.log(chalk.gray("SOL airdrop completed"));
809
- }
810
- }
811
-
812
- /**
813
- * Check for existing cloned tokens and return what's already cloned vs what needs to be cloned
814
- */
815
- async function checkExistingClonedTokens(
816
- tokens: TokenConfig[],
817
- _tokenCloner: TokenCloner,
818
- ): Promise<{ existingTokens: ClonedToken[]; tokensToClone: TokenConfig[] }> {
819
- const existingTokens: ClonedToken[] = [];
820
- const tokensToClone: TokenConfig[] = [];
821
- const workDir = ".solforge";
822
-
823
- // Check for shared mint authority
824
- const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
825
- let sharedMintAuthority: { publicKey: string; secretKey: number[] } | null =
826
- null;
827
-
828
- if (existsSync(sharedMintAuthorityPath)) {
829
- try {
830
- const fileContent = JSON.parse(
831
- readFileSync(sharedMintAuthorityPath, "utf8"),
832
- );
833
-
834
- if (Array.isArray(fileContent)) {
835
- // New format: file contains just the secret key array
836
- const { Keypair } = await import("@solana/web3.js");
837
- const keypair = Keypair.fromSecretKey(new Uint8Array(fileContent));
838
- sharedMintAuthority = {
839
- publicKey: keypair.publicKey.toBase58(),
840
- secretKey: Array.from(keypair.secretKey),
841
- };
842
-
843
- // Check metadata for consistency
844
- const metadataPath = join(workDir, "shared-mint-authority-meta.json");
845
- if (existsSync(metadataPath)) {
846
- const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
847
- if (metadata.publicKey !== sharedMintAuthority.publicKey) {
848
- sharedMintAuthority.publicKey = metadata.publicKey;
849
- }
850
- }
851
- } else {
852
- // Old format: file contains {publicKey, secretKey}
853
- sharedMintAuthority = fileContent;
854
- }
855
- } catch (_error) {
856
- // If we can't read the shared mint authority, treat all tokens as needing to be cloned
857
- sharedMintAuthority = null;
858
- }
859
- }
860
-
861
- for (const token of tokens) {
862
- const tokenDir = join(workDir, `token-${token.symbol.toLowerCase()}`);
863
- const modifiedAccountPath = join(tokenDir, "modified.json");
864
-
865
- // Check if this token has already been cloned
866
- if (existsSync(modifiedAccountPath) && sharedMintAuthority) {
867
- // Token appears to be already cloned
868
- existingTokens.push({
869
- config: token,
870
- mintAuthorityPath: sharedMintAuthorityPath,
871
- modifiedAccountPath,
872
- mintAuthority: sharedMintAuthority,
873
- });
874
- } else {
875
- // Token needs to be cloned
876
- tokensToClone.push(token);
877
- }
878
- }
879
-
880
- return { existingTokens, tokensToClone };
881
- }