solforge 0.2.5 → 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 (73) 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/methods/account/get-account-info.ts +3 -7
  5. package/server/methods/account/get-balance.ts +3 -7
  6. package/server/methods/account/get-multiple-accounts.ts +2 -1
  7. package/server/methods/account/get-parsed-account-info.ts +3 -7
  8. package/server/methods/account/parsers/index.ts +2 -2
  9. package/server/methods/account/parsers/loader-upgradeable.ts +14 -1
  10. package/server/methods/account/parsers/spl-token.ts +29 -10
  11. package/server/methods/account/request-airdrop.ts +44 -31
  12. package/server/methods/block/get-block.ts +3 -7
  13. package/server/methods/block/get-blocks-with-limit.ts +3 -7
  14. package/server/methods/block/is-blockhash-valid.ts +3 -7
  15. package/server/methods/get-address-lookup-table.ts +3 -7
  16. package/server/methods/program/get-program-accounts.ts +9 -9
  17. package/server/methods/program/get-token-account-balance.ts +3 -7
  18. package/server/methods/program/get-token-accounts-by-delegate.ts +4 -3
  19. package/server/methods/program/get-token-accounts-by-owner.ts +54 -33
  20. package/server/methods/program/get-token-largest-accounts.ts +3 -2
  21. package/server/methods/program/get-token-supply.ts +3 -2
  22. package/server/methods/solforge/index.ts +9 -6
  23. package/server/methods/transaction/get-parsed-transaction.ts +3 -7
  24. package/server/methods/transaction/get-signature-statuses.ts +14 -7
  25. package/server/methods/transaction/get-signatures-for-address.ts +3 -7
  26. package/server/methods/transaction/get-transaction.ts +167 -81
  27. package/server/methods/transaction/send-transaction.ts +29 -16
  28. package/server/methods/transaction/simulate-transaction.ts +3 -2
  29. package/server/rpc-server.ts +47 -34
  30. package/server/types.ts +9 -6
  31. package/server/ws-server.ts +11 -7
  32. package/src/api-server-entry.ts +5 -5
  33. package/src/cli/commands/airdrop.ts +2 -2
  34. package/src/cli/commands/config.ts +2 -2
  35. package/src/cli/commands/mint.ts +3 -3
  36. package/src/cli/commands/program-clone.ts +9 -11
  37. package/src/cli/commands/program-load.ts +3 -3
  38. package/src/cli/commands/rpc-start.ts +7 -7
  39. package/src/cli/commands/token-adopt-authority.ts +1 -1
  40. package/src/cli/commands/token-clone.ts +5 -6
  41. package/src/cli/commands/token-create.ts +5 -5
  42. package/src/cli/main.ts +33 -36
  43. package/src/cli/run-solforge.ts +3 -3
  44. package/src/cli/setup-wizard.ts +8 -6
  45. package/src/commands/add-program.ts +1 -1
  46. package/src/commands/init.ts +2 -2
  47. package/src/commands/mint.ts +5 -6
  48. package/src/commands/start.ts +10 -9
  49. package/src/commands/status.ts +1 -1
  50. package/src/commands/stop.ts +1 -1
  51. package/src/config/index.ts +33 -17
  52. package/src/config/manager.ts +3 -3
  53. package/src/db/index.ts +2 -2
  54. package/src/db/tx-store.ts +12 -8
  55. package/src/gui/public/app.css +13 -13
  56. package/src/gui/server.ts +1 -1
  57. package/src/gui/src/api.ts +1 -1
  58. package/src/gui/src/app.tsx +49 -17
  59. package/src/gui/src/components/airdrop-mint-form.tsx +32 -8
  60. package/src/gui/src/components/clone-program-modal.tsx +25 -6
  61. package/src/gui/src/components/clone-token-modal.tsx +25 -6
  62. package/src/gui/src/components/modal.tsx +6 -1
  63. package/src/gui/src/components/status-panel.tsx +1 -1
  64. package/src/index.ts +19 -6
  65. package/src/services/api-server.ts +41 -19
  66. package/src/services/port-manager.ts +7 -10
  67. package/src/services/process-registry.ts +4 -5
  68. package/src/services/program-cloner.ts +4 -4
  69. package/src/services/token-cloner.ts +4 -4
  70. package/src/services/validator.ts +2 -4
  71. package/src/types/config.ts +2 -2
  72. package/src/utils/shell.ts +1 -1
  73. package/src/utils/token-loader.ts +2 -2
@@ -170,9 +170,10 @@ async function resolveTokens(selections: string[], existing: string[] = []) {
170
170
  const set = new Set(existing);
171
171
  for (const selection of selections) {
172
172
  if (selection === "__custom__") {
173
- (await collectCustomEntries("token mint address")).forEach((value) =>
174
- set.add(value),
175
- );
173
+ {
174
+ const values = await collectCustomEntries("token mint address");
175
+ for (const value of values) set.add(value);
176
+ }
176
177
  continue;
177
178
  }
178
179
  const preset = TOKEN_PRESETS.find((token) => token.value === selection);
@@ -193,9 +194,10 @@ async function resolvePrograms(selections: string[], existing: string[] = []) {
193
194
  const set = new Set(existing);
194
195
  for (const selection of selections) {
195
196
  if (selection === "__custom__") {
196
- (await collectCustomEntries("program id")).forEach((value) =>
197
- set.add(value),
198
- );
197
+ {
198
+ const values = await collectCustomEntries("program id");
199
+ for (const value of values) set.add(value);
200
+ }
199
201
  continue;
200
202
  }
201
203
  const preset = PROGRAM_PRESETS.find(
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { existsSync } from "fs";
2
+ import { existsSync } from "node:fs";
3
3
  import inquirer from "inquirer";
4
4
  import { configManager } from "../config/manager.js";
5
5
  import { processRegistry } from "../services/process-registry.js";
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
- import { existsSync, writeFileSync } from "fs";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
3
  import inquirer from "inquirer";
4
- import { resolve } from "path";
4
+ import { resolve } from "node:path";
5
5
  import type { Config } from "../types/config.js";
6
6
 
7
7
  const defaultConfig: Config = {
@@ -1,9 +1,8 @@
1
1
  import { input, select } from "@inquirer/prompts";
2
- import { Keypair, PublicKey } from "@solana/web3.js";
2
+ import { PublicKey } from "@solana/web3.js";
3
3
  import chalk from "chalk";
4
4
  import { Command } from "commander";
5
- import { existsSync, readFileSync } from "fs";
6
- import { join } from "path";
5
+ import { existsSync, readFileSync } from "node:fs";
7
6
  import type { TokenConfig } from "../types/config.js";
8
7
  import { runCommand } from "../utils/shell";
9
8
  import {
@@ -115,7 +114,7 @@ export const mintCommand = new Command()
115
114
  let amount: string;
116
115
  if (options.amount) {
117
116
  const num = parseFloat(options.amount);
118
- if (isNaN(num) || num <= 0) {
117
+ if (Number.isNaN(num) || num <= 0) {
119
118
  console.error(chalk.red("❌ Invalid amount"));
120
119
  process.exit(1);
121
120
  }
@@ -126,7 +125,7 @@ export const mintCommand = new Command()
126
125
  message: "Enter amount to mint:",
127
126
  validate: (value: string) => {
128
127
  const num = parseFloat(value);
129
- if (isNaN(num) || num <= 0) {
128
+ if (Number.isNaN(num) || num <= 0) {
130
129
  return "Please enter a valid positive number";
131
130
  }
132
131
  return true;
@@ -219,7 +218,7 @@ export async function mintTokenToWallet(
219
218
  break;
220
219
  }
221
220
  }
222
- } catch (error) {
221
+ } catch (_error) {
223
222
  // No existing accounts found or parsing error, will create new account
224
223
  }
225
224
  }
@@ -1,8 +1,8 @@
1
1
  import chalk from "chalk";
2
- import { spawn } from "child_process";
3
- import { existsSync, readFileSync } from "fs";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import ora from "ora";
5
- import { join } from "path";
5
+ import { join } from "node:path";
6
6
  import { configManager } from "../config/manager.js";
7
7
  import { portManager } from "../services/port-manager.js";
8
8
  import type { RunningValidator } from "../services/process-registry.js";
@@ -392,7 +392,7 @@ export async function startCommand(
392
392
  if (pidResult.success && pidResult.stdout.trim()) {
393
393
  const pidLine = pidResult.stdout.trim().split("\n")[0];
394
394
  if (pidLine) {
395
- apiServerPid = parseInt(pidLine);
395
+ apiServerPid = parseInt(pidLine, 10);
396
396
  }
397
397
  }
398
398
  } else {
@@ -434,7 +434,7 @@ export async function startCommand(
434
434
  const runningValidator: RunningValidator = {
435
435
  id: validatorId,
436
436
  name: config.name,
437
- pid: validatorProcess.pid!,
437
+ pid: validatorProcess.pid,
438
438
  rpcPort: config.localnet.port,
439
439
  faucetPort: config.localnet.faucetPort,
440
440
  rpcUrl: `http://127.0.0.1:${config.localnet.port}`,
@@ -558,7 +558,7 @@ export async function startCommand(
558
558
  console.log(chalk.yellow("\n📦 Cloned programs:"));
559
559
  config.programs.forEach((program) => {
560
560
  const name =
561
- program.name || program.mainnetProgramId.slice(0, 8) + "...";
561
+ program.name || `${program.mainnetProgramId.slice(0, 8)}...`;
562
562
  console.log(chalk.gray(` - ${name}: ${program.mainnetProgramId}`));
563
563
  });
564
564
  }
@@ -778,8 +778,9 @@ async function waitForValidatorReady(
778
778
  /**
779
779
  * Airdrop SOL to the mint authority for fee payments
780
780
  */
781
+
781
782
  async function airdropSolToMintAuthority(
782
- clonedToken: any,
783
+ clonedToken: ClonedToken,
783
784
  rpcUrl: string,
784
785
  debug: boolean = false,
785
786
  ): Promise<void> {
@@ -813,7 +814,7 @@ async function airdropSolToMintAuthority(
813
814
  */
814
815
  async function checkExistingClonedTokens(
815
816
  tokens: TokenConfig[],
816
- tokenCloner: TokenCloner,
817
+ _tokenCloner: TokenCloner,
817
818
  ): Promise<{ existingTokens: ClonedToken[]; tokensToClone: TokenConfig[] }> {
818
819
  const existingTokens: ClonedToken[] = [];
819
820
  const tokensToClone: TokenConfig[] = [];
@@ -851,7 +852,7 @@ async function checkExistingClonedTokens(
851
852
  // Old format: file contains {publicKey, secretKey}
852
853
  sharedMintAuthority = fileContent;
853
854
  }
854
- } catch (error) {
855
+ } catch (_error) {
855
856
  // If we can't read the shared mint authority, treat all tokens as needing to be cloned
856
857
  sharedMintAuthority = null;
857
858
  }
@@ -90,7 +90,7 @@ export async function statusCommand(): Promise<void> {
90
90
  console.log(` 📝 Project: ${config.name}`);
91
91
  console.log(` 🪙 Tokens: ${config.tokens.length}`);
92
92
  console.log(` 📦 Programs: ${config.programs.length}`);
93
- } catch (error) {
93
+ } catch (_error) {
94
94
  console.log(` ❌ No valid configuration found`);
95
95
  console.log(` 💡 Run 'solforge init' to create one`);
96
96
  }
@@ -186,7 +186,7 @@ async function waitForProcessShutdown(
186
186
  process.kill(pid, 0);
187
187
  // If no error thrown, process is still running
188
188
  await new Promise((resolve) => setTimeout(resolve, 500));
189
- } catch (error) {
189
+ } catch (_error) {
190
190
  // Process is gone
191
191
  return { success: true };
192
192
  }
@@ -68,7 +68,7 @@ export async function writeDefaultConfig(opts: { force?: boolean } = {}) {
68
68
  mkdirSync(dir, { recursive: true });
69
69
  } catch {}
70
70
  }
71
- writeFileSync(p, JSON.stringify(defaultConfig, null, 2) + "\n");
71
+ writeFileSync(p, `${JSON.stringify(defaultConfig, null, 2)}\n`);
72
72
  }
73
73
 
74
74
  export async function writeConfig(
@@ -81,50 +81,66 @@ export async function writeConfig(
81
81
  mkdirSync(dir, { recursive: true });
82
82
  } catch {}
83
83
  }
84
- await Bun.write(path, JSON.stringify(config, null, 2) + "\n");
84
+ await Bun.write(path, `${JSON.stringify(config, null, 2)}\n`);
85
85
  }
86
86
 
87
- export function getConfigValue(cfg: any, path?: string) {
87
+ export function getConfigValue(
88
+ cfg: Record<string, unknown>,
89
+ path?: string,
90
+ ): unknown {
88
91
  if (!path) return cfg;
89
- return path.split(".").reduce((o, k) => (o ? o[k] : undefined), cfg);
92
+ let cur: unknown = cfg;
93
+ for (const k of path.split(".")) {
94
+ if (
95
+ cur &&
96
+ typeof cur === "object" &&
97
+ k in (cur as Record<string, unknown>)
98
+ ) {
99
+ cur = (cur as Record<string, unknown>)[k];
100
+ } else {
101
+ return undefined;
102
+ }
103
+ }
104
+ return cur;
90
105
  }
91
106
 
92
- export function setConfigValue<T extends Record<string, any>>(
107
+ export function setConfigValue<T extends Record<string, unknown>>(
93
108
  cfg: T,
94
109
  path: string,
95
- value: any,
110
+ value: unknown,
96
111
  ): T {
97
112
  const parts = path.split(".");
98
- let node: any = cfg;
113
+ let node: Record<string, unknown> = cfg;
99
114
  for (let i = 0; i < parts.length - 1; i++) {
100
115
  const k = parts[i];
101
116
  if (!node[k] || typeof node[k] !== "object") node[k] = {};
102
- node = node[k];
117
+ node = node[k] as Record<string, unknown>;
103
118
  }
104
119
  node[parts[parts.length - 1]] = coerceValue(value);
105
120
  return cfg;
106
121
  }
107
122
 
108
- function coerceValue(v: any) {
123
+ function coerceValue(v: unknown): unknown {
109
124
  if (v === "true") return true;
110
125
  if (v === "false") return false;
111
- if (v !== "" && !isNaN(Number(v))) return Number(v);
126
+ if (typeof v === "string" && v !== "" && !Number.isNaN(Number(v)))
127
+ return Number(v);
112
128
  try {
113
- return JSON.parse(v);
129
+ return typeof v === "string" ? JSON.parse(v) : v;
114
130
  } catch {
115
131
  return v;
116
132
  }
117
133
  }
118
134
 
119
135
  function deepMerge<T>(a: T, b: Partial<T>): T {
120
- if (Array.isArray(a) || Array.isArray(b)) return (b as any) ?? (a as any);
136
+ if (Array.isArray(a) || Array.isArray(b)) return (b ?? a) as unknown as T;
121
137
  if (typeof a === "object" && typeof b === "object" && a && b) {
122
- const out: any = { ...a };
138
+ const out: Record<string, unknown> = { ...(a as Record<string, unknown>) };
123
139
  for (const [k, v] of Object.entries(b)) {
124
- const ak = (a as any)[k];
125
- out[k] = deepMerge(ak, v as any);
140
+ const ak = (a as Record<string, unknown>)[k];
141
+ out[k] = deepMerge(ak as unknown, v as unknown) as unknown;
126
142
  }
127
- return out;
143
+ return out as unknown as T;
128
144
  }
129
- return (b as any) ?? (a as any);
145
+ return (b ?? a) as unknown as T;
130
146
  }
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
- import { join, resolve } from "path";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
3
  import type { Config, ValidationResult } from "../types/config.js";
4
4
  import { ConfigSchema } from "../types/config.js";
5
5
 
@@ -77,7 +77,7 @@ export class ConfigManager {
77
77
  /**
78
78
  * Validate a configuration object
79
79
  */
80
- validate(config: any): ValidationResult {
80
+ validate(config: unknown): ValidationResult {
81
81
  const result = ConfigSchema.safeParse(config);
82
82
 
83
83
  if (result.success) {
package/src/db/index.ts CHANGED
@@ -27,10 +27,10 @@ if (!PERSIST && DB_PATH !== ":memory:") {
27
27
  if (existsSync(DB_PATH)) unlinkSync(DB_PATH);
28
28
  } catch {}
29
29
  try {
30
- if (existsSync(DB_PATH + "-wal")) unlinkSync(DB_PATH + "-wal");
30
+ if (existsSync(`${DB_PATH}-wal`)) unlinkSync(`${DB_PATH}-wal`);
31
31
  } catch {}
32
32
  try {
33
- if (existsSync(DB_PATH + "-shm")) unlinkSync(DB_PATH + "-shm");
33
+ if (existsSync(`${DB_PATH}-shm`)) unlinkSync(`${DB_PATH}-shm`);
34
34
  } catch {}
35
35
  }
36
36
 
@@ -23,8 +23,8 @@ export type InsertTxBundle = {
23
23
  writable: boolean;
24
24
  programIdIndex?: number;
25
25
  }>;
26
- preTokenBalances?: any[];
27
- postTokenBalances?: any[];
26
+ preTokenBalances?: unknown[];
27
+ postTokenBalances?: unknown[];
28
28
  };
29
29
 
30
30
  export type AccountSnapshot = {
@@ -133,7 +133,7 @@ export class TxStore {
133
133
 
134
134
  async getStatuses(signatures: string[]) {
135
135
  if (!Array.isArray(signatures) || signatures.length === 0)
136
- return new Map<string, { slot: number; err: any | null }>();
136
+ return new Map<string, { slot: number; err: unknown | null }>();
137
137
  const results = await db
138
138
  .select({
139
139
  signature: transactions.signature,
@@ -142,7 +142,7 @@ export class TxStore {
142
142
  })
143
143
  .from(transactions)
144
144
  .where(inArraySafe(transactions.signature, signatures));
145
- const map = new Map<string, { slot: number; err: any | null }>();
145
+ const map = new Map<string, { slot: number; err: unknown | null }>();
146
146
  for (const r of results)
147
147
  map.set(r.signature, {
148
148
  slot: Number(r.slot),
@@ -167,7 +167,7 @@ export class TxStore {
167
167
  }
168
168
  const limit = Math.min(Math.max(opts.limit ?? 1000, 1), 1000);
169
169
 
170
- const whereClauses = [eq(addressSignatures.address, address)] as any[];
170
+ const whereClauses = [eq(addressSignatures.address, address)] as unknown[];
171
171
  if (typeof beforeSlot === "number")
172
172
  whereClauses.push(lt(addressSignatures.slot, beforeSlot));
173
173
  if (typeof untilSlot === "number")
@@ -216,7 +216,7 @@ export class TxStore {
216
216
  }
217
217
  }
218
218
 
219
- function safeParse<T = any>(s: string): T | null {
219
+ function safeParse<T = unknown>(s: string): T | null {
220
220
  try {
221
221
  return JSON.parse(s) as T;
222
222
  } catch {
@@ -224,6 +224,10 @@ function safeParse<T = any>(s: string): T | null {
224
224
  }
225
225
  }
226
226
 
227
- function inArraySafe<T>(col: any, arr: T[]) {
228
- return arr.length > 0 ? inArray(col, arr as any) : eq(col, "__never__");
227
+ function inArraySafe<T>(col: unknown, arr: T[]) {
228
+ return arr.length > 0
229
+ ? // biome-ignore lint/suspicious/noExplicitAny: Drizzle generic typing workaround
230
+ (inArray as unknown as (c: unknown, a: T[]) => any)(col, arr)
231
+ : // biome-ignore lint/suspicious/noExplicitAny: Force an always-false predicate without over-constraining types
232
+ eq(col as any, "__never__");
229
233
  }
@@ -442,11 +442,11 @@ video {
442
442
  line-height: 1.25rem;
443
443
  }
444
444
  .\!input {
445
- background: var(--color-bg-surface) !important;
446
- border: 1px solid var(--color-border-subtle) !important;
447
- color: var(--color-text-primary) !important;
448
- transition: var(--transition-base) !important;
449
- font-family: Inter, sans-serif !important;
445
+ background: var(--color-bg-surface);
446
+ border: 1px solid var(--color-border-subtle);
447
+ color: var(--color-text-primary);
448
+ transition: var(--transition-base);
449
+ font-family: Inter, sans-serif;
450
450
  }
451
451
  .input {
452
452
  background: var(--color-bg-surface);
@@ -456,18 +456,18 @@ video {
456
456
  font-family: Inter, sans-serif;
457
457
  }
458
458
  .\!input:hover {
459
- background: var(--color-bg-elevated) !important;
460
- border-color: var(--color-border-default) !important;
459
+ background: var(--color-bg-elevated);
460
+ border-color: var(--color-border-default);
461
461
  }
462
462
  .input:hover {
463
463
  background: var(--color-bg-elevated);
464
464
  border-color: var(--color-border-default);
465
465
  }
466
466
  .\!input:focus {
467
- outline: none !important;
468
- border-color: var(--color-accent-primary) !important;
469
- box-shadow: 0 0 0 3px var(--color-accent-glow) !important;
470
- background: var(--color-bg-elevated) !important;
467
+ outline: none;
468
+ border-color: var(--color-accent-primary);
469
+ box-shadow: 0 0 0 3px var(--color-accent-glow);
470
+ background: var(--color-bg-elevated);
471
471
  }
472
472
  .input:focus {
473
473
  outline: none;
@@ -476,10 +476,10 @@ video {
476
476
  background: var(--color-bg-elevated);
477
477
  }
478
478
  .\!input::-moz-placeholder {
479
- color: var(--color-text-muted) !important;
479
+ color: var(--color-text-muted);
480
480
  }
481
481
  .\!input::placeholder {
482
- color: var(--color-text-muted) !important;
482
+ color: var(--color-text-muted);
483
483
  }
484
484
  .input::-moz-placeholder {
485
485
  color: var(--color-text-muted);
package/src/gui/server.ts CHANGED
@@ -107,7 +107,7 @@ export function startGuiServer(opts: GuiStartOptions = {}) {
107
107
  const rpcServer = opts.rpcServer;
108
108
  const rpcUrl = `http://${host}:${rpcPort}`;
109
109
 
110
- const callRpc = async (method: string, params: any[] = []) => {
110
+ const callRpc = async (method: string, params: unknown[] = []) => {
111
111
  if (!rpcServer) throw new HttpError(503, "RPC server not available");
112
112
  const response: JsonRpcResponse = await rpcServer.handleRequest({
113
113
  jsonrpc: "2.0",
@@ -72,7 +72,7 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
72
72
  if (!headers.has("content-type") && init.body)
73
73
  headers.set("content-type", "application/json");
74
74
  const response = await fetch(path, { ...init, headers });
75
- let payload: any = null;
75
+ let payload: unknown = null;
76
76
  const text = await response.text();
77
77
  if (text) {
78
78
  try {
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
1
+ import { useCallback, useEffect, useId, useState } from "react";
2
2
  import {
3
3
  type ApiConfig,
4
4
  type ApiStatus,
@@ -40,8 +40,9 @@ export function App() {
40
40
  const cfg = await fetchConfig();
41
41
  setConfig(cfg);
42
42
  setBannerError(null);
43
- } catch (error: any) {
44
- setBannerError(error?.message ?? String(error));
43
+ } catch (error) {
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ setBannerError(message);
45
46
  }
46
47
  }, []);
47
48
 
@@ -50,8 +51,9 @@ export function App() {
50
51
  try {
51
52
  const data = await fetchStatus();
52
53
  setStatus(data);
53
- } catch (error: any) {
54
- setBannerError(error?.message ?? String(error));
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ setBannerError(message);
55
57
  } finally {
56
58
  setLoadingStatus(false);
57
59
  }
@@ -62,8 +64,9 @@ export function App() {
62
64
  try {
63
65
  const data = await fetchPrograms();
64
66
  setPrograms(data);
65
- } catch (error: any) {
66
- setBannerError(error?.message ?? String(error));
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ setBannerError(message);
67
70
  } finally {
68
71
  setLoadingPrograms(false);
69
72
  }
@@ -74,8 +77,9 @@ export function App() {
74
77
  try {
75
78
  const data = await fetchTokens();
76
79
  setTokens(data);
77
- } catch (error: any) {
78
- setBannerError(error?.message ?? String(error));
80
+ } catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ setBannerError(message);
79
83
  } finally {
80
84
  setLoadingTokens(false);
81
85
  }
@@ -141,9 +145,20 @@ export function App() {
141
145
  [loadTokens],
142
146
  );
143
147
 
144
- const scrollToSection = (sectionId: string) => {
148
+ type SectionKey = "status" | "actions" | "programs" | "tokens";
149
+ const uid = useId();
150
+ const sectionIds: Record<SectionKey, string> = {
151
+ status: `${uid}-status`,
152
+ actions: `${uid}-actions`,
153
+ programs: `${uid}-programs`,
154
+ tokens: `${uid}-tokens`,
155
+ };
156
+
157
+ const scrollToSection = (sectionId: SectionKey) => {
145
158
  setActiveSection(sectionId);
146
- document.getElementById(sectionId)?.scrollIntoView({ behavior: "smooth" });
159
+ document
160
+ .getElementById(sectionIds[sectionId])
161
+ ?.scrollIntoView({ behavior: "smooth" });
147
162
  setSidebarOpen(false);
148
163
  };
149
164
 
@@ -151,6 +166,7 @@ export function App() {
151
166
  <div className="min-h-screen relative">
152
167
  {/* Mobile Menu Button */}
153
168
  <button
169
+ type="button"
154
170
  onClick={() => setSidebarOpen(!sidebarOpen)}
155
171
  className="lg:hidden fixed top-4 left-4 z-50 btn-icon bg-gradient-to-br from-purple-600 to-violet-600 border-purple-500/30"
156
172
  aria-label="Menu"
@@ -183,6 +199,7 @@ export function App() {
183
199
  {/* Navigation Items */}
184
200
  <nav className="space-y-2">
185
201
  <button
202
+ type="button"
186
203
  onClick={() => scrollToSection("status")}
187
204
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
188
205
  activeSection === "status"
@@ -194,6 +211,7 @@ export function App() {
194
211
  <span className="font-medium">Network Status</span>
195
212
  </button>
196
213
  <button
214
+ type="button"
197
215
  onClick={() => scrollToSection("actions")}
198
216
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
199
217
  activeSection === "actions"
@@ -205,6 +223,7 @@ export function App() {
205
223
  <span className="font-medium">Quick Actions</span>
206
224
  </button>
207
225
  <button
226
+ type="button"
208
227
  onClick={() => scrollToSection("programs")}
209
228
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
210
229
  activeSection === "programs"
@@ -216,6 +235,7 @@ export function App() {
216
235
  <span className="font-medium">Programs</span>
217
236
  </button>
218
237
  <button
238
+ type="button"
219
239
  onClick={() => scrollToSection("tokens")}
220
240
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
221
241
  activeSection === "tokens"
@@ -250,9 +270,16 @@ export function App() {
250
270
 
251
271
  {/* Overlay for mobile */}
252
272
  {sidebarOpen && (
253
- <div
273
+ <button
274
+ type="button"
254
275
  className="fixed inset-0 bg-black/50 backdrop-blur-sm z-30 lg:hidden"
276
+ aria-label="Close sidebar overlay"
255
277
  onClick={() => setSidebarOpen(false)}
278
+ onKeyDown={(e) => {
279
+ if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
280
+ setSidebarOpen(false);
281
+ }
282
+ }}
256
283
  />
257
284
  )}
258
285
 
@@ -270,7 +297,11 @@ export function App() {
270
297
  Manage your local Solana development environment
271
298
  </p>
272
299
  </div>
273
- <button onClick={loadStatus} className="btn-secondary">
300
+ <button
301
+ type="button"
302
+ onClick={loadStatus}
303
+ className="btn-secondary"
304
+ >
274
305
  <i
275
306
  className={`fas fa-sync-alt ${loadingStatus ? "animate-spin" : ""}`}
276
307
  ></i>
@@ -286,6 +317,7 @@ export function App() {
286
317
  <p className="text-sm text-red-300">{bannerError}</p>
287
318
  </div>
288
319
  <button
320
+ type="button"
289
321
  onClick={() => setBannerError(null)}
290
322
  className="text-red-400 hover:text-red-300"
291
323
  aria-label="Close error"
@@ -309,7 +341,7 @@ export function App() {
309
341
  </div>
310
342
 
311
343
  {/* Status Panel */}
312
- <div id="status" className="animate-fadeIn scroll-mt-24">
344
+ <div id={sectionIds.status} className="animate-fadeIn scroll-mt-24">
313
345
  <StatusPanel
314
346
  status={status}
315
347
  loading={loadingStatus}
@@ -319,7 +351,7 @@ export function App() {
319
351
 
320
352
  {/* Quick Actions - Optional */}
321
353
  <div
322
- id="actions"
354
+ id={sectionIds.actions}
323
355
  className="glass-panel p-6 animate-fadeIn scroll-mt-24"
324
356
  style={{ animationDelay: "0.1s" }}
325
357
  >
@@ -333,7 +365,7 @@ export function App() {
333
365
  {/* Programs and Tokens Stacked */}
334
366
  <div className="space-y-6">
335
367
  <div
336
- id="programs"
368
+ id={sectionIds.programs}
337
369
  className="animate-fadeIn scroll-mt-24"
338
370
  style={{ animationDelay: "0.2s" }}
339
371
  >
@@ -345,7 +377,7 @@ export function App() {
345
377
  />
346
378
  </div>
347
379
  <div
348
- id="tokens"
380
+ id={sectionIds.tokens}
349
381
  className="animate-fadeIn scroll-mt-24"
350
382
  style={{ animationDelay: "0.3s" }}
351
383
  >