solforge 0.2.3 → 0.2.5

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 (43) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +323 -364
  3. package/cli.cjs +126 -69
  4. package/package.json +1 -1
  5. package/scripts/install.sh +112 -0
  6. package/scripts/postinstall.cjs +66 -58
  7. package/server/methods/program/get-token-accounts-by-owner.ts +7 -2
  8. package/server/ws-server.ts +4 -1
  9. package/src/api-server-entry.ts +91 -91
  10. package/src/cli/commands/rpc-start.ts +4 -1
  11. package/src/cli/main.ts +39 -14
  12. package/src/cli/run-solforge.ts +20 -6
  13. package/src/commands/add-program.ts +324 -328
  14. package/src/commands/init.ts +106 -106
  15. package/src/commands/list.ts +125 -125
  16. package/src/commands/mint.ts +246 -246
  17. package/src/commands/start.ts +834 -831
  18. package/src/commands/status.ts +80 -80
  19. package/src/commands/stop.ts +381 -382
  20. package/src/config/manager.ts +149 -149
  21. package/src/gui/public/app.css +1556 -1
  22. package/src/gui/public/build/main.css +1569 -1
  23. package/src/gui/server.ts +20 -21
  24. package/src/gui/src/app.tsx +56 -37
  25. package/src/gui/src/components/airdrop-mint-form.tsx +17 -11
  26. package/src/gui/src/components/clone-program-modal.tsx +6 -6
  27. package/src/gui/src/components/clone-token-modal.tsx +7 -7
  28. package/src/gui/src/components/modal.tsx +13 -11
  29. package/src/gui/src/components/programs-panel.tsx +27 -15
  30. package/src/gui/src/components/status-panel.tsx +31 -17
  31. package/src/gui/src/components/tokens-panel.tsx +25 -19
  32. package/src/gui/src/index.css +491 -463
  33. package/src/index.ts +161 -146
  34. package/src/rpc/start.ts +1 -1
  35. package/src/services/api-server.ts +470 -473
  36. package/src/services/port-manager.ts +167 -167
  37. package/src/services/process-registry.ts +143 -143
  38. package/src/services/program-cloner.ts +312 -312
  39. package/src/services/token-cloner.ts +799 -797
  40. package/src/services/validator.ts +288 -288
  41. package/src/types/config.ts +71 -71
  42. package/src/utils/shell.ts +75 -75
  43. package/src/utils/token-loader.ts +77 -77
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)
@@ -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
  }
@@ -143,7 +143,7 @@ export function App() {
143
143
 
144
144
  const scrollToSection = (sectionId: string) => {
145
145
  setActiveSection(sectionId);
146
- document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' });
146
+ document.getElementById(sectionId)?.scrollIntoView({ behavior: "smooth" });
147
147
  setSidebarOpen(false);
148
148
  };
149
149
 
@@ -155,13 +155,17 @@ export function App() {
155
155
  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
156
  aria-label="Menu"
157
157
  >
158
- <i className={`fas fa-${sidebarOpen ? 'times' : 'bars'} text-white`}></i>
158
+ <i
159
+ className={`fas fa-${sidebarOpen ? "times" : "bars"} text-white`}
160
+ ></i>
159
161
  </button>
160
162
 
161
163
  {/* 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
- }`}>
164
+ <aside
165
+ 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 ${
166
+ sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
167
+ }`}
168
+ >
165
169
  <div className="p-6 space-y-8">
166
170
  {/* Logo */}
167
171
  <div className="flex items-center gap-3">
@@ -178,45 +182,45 @@ export function App() {
178
182
 
179
183
  {/* Navigation Items */}
180
184
  <nav className="space-y-2">
181
- <button
182
- onClick={() => scrollToSection('status')}
185
+ <button
186
+ onClick={() => scrollToSection("status")}
183
187
  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'
188
+ activeSection === "status"
189
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
190
+ : "text-gray-400 hover:bg-white/5"
187
191
  }`}
188
192
  >
189
193
  <i className="fas fa-server w-5"></i>
190
194
  <span className="font-medium">Network Status</span>
191
195
  </button>
192
- <button
193
- onClick={() => scrollToSection('actions')}
196
+ <button
197
+ onClick={() => scrollToSection("actions")}
194
198
  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'
199
+ activeSection === "actions"
200
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
201
+ : "text-gray-400 hover:bg-white/5"
198
202
  }`}
199
203
  >
200
204
  <i className="fas fa-paper-plane w-5"></i>
201
205
  <span className="font-medium">Quick Actions</span>
202
206
  </button>
203
- <button
204
- onClick={() => scrollToSection('programs')}
207
+ <button
208
+ onClick={() => scrollToSection("programs")}
205
209
  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'
210
+ activeSection === "programs"
211
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
212
+ : "text-gray-400 hover:bg-white/5"
209
213
  }`}
210
214
  >
211
215
  <i className="fas fa-code w-5"></i>
212
216
  <span className="font-medium">Programs</span>
213
217
  </button>
214
- <button
215
- onClick={() => scrollToSection('tokens')}
218
+ <button
219
+ onClick={() => scrollToSection("tokens")}
216
220
  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'
221
+ activeSection === "tokens"
222
+ ? "bg-gradient-to-r from-purple-600/20 to-violet-600/20 border border-purple-500/30 text-purple-300"
223
+ : "text-gray-400 hover:bg-white/5"
220
224
  }`}
221
225
  >
222
226
  <i className="fas fa-coins w-5"></i>
@@ -227,13 +231,17 @@ export function App() {
227
231
  {/* Quick Stats */}
228
232
  {config && (
229
233
  <div className="space-y-3">
230
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Connection</h3>
234
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
235
+ Connection
236
+ </h3>
231
237
  <div className="p-3 rounded-xl bg-white/5 border border-white/10">
232
238
  <div className="flex items-center gap-2 mb-2">
233
239
  <span className="status-dot online"></span>
234
240
  <span className="text-xs text-gray-400">Connected</span>
235
241
  </div>
236
- <p className="text-xs text-gray-500 font-mono break-all">{config.rpcUrl}</p>
242
+ <p className="text-xs text-gray-500 font-mono break-all">
243
+ {config.rpcUrl}
244
+ </p>
237
245
  </div>
238
246
  </div>
239
247
  )}
@@ -242,7 +250,7 @@ export function App() {
242
250
 
243
251
  {/* Overlay for mobile */}
244
252
  {sidebarOpen && (
245
- <div
253
+ <div
246
254
  className="fixed inset-0 bg-black/50 backdrop-blur-sm z-30 lg:hidden"
247
255
  onClick={() => setSidebarOpen(false)}
248
256
  />
@@ -262,11 +270,10 @@ export function App() {
262
270
  Manage your local Solana development environment
263
271
  </p>
264
272
  </div>
265
- <button
266
- onClick={loadStatus}
267
- className="btn-secondary"
268
- >
269
- <i className={`fas fa-sync-alt ${loadingStatus ? 'animate-spin' : ''}`}></i>
273
+ <button onClick={loadStatus} className="btn-secondary">
274
+ <i
275
+ className={`fas fa-sync-alt ${loadingStatus ? "animate-spin" : ""}`}
276
+ ></i>
270
277
  <span>Refresh All</span>
271
278
  </button>
272
279
  </div>
@@ -311,7 +318,11 @@ export function App() {
311
318
  </div>
312
319
 
313
320
  {/* Quick Actions - Optional */}
314
- <div id="actions" className="glass-panel p-6 animate-fadeIn scroll-mt-24" style={{animationDelay: '0.1s'}}>
321
+ <div
322
+ id="actions"
323
+ className="glass-panel p-6 animate-fadeIn scroll-mt-24"
324
+ style={{ animationDelay: "0.1s" }}
325
+ >
315
326
  <AirdropMintForm
316
327
  tokens={tokens}
317
328
  onAirdrop={onAirdrop}
@@ -321,7 +332,11 @@ export function App() {
321
332
 
322
333
  {/* Programs and Tokens Stacked */}
323
334
  <div className="space-y-6">
324
- <div id="programs" className="animate-fadeIn scroll-mt-24" style={{animationDelay: '0.2s'}}>
335
+ <div
336
+ id="programs"
337
+ className="animate-fadeIn scroll-mt-24"
338
+ style={{ animationDelay: "0.2s" }}
339
+ >
325
340
  <ProgramsPanel
326
341
  programs={programs}
327
342
  loading={loadingPrograms}
@@ -329,7 +344,11 @@ export function App() {
329
344
  onAdd={openProgramModal}
330
345
  />
331
346
  </div>
332
- <div id="tokens" className="animate-fadeIn scroll-mt-24" style={{animationDelay: '0.3s'}}>
347
+ <div
348
+ id="tokens"
349
+ className="animate-fadeIn scroll-mt-24"
350
+ style={{ animationDelay: "0.3s" }}
351
+ >
333
352
  <TokensPanel
334
353
  tokens={tokens}
335
354
  loading={loadingTokens}
@@ -387,4 +406,4 @@ export function App() {
387
406
  `}</style>
388
407
  </div>
389
408
  );
390
- }
409
+ }
@@ -113,7 +113,7 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
113
113
  </span>
114
114
  </div>
115
115
  </div>
116
-
116
+
117
117
  <div className="grid gap-4 lg:grid-cols-3">
118
118
  <div className="space-y-2">
119
119
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
@@ -131,7 +131,7 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
131
131
  <i className="fas fa-user absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
132
132
  </div>
133
133
  </div>
134
-
134
+
135
135
  <div className="space-y-2">
136
136
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
137
137
  Asset
@@ -153,7 +153,7 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
153
153
  <i className="fas fa-coins absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
154
154
  </div>
155
155
  </div>
156
-
156
+
157
157
  <div className="space-y-2">
158
158
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
159
159
  Amount
@@ -177,12 +177,12 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
177
177
  </p>
178
178
  </div>
179
179
  </div>
180
-
180
+
181
181
  <div className="mt-6 flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
182
182
  <button
183
183
  type="submit"
184
184
  disabled={pending}
185
- className={`btn-primary flex-1 sm:flex-initial ${pending ? 'opacity-50 cursor-not-allowed' : ''}`}
185
+ className={`btn-primary flex-1 sm:flex-initial ${pending ? "opacity-50 cursor-not-allowed" : ""}`}
186
186
  >
187
187
  {pending ? (
188
188
  <>
@@ -191,26 +191,32 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
191
191
  </>
192
192
  ) : (
193
193
  <>
194
- <i className={`fas fa-${asset === SOL_OPTION.value ? 'parachute-box' : 'coins'}`}></i>
195
- <span>{asset === SOL_OPTION.value ? "Airdrop SOL" : "Mint Tokens"}</span>
194
+ <i
195
+ className={`fas fa-${asset === SOL_OPTION.value ? "parachute-box" : "coins"}`}
196
+ ></i>
197
+ <span>
198
+ {asset === SOL_OPTION.value ? "Airdrop SOL" : "Mint Tokens"}
199
+ </span>
196
200
  </>
197
201
  )}
198
202
  </button>
199
-
203
+
200
204
  {error && (
201
205
  <div className="flex-1 flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
202
206
  <i className="fas fa-exclamation-circle text-red-400"></i>
203
207
  <p className="text-sm text-red-300">{error}</p>
204
208
  </div>
205
209
  )}
206
-
210
+
207
211
  {message && (
208
212
  <div className="flex-1 flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/30">
209
213
  <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>
214
+ <p className="text-sm text-green-300 font-mono text-xs break-all">
215
+ {message}
216
+ </p>
211
217
  </div>
212
218
  )}
213
219
  </div>
214
220
  </form>
215
221
  );
216
- }
222
+ }
@@ -72,7 +72,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
72
72
  type="button"
73
73
  onClick={handleSubmit}
74
74
  disabled={pending || programId.trim().length === 0}
75
- className={`btn-primary ${(pending || programId.trim().length === 0) ? 'opacity-50 cursor-not-allowed' : ''}`}
75
+ className={`btn-primary ${pending || programId.trim().length === 0 ? "opacity-50 cursor-not-allowed" : ""}`}
76
76
  >
77
77
  {pending ? (
78
78
  <>
@@ -107,7 +107,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
107
107
  <i className="fas fa-fingerprint absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
108
108
  </div>
109
109
  </div>
110
-
110
+
111
111
  <div className="space-y-2">
112
112
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
113
113
  RPC Endpoint (Optional)
@@ -124,7 +124,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
124
124
  <i className="fas fa-globe absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
125
125
  </div>
126
126
  </div>
127
-
127
+
128
128
  <div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3">
129
129
  <label className="flex items-center gap-3 cursor-pointer group">
130
130
  <input
@@ -144,7 +144,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
144
144
  </p>
145
145
  </div>
146
146
  </label>
147
-
147
+
148
148
  {withAccounts && (
149
149
  <div className="ml-8 space-y-2 pt-2 border-t border-white/5">
150
150
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
@@ -170,7 +170,7 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
170
170
  </div>
171
171
  )}
172
172
  </div>
173
-
173
+
174
174
  {error && (
175
175
  <div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
176
176
  <i className="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
@@ -180,4 +180,4 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
180
180
  </div>
181
181
  </Modal>
182
182
  );
183
- }
183
+ }
@@ -77,7 +77,7 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
77
77
  type="button"
78
78
  onClick={handleSubmit}
79
79
  disabled={pending || mint.trim().length === 0}
80
- className={`btn-primary ${(pending || mint.trim().length === 0) ? 'opacity-50 cursor-not-allowed' : ''}`}
80
+ className={`btn-primary ${pending || mint.trim().length === 0 ? "opacity-50 cursor-not-allowed" : ""}`}
81
81
  >
82
82
  {pending ? (
83
83
  <>
@@ -112,7 +112,7 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
112
112
  <i className="fas fa-coin absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
113
113
  </div>
114
114
  </div>
115
-
115
+
116
116
  <div className="space-y-2">
117
117
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
118
118
  RPC Endpoint (Optional)
@@ -129,7 +129,7 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
129
129
  <i className="fas fa-globe absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
130
130
  </div>
131
131
  </div>
132
-
132
+
133
133
  <div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3">
134
134
  <label className="flex items-center gap-3 cursor-pointer group">
135
135
  <input
@@ -149,7 +149,7 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
149
149
  </p>
150
150
  </div>
151
151
  </label>
152
-
152
+
153
153
  {cloneAccounts && (
154
154
  <div className="ml-8 space-y-4 pt-3 border-t border-white/5">
155
155
  <label className="flex items-center gap-3 cursor-pointer group">
@@ -170,7 +170,7 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
170
170
  </p>
171
171
  </div>
172
172
  </label>
173
-
173
+
174
174
  {!allAccounts && (
175
175
  <div className="space-y-2">
176
176
  <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
@@ -198,7 +198,7 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
198
198
  </div>
199
199
  )}
200
200
  </div>
201
-
201
+
202
202
  {error && (
203
203
  <div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
204
204
  <i className="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
@@ -208,4 +208,4 @@ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
208
208
  </div>
209
209
  </Modal>
210
210
  );
211
- }
211
+ }
@@ -31,27 +31,29 @@ export function Modal({
31
31
  if (!isOpen) return null;
32
32
 
33
33
  const colorClasses = {
34
- purple: 'from-purple-500/20 to-violet-500/20 text-purple-400',
35
- blue: 'from-blue-500/20 to-cyan-500/20 text-blue-400',
36
- amber: 'from-amber-500/20 to-orange-500/20 text-amber-400',
37
- green: 'from-green-500/20 to-emerald-500/20 text-green-400',
34
+ purple: "from-purple-500/20 to-violet-500/20 text-purple-400",
35
+ blue: "from-blue-500/20 to-cyan-500/20 text-blue-400",
36
+ amber: "from-amber-500/20 to-orange-500/20 text-amber-400",
37
+ green: "from-green-500/20 to-emerald-500/20 text-green-400",
38
38
  };
39
39
 
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
+ <div
44
44
  className="absolute inset-0 bg-black/80 backdrop-blur-sm"
45
45
  onClick={onClose}
46
46
  />
47
-
47
+
48
48
  {/* Modal */}
49
49
  <div className="w-full max-w-lg p-0 relative animate-modalSlideIn overflow-hidden rounded-2xl border border-white/20 bg-gray-900/95 backdrop-blur-xl shadow-2xl">
50
50
  {/* Header */}
51
51
  <div className="p-6 border-b border-white/10 bg-gradient-to-r from-white/5 to-transparent">
52
52
  <div className="flex items-center justify-between">
53
53
  <div className="flex items-center gap-3">
54
- <div className={`w-10 h-10 rounded-xl bg-gradient-to-br ${colorClasses[iconColor as keyof typeof colorClasses]} flex items-center justify-center`}>
54
+ <div
55
+ className={`w-10 h-10 rounded-xl bg-gradient-to-br ${colorClasses[iconColor as keyof typeof colorClasses]} flex items-center justify-center`}
56
+ >
55
57
  <i className={`fas ${icon}`}></i>
56
58
  </div>
57
59
  <h3 className="text-xl font-bold text-white">{title}</h3>
@@ -66,12 +68,12 @@ export function Modal({
66
68
  </button>
67
69
  </div>
68
70
  </div>
69
-
71
+
70
72
  {/* Content */}
71
73
  <div className="p-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
72
74
  {children}
73
75
  </div>
74
-
76
+
75
77
  {/* Footer */}
76
78
  {footer && (
77
79
  <div className="p-6 border-t border-white/10 bg-gradient-to-r from-white/5 to-transparent">
@@ -79,7 +81,7 @@ export function Modal({
79
81
  </div>
80
82
  )}
81
83
  </div>
82
-
84
+
83
85
  <style jsx>{`
84
86
  @keyframes fadeIn {
85
87
  from {
@@ -124,4 +126,4 @@ export function Modal({
124
126
  `}</style>
125
127
  </div>
126
128
  );
127
- }
129
+ }
@@ -27,22 +27,20 @@ export function ProgramsPanel({ programs, loading, onRefresh, onAdd }: Props) {
27
27
  type="button"
28
28
  onClick={onRefresh}
29
29
  disabled={loading}
30
- className={`btn-secondary text-sm ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
30
+ className={`btn-secondary text-sm ${loading ? "opacity-50 cursor-not-allowed" : ""}`}
31
31
  >
32
- <i className={`fas fa-sync-alt ${loading ? 'animate-spin' : ''}`}></i>
32
+ <i
33
+ className={`fas fa-sync-alt ${loading ? "animate-spin" : ""}`}
34
+ ></i>
33
35
  <span>{loading ? "Refreshing" : "Refresh"}</span>
34
36
  </button>
35
- <button
36
- type="button"
37
- onClick={onAdd}
38
- className="btn-primary text-sm"
39
- >
37
+ <button type="button" onClick={onAdd} className="btn-primary text-sm">
40
38
  <i className="fas fa-plus"></i>
41
39
  <span>Add Program</span>
42
40
  </button>
43
41
  </div>
44
42
  </header>
45
-
43
+
46
44
  <div className="overflow-x-auto rounded-xl">
47
45
  {programs.length === 0 ? (
48
46
  <div className="flex flex-col items-center justify-center py-12 text-center">
@@ -50,7 +48,9 @@ export function ProgramsPanel({ programs, loading, onRefresh, onAdd }: Props) {
50
48
  <i className="fas fa-code text-blue-500 text-2xl"></i>
51
49
  </div>
52
50
  <p className="text-gray-400 mb-2">No programs deployed</p>
53
- <p className="text-sm text-gray-500">Click "Add Program" to clone from mainnet</p>
51
+ <p className="text-sm text-gray-500">
52
+ Click "Add Program" to clone from mainnet
53
+ </p>
54
54
  </div>
55
55
  ) : (
56
56
  <table className="table-modern">
@@ -65,12 +65,17 @@ export function ProgramsPanel({ programs, loading, onRefresh, onAdd }: Props) {
65
65
  </thead>
66
66
  <tbody>
67
67
  {programs.map((program, index) => (
68
- <tr key={program.programId} style={{animationDelay: `${index * 50}ms`}} className="animate-fadeIn">
68
+ <tr
69
+ key={program.programId}
70
+ style={{ animationDelay: `${index * 50}ms` }}
71
+ className="animate-fadeIn"
72
+ >
69
73
  <td>
70
74
  <div className="flex items-center gap-2">
71
75
  <i className="fas fa-cube text-blue-400 text-xs"></i>
72
76
  <span className="font-mono text-xs text-blue-300">
73
- {program.programId.slice(0, 8)}...{program.programId.slice(-6)}
77
+ {program.programId.slice(0, 8)}...
78
+ {program.programId.slice(-6)}
74
79
  </span>
75
80
  </div>
76
81
  </td>
@@ -80,9 +85,15 @@ export function ProgramsPanel({ programs, loading, onRefresh, onAdd }: Props) {
80
85
  </span>
81
86
  </td>
82
87
  <td>
83
- <span className={`badge ${program.executable ? 'badge-success' : 'badge-warning'}`}>
84
- <i className={`fas fa-${program.executable ? 'check' : 'pause'} text-xs`}></i>
85
- <span>{program.executable ? "Executable" : "Data Only"}</span>
88
+ <span
89
+ className={`badge ${program.executable ? "badge-success" : "badge-warning"}`}
90
+ >
91
+ <i
92
+ className={`fas fa-${program.executable ? "check" : "pause"} text-xs`}
93
+ ></i>
94
+ <span>
95
+ {program.executable ? "Executable" : "Data Only"}
96
+ </span>
86
97
  </span>
87
98
  </td>
88
99
  <td>
@@ -97,7 +108,8 @@ export function ProgramsPanel({ programs, loading, onRefresh, onAdd }: Props) {
97
108
  <div className="flex items-center gap-2">
98
109
  <i className="fas fa-wallet text-gray-400 text-xs"></i>
99
110
  <span className="text-gray-300">
100
- {(Number(BigInt(program.lamports)) / 1e9).toFixed(4)} SOL
111
+ {(Number(BigInt(program.lamports)) / 1e9).toFixed(4)}{" "}
112
+ SOL
101
113
  </span>
102
114
  </div>
103
115
  </td>