solforge 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +471 -79
  2. package/cli.cjs +106 -78
  3. package/package.json +1 -1
  4. package/scripts/install.sh +1 -1
  5. package/scripts/postinstall.cjs +69 -61
  6. package/server/lib/base58.ts +1 -1
  7. package/server/methods/account/get-account-info.ts +3 -7
  8. package/server/methods/account/get-balance.ts +3 -7
  9. package/server/methods/account/get-multiple-accounts.ts +2 -1
  10. package/server/methods/account/get-parsed-account-info.ts +3 -7
  11. package/server/methods/account/parsers/index.ts +2 -2
  12. package/server/methods/account/parsers/loader-upgradeable.ts +14 -1
  13. package/server/methods/account/parsers/spl-token.ts +29 -10
  14. package/server/methods/account/request-airdrop.ts +44 -31
  15. package/server/methods/block/get-block.ts +3 -7
  16. package/server/methods/block/get-blocks-with-limit.ts +3 -7
  17. package/server/methods/block/is-blockhash-valid.ts +3 -7
  18. package/server/methods/get-address-lookup-table.ts +3 -7
  19. package/server/methods/program/get-program-accounts.ts +9 -9
  20. package/server/methods/program/get-token-account-balance.ts +3 -7
  21. package/server/methods/program/get-token-accounts-by-delegate.ts +4 -3
  22. package/server/methods/program/get-token-accounts-by-owner.ts +61 -35
  23. package/server/methods/program/get-token-largest-accounts.ts +3 -2
  24. package/server/methods/program/get-token-supply.ts +3 -2
  25. package/server/methods/solforge/index.ts +9 -6
  26. package/server/methods/transaction/get-parsed-transaction.ts +3 -7
  27. package/server/methods/transaction/get-signature-statuses.ts +14 -7
  28. package/server/methods/transaction/get-signatures-for-address.ts +3 -7
  29. package/server/methods/transaction/get-transaction.ts +167 -81
  30. package/server/methods/transaction/send-transaction.ts +29 -16
  31. package/server/methods/transaction/simulate-transaction.ts +3 -2
  32. package/server/rpc-server.ts +47 -34
  33. package/server/types.ts +9 -6
  34. package/server/ws-server.ts +15 -8
  35. package/src/api-server-entry.ts +91 -91
  36. package/src/cli/commands/airdrop.ts +2 -2
  37. package/src/cli/commands/config.ts +2 -2
  38. package/src/cli/commands/mint.ts +3 -3
  39. package/src/cli/commands/program-clone.ts +9 -11
  40. package/src/cli/commands/program-load.ts +3 -3
  41. package/src/cli/commands/rpc-start.ts +8 -5
  42. package/src/cli/commands/token-adopt-authority.ts +1 -1
  43. package/src/cli/commands/token-clone.ts +5 -6
  44. package/src/cli/commands/token-create.ts +5 -5
  45. package/src/cli/main.ts +38 -37
  46. package/src/cli/run-solforge.ts +20 -6
  47. package/src/cli/setup-wizard.ts +8 -6
  48. package/src/commands/add-program.ts +324 -328
  49. package/src/commands/init.ts +106 -106
  50. package/src/commands/list.ts +125 -125
  51. package/src/commands/mint.ts +247 -248
  52. package/src/commands/start.ts +837 -833
  53. package/src/commands/status.ts +80 -80
  54. package/src/commands/stop.ts +381 -382
  55. package/src/config/index.ts +33 -17
  56. package/src/config/manager.ts +150 -150
  57. package/src/db/index.ts +2 -2
  58. package/src/db/tx-store.ts +12 -8
  59. package/src/gui/public/app.css +1556 -1
  60. package/src/gui/public/build/main.css +1569 -1
  61. package/src/gui/server.ts +21 -22
  62. package/src/gui/src/api.ts +1 -1
  63. package/src/gui/src/app.tsx +96 -45
  64. package/src/gui/src/components/airdrop-mint-form.tsx +49 -19
  65. package/src/gui/src/components/clone-program-modal.tsx +31 -12
  66. package/src/gui/src/components/clone-token-modal.tsx +32 -13
  67. package/src/gui/src/components/modal.tsx +18 -11
  68. package/src/gui/src/components/programs-panel.tsx +27 -15
  69. package/src/gui/src/components/status-panel.tsx +32 -18
  70. package/src/gui/src/components/tokens-panel.tsx +25 -19
  71. package/src/gui/src/index.css +491 -463
  72. package/src/index.ts +177 -149
  73. package/src/rpc/start.ts +1 -1
  74. package/src/services/api-server.ts +494 -475
  75. package/src/services/port-manager.ts +164 -167
  76. package/src/services/process-registry.ts +144 -145
  77. package/src/services/program-cloner.ts +312 -312
  78. package/src/services/token-cloner.ts +799 -797
  79. package/src/services/validator.ts +288 -290
  80. package/src/types/config.ts +72 -72
  81. package/src/utils/shell.ts +75 -75
  82. package/src/utils/token-loader.ts +78 -78
package/src/gui/server.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { serve } from "bun";
1
+ import { file, serve } from "bun";
2
2
  import type { LiteSVMRpcServer } from "../../server/rpc-server";
3
3
  import type { JsonRpcResponse } from "../../server/types";
4
4
  import { readConfig, writeConfig } from "../config";
5
- import indexHtml from "./public/index.html";
6
- import { file } from "bun";
7
5
  // Embed built GUI assets as files so the compiled binary can stream them
8
6
  import appCssFile from "./public/app.css" with { type: "file" };
9
- import mainJsFile from "./public/build/main.js" with { type: "file" };
10
7
  import bundledCssFile from "./public/build/main.css" with { type: "file" };
8
+ import mainJsFile from "./public/build/main.js" with { type: "file" };
9
+ import indexHtml from "./public/index.html";
11
10
 
12
11
  type GuiStartOptions = {
13
12
  port?: number;
@@ -47,13 +46,13 @@ const text = (value: string, status = 200) =>
47
46
 
48
47
  const okOptions = () => new Response(null, { status: 204, headers: CORS });
49
48
  const css = (fpath: string) =>
50
- new Response(file(fpath), {
51
- headers: { ...CORS, "Content-Type": "text/css" },
52
- });
49
+ new Response(file(fpath), {
50
+ headers: { ...CORS, "Content-Type": "text/css" },
51
+ });
53
52
  const js = (fpath: string) =>
54
- new Response(file(fpath), {
55
- headers: { ...CORS, "Content-Type": "application/javascript" },
56
- });
53
+ new Response(file(fpath), {
54
+ headers: { ...CORS, "Content-Type": "application/javascript" },
55
+ });
57
56
 
58
57
  const handleError = (error: unknown) => {
59
58
  if (error instanceof HttpError)
@@ -108,7 +107,7 @@ export function startGuiServer(opts: GuiStartOptions = {}) {
108
107
  const rpcServer = opts.rpcServer;
109
108
  const rpcUrl = `http://${host}:${rpcPort}`;
110
109
 
111
- const callRpc = async (method: string, params: any[] = []) => {
110
+ const callRpc = async (method: string, params: unknown[] = []) => {
112
111
  if (!rpcServer) throw new HttpError(503, "RPC server not available");
113
112
  const response: JsonRpcResponse = await rpcServer.handleRequest({
114
113
  jsonrpc: "2.0",
@@ -153,11 +152,11 @@ export function startGuiServer(opts: GuiStartOptions = {}) {
153
152
  }
154
153
  }
155
154
 
156
- const routes = {
157
- "/": indexHtml,
158
- "/app.css": { GET: () => css(appCssFile) },
159
- "/build/main.css": { GET: () => css(bundledCssFile) },
160
- "/build/main.js": { GET: () => js(mainJsFile) },
155
+ const routes = {
156
+ "/": indexHtml,
157
+ "/app.css": { GET: () => css(appCssFile) },
158
+ "/build/main.css": { GET: () => css(bundledCssFile) },
159
+ "/build/main.js": { GET: () => js(mainJsFile) },
161
160
  "/health": { GET: () => text("ok") },
162
161
  "/api/config": { GET: () => json({ rpcUrl }), OPTIONS: okOptions },
163
162
  "/api/status": {
@@ -284,12 +283,12 @@ export function startGuiServer(opts: GuiStartOptions = {}) {
284
283
  },
285
284
  } as const;
286
285
 
287
- const server = serve({
288
- port,
289
- hostname: host,
290
- routes,
291
- development: false,
292
- });
286
+ const server = serve({
287
+ port,
288
+ hostname: host,
289
+ routes,
290
+ development: false,
291
+ });
293
292
  console.log(`🖥️ Solforge GUI available at http://${host}:${server.port}`);
294
293
  return { server, port: server.port };
295
294
  }
@@ -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,17 +166,22 @@ 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"
157
173
  >
158
- <i className={`fas fa-${sidebarOpen ? 'times' : 'bars'} text-white`}></i>
174
+ <i
175
+ className={`fas fa-${sidebarOpen ? "times" : "bars"} text-white`}
176
+ ></i>
159
177
  </button>
160
178
 
161
179
  {/* Sidebar Navigation */}
162
- <aside className={`fixed top-0 left-0 h-full w-72 glass-panel rounded-none border-r border-white/5 z-40 transition-transform duration-300 ${
163
- sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
164
- }`}>
180
+ <aside
181
+ className={`fixed top-0 left-0 h-full w-72 glass-panel rounded-none border-r border-white/5 z-40 transition-transform duration-300 ${
182
+ sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
183
+ }`}
184
+ >
165
185
  <div className="p-6 space-y-8">
166
186
  {/* Logo */}
167
187
  <div className="flex items-center gap-3">
@@ -178,45 +198,49 @@ export function App() {
178
198
 
179
199
  {/* Navigation Items */}
180
200
  <nav className="space-y-2">
181
- <button
182
- onClick={() => scrollToSection('status')}
201
+ <button
202
+ type="button"
203
+ onClick={() => scrollToSection("status")}
183
204
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
184
- activeSection === 'status'
185
- ? 'bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300'
186
- : 'text-gray-400 hover:bg-white/5'
205
+ activeSection === "status"
206
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
207
+ : "text-gray-400 hover:bg-white/5"
187
208
  }`}
188
209
  >
189
210
  <i className="fas fa-server w-5"></i>
190
211
  <span className="font-medium">Network Status</span>
191
212
  </button>
192
- <button
193
- onClick={() => scrollToSection('actions')}
213
+ <button
214
+ type="button"
215
+ onClick={() => scrollToSection("actions")}
194
216
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
195
- activeSection === 'actions'
196
- ? 'bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300'
197
- : 'text-gray-400 hover:bg-white/5'
217
+ activeSection === "actions"
218
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
219
+ : "text-gray-400 hover:bg-white/5"
198
220
  }`}
199
221
  >
200
222
  <i className="fas fa-paper-plane w-5"></i>
201
223
  <span className="font-medium">Quick Actions</span>
202
224
  </button>
203
- <button
204
- onClick={() => scrollToSection('programs')}
225
+ <button
226
+ type="button"
227
+ onClick={() => scrollToSection("programs")}
205
228
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
206
- activeSection === 'programs'
207
- ? 'bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300'
208
- : 'text-gray-400 hover:bg-white/5'
229
+ activeSection === "programs"
230
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
231
+ : "text-gray-400 hover:bg-white/5"
209
232
  }`}
210
233
  >
211
234
  <i className="fas fa-code w-5"></i>
212
235
  <span className="font-medium">Programs</span>
213
236
  </button>
214
- <button
215
- onClick={() => scrollToSection('tokens')}
237
+ <button
238
+ type="button"
239
+ onClick={() => scrollToSection("tokens")}
216
240
  className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
217
- activeSection === 'tokens'
218
- ? 'bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300'
219
- : 'text-gray-400 hover:bg-white/5'
241
+ activeSection === "tokens"
242
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
243
+ : "text-gray-400 hover:bg-white/5"
220
244
  }`}
221
245
  >
222
246
  <i className="fas fa-coins w-5"></i>
@@ -227,13 +251,17 @@ export function App() {
227
251
  {/* Quick Stats */}
228
252
  {config && (
229
253
  <div className="space-y-3">
230
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Connection</h3>
254
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
255
+ Connection
256
+ </h3>
231
257
  <div className="p-3 rounded-xl bg-white/5 border border-white/10">
232
258
  <div className="flex items-center gap-2 mb-2">
233
259
  <span className="status-dot online"></span>
234
260
  <span className="text-xs text-gray-400">Connected</span>
235
261
  </div>
236
- <p className="text-xs text-gray-500 font-mono break-all">{config.rpcUrl}</p>
262
+ <p className="text-xs text-gray-500 font-mono break-all">
263
+ {config.rpcUrl}
264
+ </p>
237
265
  </div>
238
266
  </div>
239
267
  )}
@@ -242,9 +270,16 @@ export function App() {
242
270
 
243
271
  {/* Overlay for mobile */}
244
272
  {sidebarOpen && (
245
- <div
273
+ <button
274
+ type="button"
246
275
  className="fixed inset-0 bg-black/50 backdrop-blur-sm z-30 lg:hidden"
276
+ aria-label="Close sidebar overlay"
247
277
  onClick={() => setSidebarOpen(false)}
278
+ onKeyDown={(e) => {
279
+ if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
280
+ setSidebarOpen(false);
281
+ }
282
+ }}
248
283
  />
249
284
  )}
250
285
 
@@ -262,11 +297,14 @@ export function App() {
262
297
  Manage your local Solana development environment
263
298
  </p>
264
299
  </div>
265
- <button
300
+ <button
301
+ type="button"
266
302
  onClick={loadStatus}
267
303
  className="btn-secondary"
268
304
  >
269
- <i className={`fas fa-sync-alt ${loadingStatus ? 'animate-spin' : ''}`}></i>
305
+ <i
306
+ className={`fas fa-sync-alt ${loadingStatus ? "animate-spin" : ""}`}
307
+ ></i>
270
308
  <span>Refresh All</span>
271
309
  </button>
272
310
  </div>
@@ -279,6 +317,7 @@ export function App() {
279
317
  <p className="text-sm text-red-300">{bannerError}</p>
280
318
  </div>
281
319
  <button
320
+ type="button"
282
321
  onClick={() => setBannerError(null)}
283
322
  className="text-red-400 hover:text-red-300"
284
323
  aria-label="Close error"
@@ -302,7 +341,7 @@ export function App() {
302
341
  </div>
303
342
 
304
343
  {/* Status Panel */}
305
- <div id="status" className="animate-fadeIn scroll-mt-24">
344
+ <div id={sectionIds.status} className="animate-fadeIn scroll-mt-24">
306
345
  <StatusPanel
307
346
  status={status}
308
347
  loading={loadingStatus}
@@ -311,7 +350,11 @@ export function App() {
311
350
  </div>
312
351
 
313
352
  {/* Quick Actions - Optional */}
314
- <div id="actions" className="glass-panel p-6 animate-fadeIn scroll-mt-24" style={{animationDelay: '0.1s'}}>
353
+ <div
354
+ id={sectionIds.actions}
355
+ className="glass-panel p-6 animate-fadeIn scroll-mt-24"
356
+ style={{ animationDelay: "0.1s" }}
357
+ >
315
358
  <AirdropMintForm
316
359
  tokens={tokens}
317
360
  onAirdrop={onAirdrop}
@@ -321,7 +364,11 @@ export function App() {
321
364
 
322
365
  {/* Programs and Tokens Stacked */}
323
366
  <div className="space-y-6">
324
- <div id="programs" className="animate-fadeIn scroll-mt-24" style={{animationDelay: '0.2s'}}>
367
+ <div
368
+ id={sectionIds.programs}
369
+ className="animate-fadeIn scroll-mt-24"
370
+ style={{ animationDelay: "0.2s" }}
371
+ >
325
372
  <ProgramsPanel
326
373
  programs={programs}
327
374
  loading={loadingPrograms}
@@ -329,7 +376,11 @@ export function App() {
329
376
  onAdd={openProgramModal}
330
377
  />
331
378
  </div>
332
- <div id="tokens" className="animate-fadeIn scroll-mt-24" style={{animationDelay: '0.3s'}}>
379
+ <div
380
+ id={sectionIds.tokens}
381
+ className="animate-fadeIn scroll-mt-24"
382
+ style={{ animationDelay: "0.3s" }}
383
+ >
333
384
  <TokensPanel
334
385
  tokens={tokens}
335
386
  loading={loadingTokens}
@@ -387,4 +438,4 @@ export function App() {
387
438
  `}</style>
388
439
  </div>
389
440
  );
390
- }
441
+ }
@@ -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
  }
@@ -113,14 +125,18 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
113
125
  </span>
114
126
  </div>
115
127
  </div>
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)
@@ -131,13 +147,17 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
131
147
  <i className="fas fa-user absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
132
148
  </div>
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)
@@ -153,13 +173,17 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
153
173
  <i className="fas fa-coins absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
154
174
  </div>
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)
@@ -177,12 +201,12 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
177
201
  </p>
178
202
  </div>
179
203
  </div>
180
-
204
+
181
205
  <div className="mt-6 flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
182
206
  <button
183
207
  type="submit"
184
208
  disabled={pending}
185
- className={`btn-primary flex-1 sm:flex-initial ${pending ? 'opacity-50 cursor-not-allowed' : ''}`}
209
+ className={`btn-primary flex-1 sm:flex-initial ${pending ? "opacity-50 cursor-not-allowed" : ""}`}
186
210
  >
187
211
  {pending ? (
188
212
  <>
@@ -191,26 +215,32 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
191
215
  </>
192
216
  ) : (
193
217
  <>
194
- <i className={`fas fa-${asset === SOL_OPTION.value ? 'parachute-box' : 'coins'}`}></i>
195
- <span>{asset === SOL_OPTION.value ? "Airdrop SOL" : "Mint Tokens"}</span>
218
+ <i
219
+ className={`fas fa-${asset === SOL_OPTION.value ? "parachute-box" : "coins"}`}
220
+ ></i>
221
+ <span>
222
+ {asset === SOL_OPTION.value ? "Airdrop SOL" : "Mint Tokens"}
223
+ </span>
196
224
  </>
197
225
  )}
198
226
  </button>
199
-
227
+
200
228
  {error && (
201
229
  <div className="flex-1 flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
202
230
  <i className="fas fa-exclamation-circle text-red-400"></i>
203
231
  <p className="text-sm text-red-300">{error}</p>
204
232
  </div>
205
233
  )}
206
-
234
+
207
235
  {message && (
208
236
  <div className="flex-1 flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/30">
209
237
  <i className="fas fa-check-circle text-green-400"></i>
210
- <p className="text-sm text-green-300 font-mono text-xs break-all">{message}</p>
238
+ <p className="text-sm text-green-300 font-mono text-xs break-all">
239
+ {message}
240
+ </p>
211
241
  </div>
212
242
  )}
213
243
  </div>
214
244
  </form>
215
245
  );
216
- }
246
+ }
@@ -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
  }
@@ -72,7 +79,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
72
79
  type="button"
73
80
  onClick={handleSubmit}
74
81
  disabled={pending || programId.trim().length === 0}
75
- className={`btn-primary ${(pending || programId.trim().length === 0) ? 'opacity-50 cursor-not-allowed' : ''}`}
82
+ className={`btn-primary ${pending || programId.trim().length === 0 ? "opacity-50 cursor-not-allowed" : ""}`}
76
83
  >
77
84
  {pending ? (
78
85
  <>
@@ -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)
@@ -107,13 +118,17 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
107
118
  <i className="fas fa-fingerprint absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
108
119
  </div>
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)
@@ -124,7 +139,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
124
139
  <i className="fas fa-globe absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
125
140
  </div>
126
141
  </div>
127
-
142
+
128
143
  <div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3">
129
144
  <label className="flex items-center gap-3 cursor-pointer group">
130
145
  <input
@@ -144,14 +159,18 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
144
159
  </p>
145
160
  </div>
146
161
  </label>
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)
@@ -170,7 +189,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
170
189
  </div>
171
190
  )}
172
191
  </div>
173
-
192
+
174
193
  {error && (
175
194
  <div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
176
195
  <i className="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
@@ -180,4 +199,4 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
180
199
  </div>
181
200
  </Modal>
182
201
  );
183
- }
202
+ }