solforge 0.2.5 → 0.2.7

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 (80) hide show
  1. package/package.json +1 -1
  2. package/scripts/postinstall.cjs +3 -3
  3. package/server/lib/base58.ts +1 -1
  4. package/server/lib/instruction-parser.ts +242 -0
  5. package/server/methods/account/get-account-info.ts +3 -7
  6. package/server/methods/account/get-balance.ts +3 -7
  7. package/server/methods/account/get-multiple-accounts.ts +2 -1
  8. package/server/methods/account/get-parsed-account-info.ts +3 -7
  9. package/server/methods/account/parsers/index.ts +2 -2
  10. package/server/methods/account/parsers/loader-upgradeable.ts +14 -1
  11. package/server/methods/account/parsers/spl-token.ts +29 -10
  12. package/server/methods/account/request-airdrop.ts +122 -86
  13. package/server/methods/admin/mint-to.ts +11 -38
  14. package/server/methods/block/get-block.ts +3 -7
  15. package/server/methods/block/get-blocks-with-limit.ts +3 -7
  16. package/server/methods/block/is-blockhash-valid.ts +3 -7
  17. package/server/methods/get-address-lookup-table.ts +3 -7
  18. package/server/methods/program/get-program-accounts.ts +9 -9
  19. package/server/methods/program/get-token-account-balance.ts +3 -7
  20. package/server/methods/program/get-token-accounts-by-delegate.ts +4 -3
  21. package/server/methods/program/get-token-accounts-by-owner.ts +54 -33
  22. package/server/methods/program/get-token-largest-accounts.ts +3 -2
  23. package/server/methods/program/get-token-supply.ts +3 -2
  24. package/server/methods/solforge/index.ts +9 -6
  25. package/server/methods/transaction/get-parsed-transaction.ts +3 -7
  26. package/server/methods/transaction/get-signature-statuses.ts +14 -7
  27. package/server/methods/transaction/get-signatures-for-address.ts +3 -7
  28. package/server/methods/transaction/get-transaction.ts +434 -287
  29. package/server/methods/transaction/inner-instructions.test.ts +63 -0
  30. package/server/methods/transaction/send-transaction.ts +248 -56
  31. package/server/methods/transaction/simulate-transaction.ts +3 -2
  32. package/server/rpc-server.ts +98 -61
  33. package/server/types.ts +65 -30
  34. package/server/ws-server.ts +11 -7
  35. package/src/api-server-entry.ts +5 -5
  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 +7 -7
  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 +33 -36
  46. package/src/cli/run-solforge.ts +3 -3
  47. package/src/cli/setup-wizard.ts +8 -6
  48. package/src/commands/add-program.ts +1 -1
  49. package/src/commands/init.ts +2 -2
  50. package/src/commands/mint.ts +5 -6
  51. package/src/commands/start.ts +10 -9
  52. package/src/commands/status.ts +1 -1
  53. package/src/commands/stop.ts +1 -1
  54. package/src/config/index.ts +33 -17
  55. package/src/config/manager.ts +3 -3
  56. package/src/db/index.ts +2 -2
  57. package/src/db/schema/index.ts +1 -0
  58. package/src/db/schema/transactions.ts +29 -22
  59. package/src/db/schema/tx-account-states.ts +21 -0
  60. package/src/db/tx-store.ts +113 -76
  61. package/src/gui/public/app.css +13 -13
  62. package/src/gui/server.ts +1 -1
  63. package/src/gui/src/api.ts +1 -1
  64. package/src/gui/src/app.tsx +49 -17
  65. package/src/gui/src/components/airdrop-mint-form.tsx +32 -8
  66. package/src/gui/src/components/clone-program-modal.tsx +25 -6
  67. package/src/gui/src/components/clone-token-modal.tsx +25 -6
  68. package/src/gui/src/components/modal.tsx +6 -1
  69. package/src/gui/src/components/status-panel.tsx +1 -1
  70. package/src/index.ts +19 -6
  71. package/src/migrations-bundled.ts +8 -2
  72. package/src/services/api-server.ts +41 -19
  73. package/src/services/port-manager.ts +7 -10
  74. package/src/services/process-registry.ts +4 -5
  75. package/src/services/program-cloner.ts +4 -4
  76. package/src/services/token-cloner.ts +4 -4
  77. package/src/services/validator.ts +2 -4
  78. package/src/types/config.ts +2 -2
  79. package/src/utils/shell.ts +1 -1
  80. package/src/utils/token-loader.ts +2 -2
@@ -1,4 +1,4 @@
1
- import { type ChangeEvent, useState } from "react";
1
+ import { type ChangeEvent, useId, useState } from "react";
2
2
  import { Modal } from "./modal";
3
3
 
4
4
  interface Props {
@@ -14,6 +14,9 @@ interface Props {
14
14
  }
15
15
 
16
16
  export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
17
+ const mintId = useId();
18
+ const endpointId = useId();
19
+ const holdersId = useId();
17
20
  const [mint, setMint] = useState("");
18
21
  const [endpoint, setEndpoint] = useState("");
19
22
  // Default OFF to avoid hitting public RPC rate limits by cloning holders.
@@ -42,8 +45,12 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
42
45
  setEndpoint("");
43
46
  setHolders("20");
44
47
  setAllAccounts(false);
45
- } catch (err: any) {
46
- setError(err?.message ?? String(err));
48
+ } catch (err: unknown) {
49
+ const message =
50
+ err && typeof err === "object" && "message" in err
51
+ ? String((err as { message?: unknown }).message)
52
+ : String(err);
53
+ setError(message);
47
54
  } finally {
48
55
  setPending(false);
49
56
  }
@@ -97,11 +104,15 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
97
104
  >
98
105
  <div className="space-y-5">
99
106
  <div className="space-y-2">
100
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
107
+ <label
108
+ htmlFor={mintId}
109
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
110
+ >
101
111
  Mint Address *
102
112
  </label>
103
113
  <div className="relative">
104
114
  <input
115
+ id={mintId}
105
116
  value={mint}
106
117
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
107
118
  setMint(event.target.value)
@@ -114,11 +125,15 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
114
125
  </div>
115
126
 
116
127
  <div className="space-y-2">
117
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
128
+ <label
129
+ htmlFor={endpointId}
130
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
131
+ >
118
132
  RPC Endpoint (Optional)
119
133
  </label>
120
134
  <div className="relative">
121
135
  <input
136
+ id={endpointId}
122
137
  value={endpoint}
123
138
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
124
139
  setEndpoint(event.target.value)
@@ -173,11 +188,15 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
173
188
 
174
189
  {!allAccounts && (
175
190
  <div className="space-y-2">
176
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
191
+ <label
192
+ htmlFor={holdersId}
193
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
194
+ >
177
195
  Top Holders Limit
178
196
  </label>
179
197
  <div className="relative">
180
198
  <input
199
+ id={holdersId}
181
200
  value={holders}
182
201
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
183
202
  setHolders(event.target.value)
@@ -40,9 +40,14 @@ export function Modal({
40
40
  return (
41
41
  <div className="fixed inset-0 z-50 flex items-center justify-center p-4 animate-fadeIn">
42
42
  {/* Backdrop */}
43
- <div
43
+ <button
44
+ type="button"
44
45
  className="absolute inset-0 bg-black/80 backdrop-blur-sm"
46
+ aria-label="Close modal"
45
47
  onClick={onClose}
48
+ onKeyDown={(e) => {
49
+ if (e.key === "Enter" || e.key === " ") onClose();
50
+ }}
46
51
  />
47
52
 
48
53
  {/* Modal */}
@@ -61,7 +61,7 @@ export function StatusPanel({ status, loading, onRefresh }: Props) {
61
61
  <StatusCard
62
62
  title="Faucet Balance"
63
63
  value={`${status.faucet.sol.toFixed(3)} SOL`}
64
- subtitle={status.faucet.address.slice(0, 10) + "…"}
64
+ subtitle={`${status.faucet.address.slice(0, 10)}…`}
65
65
  icon="fa-wallet"
66
66
  color="green"
67
67
  />
package/src/index.ts CHANGED
@@ -2,20 +2,33 @@
2
2
 
3
3
  // Suppress bigint-buffer warning
4
4
  const originalStderrWrite = process.stderr.write.bind(process.stderr);
5
- process.stderr.write = (chunk: any, encoding?: any, callback?: any) => {
5
+ process.stderr.write = ((
6
+ chunk: unknown,
7
+ encoding?: unknown,
8
+ callback?: unknown,
9
+ ) => {
6
10
  if (
7
11
  typeof chunk === "string" &&
8
12
  chunk.includes("bigint: Failed to load bindings")
9
13
  ) {
10
14
  return true; // Suppress this specific warning
11
15
  }
12
- return originalStderrWrite(chunk, encoding, callback);
13
- };
16
+ const writer = originalStderrWrite as unknown as (
17
+ chunk: string | Uint8Array,
18
+ encoding?: BufferEncoding,
19
+ callback?: ((err?: Error) => void) | undefined,
20
+ ) => boolean;
21
+ return writer(
22
+ chunk as string | Uint8Array,
23
+ encoding as BufferEncoding | undefined,
24
+ callback as ((err?: Error) => void) | undefined,
25
+ );
26
+ }) as typeof process.stderr.write;
14
27
 
15
28
  import chalk from "chalk";
16
29
  import { Command } from "commander";
17
- import { existsSync } from "fs";
18
- import { resolve } from "path";
30
+ import { existsSync } from "node:fs";
31
+ import { resolve } from "node:path";
19
32
  import packageJson from "../package.json" with { type: "json" };
20
33
  import { addProgramCommand } from "./commands/add-program.js";
21
34
  import { initCommand } from "./commands/init.js";
@@ -123,7 +136,7 @@ program
123
136
  const config = configManager.getConfig();
124
137
 
125
138
  const apiServer = new APIServer({
126
- port: parseInt(options.port),
139
+ port: parseInt(options.port, 10),
127
140
  host: options.host,
128
141
  validatorRpcUrl: options.rpcUrl,
129
142
  validatorFaucetUrl: options.faucetUrl,
@@ -10,8 +10,14 @@ import mig0000 from "../drizzle/0000_friendly_millenium_guard.sql" with {
10
10
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11
11
  // @ts-expect-error - Bun import attributes
12
12
  import mig0001 from "../drizzle/0001_stale_sentinels.sql" with { type: "file" };
13
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
14
+ // @ts-expect-error - Bun import attributes
15
+ import mig0002 from "../drizzle/0002_graceful_caretaker.sql" with {
16
+ type: "file",
17
+ };
13
18
 
14
19
  export const bundledMigrations: Array<{ name: string; path: string }> = [
15
- { name: "0000_friendly_millenium_guard.sql", path: mig0000 },
16
- { name: "0001_stale_sentinels.sql", path: mig0001 },
20
+ { name: "0000_friendly_millenium_guard.sql", path: mig0000 },
21
+ { name: "0001_stale_sentinels.sql", path: mig0001 },
22
+ { name: "0002_graceful_caretaker.sql", path: mig0002 },
17
23
  ];
@@ -1,12 +1,11 @@
1
1
  import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
2
- import { Connection, Keypair, PublicKey } from "@solana/web3.js";
2
+ import { Connection, PublicKey } from "@solana/web3.js";
3
3
  import chalk from "chalk";
4
- import { ChildProcess, spawn } from "child_process";
5
4
  import cors from "cors";
6
5
  import express from "express";
7
- import { existsSync, readFileSync } from "fs";
8
- import type { Server } from "http";
9
- import { join } from "path";
6
+ import { existsSync } from "node:fs";
7
+ import type { Server } from "node:http";
8
+ import { join } from "node:path";
10
9
  import { mintTokenToWallet as mintTokenToWalletShared } from "../commands/mint.js";
11
10
  import type { Config } from "../types/config.js";
12
11
  import { runCommand } from "../utils/shell.js";
@@ -31,8 +30,6 @@ export class APIServer {
31
30
  private app: express.Application;
32
31
  private server: Server | null = null;
33
32
  private config: APIServerConfig;
34
- private tokenCloner: TokenCloner;
35
- private programCloner: ProgramCloner;
36
33
  private connection: Connection;
37
34
 
38
35
  constructor(config: APIServerConfig) {
@@ -51,7 +48,7 @@ export class APIServer {
51
48
  this.app.use(express.json());
52
49
 
53
50
  // Request logging
54
- this.app.use((req, res, next) => {
51
+ this.app.use((req, _res, next) => {
55
52
  console.log(chalk.gray(`🌐 API: ${req.method} ${req.path}`));
56
53
  next();
57
54
  });
@@ -61,12 +58,12 @@ export class APIServer {
61
58
  const router = express.Router();
62
59
 
63
60
  // Health check
64
- router.get("/health", (req, res) => {
61
+ router.get("/health", (_req, res) => {
65
62
  res.json({ status: "ok", timestamp: new Date().toISOString() });
66
63
  });
67
64
 
68
65
  // Get validator info
69
- router.get("/validator/info", async (req, res) => {
66
+ router.get("/validator/info", async (_req, res) => {
70
67
  try {
71
68
  const version = await this.connection.getVersion();
72
69
  const blockHeight = await this.connection.getBlockHeight();
@@ -88,7 +85,7 @@ export class APIServer {
88
85
  });
89
86
 
90
87
  // Get all cloned tokens
91
- router.get("/tokens", async (req, res) => {
88
+ router.get("/tokens", async (_req, res) => {
92
89
  try {
93
90
  const clonedTokens = await this.getClonedTokens();
94
91
  res.json({
@@ -110,7 +107,7 @@ export class APIServer {
110
107
  });
111
108
 
112
109
  // Get all cloned programs
113
- router.get("/programs", async (req, res) => {
110
+ router.get("/programs", async (_req, res) => {
114
111
  try {
115
112
  const clonedPrograms = await this.getClonedPrograms();
116
113
  res.json({
@@ -233,7 +230,10 @@ export class APIServer {
233
230
  // Get recent transactions
234
231
  router.get("/transactions/recent", async (req, res) => {
235
232
  try {
236
- const limit = Math.min(parseInt(req.query.limit as string) || 10, 100);
233
+ const limit = Math.min(
234
+ parseInt(req.query.limit as string, 10) || 10,
235
+ 100,
236
+ );
237
237
  const signatures = await this.connection.getSignaturesForAddress(
238
238
  new PublicKey("11111111111111111111111111111111"), // System program
239
239
  { limit },
@@ -254,7 +254,7 @@ export class APIServer {
254
254
  this.app.use("/api", router);
255
255
 
256
256
  // 404 handler
257
- this.app.use("*", (req, res) => {
257
+ this.app.use("*", (_req, res) => {
258
258
  res.status(404).json({ error: "Endpoint not found" });
259
259
  });
260
260
  }
@@ -296,7 +296,13 @@ export class APIServer {
296
296
  mintAddress: string,
297
297
  walletAddress: string,
298
298
  amount: number,
299
- ): Promise<any> {
299
+ ): Promise<{
300
+ success: true;
301
+ symbol: string;
302
+ amount: number;
303
+ walletAddress: string;
304
+ mintAddress: string;
305
+ }> {
300
306
  const clonedTokens = await this.getClonedTokens();
301
307
  const token = findTokenByMint(clonedTokens, mintAddress);
302
308
 
@@ -321,7 +327,18 @@ export class APIServer {
321
327
  };
322
328
  }
323
329
 
324
- private async getWalletBalances(walletAddress: string): Promise<any> {
330
+ private async getWalletBalances(walletAddress: string): Promise<{
331
+ walletAddress: string;
332
+ solBalance: { lamports: number; sol: number };
333
+ tokenBalances: Array<{
334
+ mint: string;
335
+ symbol: string;
336
+ balance: string;
337
+ decimals: number;
338
+ uiAmount: number | null;
339
+ }>;
340
+ timestamp: string;
341
+ }> {
325
342
  try {
326
343
  const publicKey = new PublicKey(walletAddress);
327
344
 
@@ -371,7 +388,7 @@ export class APIServer {
371
388
  }
372
389
  }
373
390
  }
374
- } catch (error) {}
391
+ } catch (_error) {}
375
392
  }
376
393
 
377
394
  return {
@@ -395,7 +412,12 @@ export class APIServer {
395
412
  private async airdropSol(
396
413
  walletAddress: string,
397
414
  amount: number,
398
- ): Promise<any> {
415
+ ): Promise<{
416
+ success: true;
417
+ amount: number;
418
+ walletAddress: string;
419
+ signature: string;
420
+ }> {
399
421
  const result = await runCommand(
400
422
  "solana",
401
423
  [
@@ -477,6 +499,6 @@ export class APIServer {
477
499
  }
478
500
 
479
501
  isRunning(): boolean {
480
- return this.server !== null && this.server.listening;
502
+ return this.server?.listening;
481
503
  }
482
504
  }
@@ -7,7 +7,6 @@ export interface PortAllocation {
7
7
 
8
8
  export class PortManager {
9
9
  private readonly defaultRpcPort = 8899;
10
- private readonly defaultFaucetPort = 9900;
11
10
  private readonly portRangeStart = 8000;
12
11
  private readonly portRangeEnd = 9999;
13
12
 
@@ -147,19 +146,17 @@ export class PortManager {
147
146
  */
148
147
  private async checkPortActuallyFree(port: number): Promise<boolean> {
149
148
  return new Promise((resolve) => {
150
- const net = require("net");
149
+ const net = require("node:net");
151
150
  const server = net.createServer();
152
151
 
153
- server.listen(port, (err: any) => {
154
- if (err) {
155
- resolve(false);
156
- } else {
157
- server.once("close", () => resolve(true));
158
- server.close();
159
- }
152
+ server.once("listening", () => {
153
+ server.once("close", () => resolve(true));
154
+ server.close();
160
155
  });
161
156
 
162
- server.on("error", () => resolve(false));
157
+ server.once("error", () => resolve(false));
158
+
159
+ server.listen(port);
163
160
  });
164
161
  }
165
162
 
@@ -1,7 +1,6 @@
1
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
- import { homedir } from "os";
3
- import { join } from "path";
4
- import type { Config } from "../types/config.js";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
5
4
 
6
5
  export interface RunningValidator {
7
6
  id: string;
@@ -143,7 +142,7 @@ export class ProcessRegistry {
143
142
  // Ensure directory exists
144
143
  const dir = join(homedir(), ".solforge");
145
144
  if (!existsSync(dir)) {
146
- require("fs").mkdirSync(dir, { recursive: true });
145
+ require("node:fs").mkdirSync(dir, { recursive: true });
147
146
  }
148
147
 
149
148
  writeFileSync(this.registryPath, JSON.stringify(validators, null, 2));
@@ -1,7 +1,7 @@
1
1
  import { Connection, PublicKey } from "@solana/web3.js";
2
2
  import chalk from "chalk";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
- import { join } from "path";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
5
  import type { ProgramConfig } from "../types/config.js";
6
6
  import { runCommand } from "../utils/shell.js";
7
7
 
@@ -276,7 +276,7 @@ export class ProgramCloner {
276
276
  const programAccount = await connection.getAccountInfo(
277
277
  new PublicKey(programId),
278
278
  );
279
- return programAccount !== null && programAccount.executable;
279
+ return programAccount?.executable;
280
280
  } catch {
281
281
  return false;
282
282
  }
@@ -310,7 +310,7 @@ export class ProgramCloner {
310
310
  owner: programAccount.owner.toBase58(),
311
311
  size: programAccount.data.length,
312
312
  };
313
- } catch (error) {
313
+ } catch (_error) {
314
314
  return { exists: false };
315
315
  }
316
316
  }
@@ -1,7 +1,7 @@
1
1
  import { Keypair, PublicKey } from "@solana/web3.js";
2
2
  import chalk from "chalk";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
- import { join, resolve } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
5
  import type { TokenConfig } from "../types/config.js";
6
6
  import { runCommand } from "../utils/shell.js";
7
7
 
@@ -614,7 +614,7 @@ export class TokenCloner {
614
614
  );
615
615
  }
616
616
  }
617
- } catch (error) {
617
+ } catch (_error) {
618
618
  if (debug) {
619
619
  console.log(
620
620
  chalk.gray(` ℹ️ No existing accounts found or parsing error`),
@@ -749,7 +749,7 @@ export class TokenCloner {
749
749
  break;
750
750
  }
751
751
  }
752
- } catch (error) {
752
+ } catch (_error) {
753
753
  // No existing accounts found or parsing error, will create new account
754
754
  }
755
755
  }
@@ -1,6 +1,4 @@
1
- import { type ChildProcess, spawn } from "child_process";
2
- import { existsSync } from "fs";
3
- import { join } from "path";
1
+ import { type ChildProcess, spawn } from "node:child_process";
4
2
  import type {
5
3
  LocalnetConfig,
6
4
  OperationResult,
@@ -251,7 +249,7 @@ export class ValidatorService {
251
249
  if (response.ok) {
252
250
  return; // Validator is ready
253
251
  }
254
- } catch (error) {
252
+ } catch (_error) {
255
253
  // Continue waiting
256
254
  }
257
255
 
@@ -86,11 +86,11 @@ export interface ValidatorState {
86
86
  }
87
87
 
88
88
  // Operation result types
89
- export interface OperationResult<T = any> {
89
+ export interface OperationResult<T = unknown> {
90
90
  success: boolean;
91
91
  data?: T;
92
92
  error?: string;
93
- details?: any;
93
+ details?: unknown;
94
94
  }
95
95
 
96
96
  export interface CloneResult {
@@ -56,7 +56,7 @@ export async function runCommand(
56
56
  if (jsonOutput && success && stdout.trim()) {
57
57
  try {
58
58
  parsedOutput = JSON.parse(stdout);
59
- } catch (e) {
59
+ } catch (_e) {
60
60
  // If JSON parsing fails, keep original stdout
61
61
  console.warn(
62
62
  chalk.yellow("Warning: Expected JSON output but got invalid JSON"),
@@ -1,6 +1,6 @@
1
1
  import { Keypair } from "@solana/web3.js";
2
- import { existsSync, readFileSync } from "fs";
3
- import { join } from "path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
4
  import type { TokenConfig } from "../types/config.js";
5
5
 
6
6
  export interface ClonedToken {