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
@@ -4,27 +4,36 @@ import { accounts } from "./schema/accounts";
4
4
  import { addressSignatures } from "./schema/address-signatures";
5
5
  import { transactions } from "./schema/transactions";
6
6
  import { txAccounts } from "./schema/tx-accounts";
7
+ import { txAccountStates } from "./schema/tx-account-states";
7
8
 
8
9
  export type InsertTxBundle = {
9
- signature: string;
10
- slot: number;
11
- blockTime?: number;
12
- version: 0 | "legacy";
13
- fee: number;
14
- err: unknown | null;
15
- rawBase64: string;
16
- preBalances: number[];
17
- postBalances: number[];
18
- logs: string[];
19
- accounts: Array<{
20
- address: string;
21
- index: number;
22
- signer: boolean;
23
- writable: boolean;
24
- programIdIndex?: number;
25
- }>;
26
- preTokenBalances?: any[];
27
- postTokenBalances?: any[];
10
+ signature: string;
11
+ slot: number;
12
+ blockTime?: number;
13
+ version: 0 | "legacy";
14
+ fee: number;
15
+ err: unknown | null;
16
+ rawBase64: string;
17
+ preBalances: number[];
18
+ postBalances: number[];
19
+ logs: string[];
20
+ innerInstructions?: unknown[];
21
+ computeUnits?: number | bigint | null;
22
+ returnData?: { programId: string; dataBase64: string } | null;
23
+ accounts: Array<{
24
+ address: string;
25
+ index: number;
26
+ signer: boolean;
27
+ writable: boolean;
28
+ programIdIndex?: number;
29
+ }>;
30
+ preTokenBalances?: unknown[];
31
+ postTokenBalances?: unknown[];
32
+ accountStates?: Array<{
33
+ address: string;
34
+ pre?: Partial<AccountSnapshot> | null;
35
+ post?: Partial<AccountSnapshot> | null;
36
+ }>;
28
37
  };
29
38
 
30
39
  export type AccountSnapshot = {
@@ -39,57 +48,81 @@ export type AccountSnapshot = {
39
48
  };
40
49
 
41
50
  export class TxStore {
42
- async insertTransactionBundle(bundle: InsertTxBundle): Promise<void> {
43
- const errJson = bundle.err ? JSON.stringify(bundle.err) : null;
44
- await db.transaction(async (tx) => {
45
- await tx
46
- .insert(transactions)
47
- .values({
48
- signature: bundle.signature,
49
- slot: bundle.slot,
50
- blockTime: bundle.blockTime ?? null,
51
- version: String(bundle.version),
52
- errJson,
53
- fee: bundle.fee,
54
- rawBase64: bundle.rawBase64,
55
- preBalancesJson: JSON.stringify(bundle.preBalances ?? []),
56
- postBalancesJson: JSON.stringify(bundle.postBalances ?? []),
57
- logsJson: JSON.stringify(bundle.logs ?? []),
58
- preTokenBalancesJson: JSON.stringify(bundle.preTokenBalances ?? []),
59
- postTokenBalancesJson: JSON.stringify(bundle.postTokenBalances ?? []),
60
- })
61
- .onConflictDoNothing();
62
-
63
- if (Array.isArray(bundle.accounts) && bundle.accounts.length > 0) {
64
- await tx
65
- .insert(txAccounts)
66
- .values(
67
- bundle.accounts.map((a) => ({
68
- signature: bundle.signature,
69
- accountIndex: a.index,
70
- address: a.address,
71
- signer: a.signer ? 1 : 0,
72
- writable: a.writable ? 1 : 0,
73
- programIdIndex: a.programIdIndex ?? null,
74
- })),
75
- )
76
- .onConflictDoNothing();
77
-
78
- await tx
79
- .insert(addressSignatures)
80
- .values(
81
- bundle.accounts.map((a) => ({
82
- address: a.address,
83
- signature: bundle.signature,
84
- slot: bundle.slot,
85
- err: errJson ? 1 : 0,
86
- blockTime: bundle.blockTime ?? null,
87
- })),
88
- )
89
- .onConflictDoNothing();
90
- }
91
- });
92
- }
51
+ async insertTransactionBundle(bundle: InsertTxBundle): Promise<void> {
52
+ const errJson = bundle.err ? JSON.stringify(bundle.err) : null;
53
+ await db.transaction(async (tx) => {
54
+ await tx
55
+ .insert(transactions)
56
+ .values({
57
+ signature: bundle.signature,
58
+ slot: bundle.slot,
59
+ blockTime: bundle.blockTime ?? null,
60
+ version: String(bundle.version),
61
+ errJson,
62
+ fee: bundle.fee,
63
+ rawBase64: bundle.rawBase64,
64
+ preBalancesJson: JSON.stringify(bundle.preBalances ?? []),
65
+ postBalancesJson: JSON.stringify(bundle.postBalances ?? []),
66
+ logsJson: JSON.stringify(bundle.logs ?? []),
67
+ preTokenBalancesJson: JSON.stringify(bundle.preTokenBalances ?? []),
68
+ postTokenBalancesJson: JSON.stringify(bundle.postTokenBalances ?? []),
69
+ innerInstructionsJson: JSON.stringify(bundle.innerInstructions ?? []),
70
+ computeUnits:
71
+ bundle.computeUnits == null
72
+ ? null
73
+ : Number(bundle.computeUnits),
74
+ returnDataProgramId: bundle.returnData?.programId ?? null,
75
+ returnDataBase64: bundle.returnData?.dataBase64 ?? null,
76
+ })
77
+ .onConflictDoNothing();
78
+
79
+ if (Array.isArray(bundle.accounts) && bundle.accounts.length > 0) {
80
+ await tx
81
+ .insert(txAccounts)
82
+ .values(
83
+ bundle.accounts.map((a) => ({
84
+ signature: bundle.signature,
85
+ accountIndex: a.index,
86
+ address: a.address,
87
+ signer: a.signer ? 1 : 0,
88
+ writable: a.writable ? 1 : 0,
89
+ programIdIndex: a.programIdIndex ?? null,
90
+ })),
91
+ )
92
+ .onConflictDoNothing();
93
+
94
+ await tx
95
+ .insert(addressSignatures)
96
+ .values(
97
+ bundle.accounts.map((a) => ({
98
+ address: a.address,
99
+ signature: bundle.signature,
100
+ slot: bundle.slot,
101
+ err: errJson ? 1 : 0,
102
+ blockTime: bundle.blockTime ?? null,
103
+ })),
104
+ )
105
+ .onConflictDoNothing();
106
+ }
107
+
108
+ if (
109
+ Array.isArray(bundle.accountStates) &&
110
+ bundle.accountStates.length > 0
111
+ ) {
112
+ await tx
113
+ .insert(txAccountStates)
114
+ .values(
115
+ bundle.accountStates.map((s) => ({
116
+ signature: bundle.signature,
117
+ address: s.address,
118
+ preJson: s.pre ? JSON.stringify(s.pre) : null,
119
+ postJson: s.post ? JSON.stringify(s.post) : null,
120
+ })),
121
+ )
122
+ .onConflictDoNothing();
123
+ }
124
+ });
125
+ }
93
126
 
94
127
  async upsertAccounts(snapshots: AccountSnapshot[]): Promise<void> {
95
128
  if (!Array.isArray(snapshots) || snapshots.length === 0) return;
@@ -133,7 +166,7 @@ export class TxStore {
133
166
 
134
167
  async getStatuses(signatures: string[]) {
135
168
  if (!Array.isArray(signatures) || signatures.length === 0)
136
- return new Map<string, { slot: number; err: any | null }>();
169
+ return new Map<string, { slot: number; err: unknown | null }>();
137
170
  const results = await db
138
171
  .select({
139
172
  signature: transactions.signature,
@@ -142,7 +175,7 @@ export class TxStore {
142
175
  })
143
176
  .from(transactions)
144
177
  .where(inArraySafe(transactions.signature, signatures));
145
- const map = new Map<string, { slot: number; err: any | null }>();
178
+ const map = new Map<string, { slot: number; err: unknown | null }>();
146
179
  for (const r of results)
147
180
  map.set(r.signature, {
148
181
  slot: Number(r.slot),
@@ -167,7 +200,7 @@ export class TxStore {
167
200
  }
168
201
  const limit = Math.min(Math.max(opts.limit ?? 1000, 1), 1000);
169
202
 
170
- const whereClauses = [eq(addressSignatures.address, address)] as any[];
203
+ const whereClauses = [eq(addressSignatures.address, address)] as unknown[];
171
204
  if (typeof beforeSlot === "number")
172
205
  whereClauses.push(lt(addressSignatures.slot, beforeSlot));
173
206
  if (typeof untilSlot === "number")
@@ -216,7 +249,7 @@ export class TxStore {
216
249
  }
217
250
  }
218
251
 
219
- function safeParse<T = any>(s: string): T | null {
252
+ function safeParse<T = unknown>(s: string): T | null {
220
253
  try {
221
254
  return JSON.parse(s) as T;
222
255
  } catch {
@@ -224,6 +257,10 @@ function safeParse<T = any>(s: string): T | null {
224
257
  }
225
258
  }
226
259
 
227
- function inArraySafe<T>(col: any, arr: T[]) {
228
- return arr.length > 0 ? inArray(col, arr as any) : eq(col, "__never__");
260
+ function inArraySafe<T>(col: unknown, arr: T[]) {
261
+ return arr.length > 0
262
+ ? // biome-ignore lint/suspicious/noExplicitAny: Drizzle generic typing workaround
263
+ (inArray as unknown as (c: unknown, a: T[]) => any)(col, arr)
264
+ : // biome-ignore lint/suspicious/noExplicitAny: Force an always-false predicate without over-constraining types
265
+ eq(col as any, "__never__");
229
266
  }
@@ -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
  >
@@ -1,14 +1,20 @@
1
- import { type ChangeEvent, type FormEvent, useMemo, useState } from "react";
1
+ import {
2
+ type ChangeEvent,
3
+ type FormEvent,
4
+ useId,
5
+ useMemo,
6
+ useState,
7
+ } from "react";
2
8
  import type { TokenSummary } from "../api";
3
9
 
4
10
  interface Props {
5
11
  tokens: TokenSummary[];
6
- onAirdrop: (address: string, lamports: string) => Promise<string | void>;
12
+ onAirdrop: (address: string, lamports: string) => Promise<string | undefined>;
7
13
  onMint: (
8
14
  mint: string,
9
15
  owner: string,
10
16
  amountRaw: string,
11
- ) => Promise<string | void>;
17
+ ) => Promise<string | undefined>;
12
18
  }
13
19
 
14
20
  const SOL_OPTION = {
@@ -53,6 +59,11 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
53
59
  const [error, setError] = useState<string | null>(null);
54
60
  const [message, setMessage] = useState<string | null>(null);
55
61
 
62
+ const uid = useId();
63
+ const recipientId = `${uid}-recipient`;
64
+ const assetId = `${uid}-asset`;
65
+ const amountId = `${uid}-amount`;
66
+
56
67
  const options = useMemo(() => {
57
68
  const tokenOpts = tokens.map((token) => ({
58
69
  value: token.mint,
@@ -87,8 +98,9 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
87
98
  try {
88
99
  const note = await submit();
89
100
  setMessage(note);
90
- } catch (err: any) {
91
- setError(err?.message ?? String(err));
101
+ } catch (err) {
102
+ const message = err instanceof Error ? err.message : String(err);
103
+ setError(message);
92
104
  } finally {
93
105
  setPending(false);
94
106
  }
@@ -116,11 +128,15 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
116
128
 
117
129
  <div className="grid gap-4 lg:grid-cols-3">
118
130
  <div className="space-y-2">
119
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
131
+ <label
132
+ htmlFor={recipientId}
133
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
134
+ >
120
135
  Recipient Address
121
136
  </label>
122
137
  <div className="relative">
123
138
  <input
139
+ id={recipientId}
124
140
  value={recipient}
125
141
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
126
142
  setRecipient(event.target.value)
@@ -133,11 +149,15 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
133
149
  </div>
134
150
 
135
151
  <div className="space-y-2">
136
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
152
+ <label
153
+ htmlFor={assetId}
154
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
155
+ >
137
156
  Asset
138
157
  </label>
139
158
  <div className="relative">
140
159
  <select
160
+ id={assetId}
141
161
  value={asset}
142
162
  onChange={(event: ChangeEvent<HTMLSelectElement>) =>
143
163
  setAsset(event.target.value)
@@ -155,11 +175,15 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
155
175
  </div>
156
176
 
157
177
  <div className="space-y-2">
158
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
178
+ <label
179
+ htmlFor={amountId}
180
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
181
+ >
159
182
  Amount
160
183
  </label>
161
184
  <div className="relative">
162
185
  <input
186
+ id={amountId}
163
187
  value={amount}
164
188
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
165
189
  setAmount(event.target.value)
@@ -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 {
@@ -13,6 +13,9 @@ interface Props {
13
13
  }
14
14
 
15
15
  export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
16
+ const programIdId = useId();
17
+ const endpointId = useId();
18
+ const accountLimitId = useId();
16
19
  const [programId, setProgramId] = useState("");
17
20
  const [endpoint, setEndpoint] = useState("");
18
21
  const [withAccounts, setWithAccounts] = useState(true);
@@ -37,8 +40,12 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
37
40
  setProgramId("");
38
41
  setEndpoint("");
39
42
  setAccountsLimit("100");
40
- } catch (err: any) {
41
- setError(err?.message ?? String(err));
43
+ } catch (err: unknown) {
44
+ const message =
45
+ err && typeof err === "object" && "message" in err
46
+ ? String((err as { message?: unknown }).message)
47
+ : String(err);
48
+ setError(message);
42
49
  } finally {
43
50
  setPending(false);
44
51
  }
@@ -92,11 +99,15 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
92
99
  >
93
100
  <div className="space-y-5">
94
101
  <div className="space-y-2">
95
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
102
+ <label
103
+ htmlFor={programIdId}
104
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
105
+ >
96
106
  Program ID *
97
107
  </label>
98
108
  <div className="relative">
99
109
  <input
110
+ id={programIdId}
100
111
  value={programId}
101
112
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
102
113
  setProgramId(event.target.value)
@@ -109,11 +120,15 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
109
120
  </div>
110
121
 
111
122
  <div className="space-y-2">
112
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
123
+ <label
124
+ htmlFor={endpointId}
125
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
126
+ >
113
127
  RPC Endpoint (Optional)
114
128
  </label>
115
129
  <div className="relative">
116
130
  <input
131
+ id={endpointId}
117
132
  value={endpoint}
118
133
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
119
134
  setEndpoint(event.target.value)
@@ -147,11 +162,15 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
147
162
 
148
163
  {withAccounts && (
149
164
  <div className="ml-8 space-y-2 pt-2 border-t border-white/5">
150
- <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
165
+ <label
166
+ htmlFor={accountLimitId}
167
+ className="block text-xs font-semibold text-gray-400 uppercase tracking-wider"
168
+ >
151
169
  Account Limit
152
170
  </label>
153
171
  <div className="relative">
154
172
  <input
173
+ id={accountLimitId}
155
174
  value={accountsLimit}
156
175
  onChange={(event: ChangeEvent<HTMLInputElement>) =>
157
176
  setAccountsLimit(event.target.value)