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.
- package/README.md +471 -79
- package/cli.cjs +106 -78
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.cjs +69 -61
- package/server/lib/base58.ts +1 -1
- package/server/methods/account/get-account-info.ts +3 -7
- package/server/methods/account/get-balance.ts +3 -7
- package/server/methods/account/get-multiple-accounts.ts +2 -1
- package/server/methods/account/get-parsed-account-info.ts +3 -7
- package/server/methods/account/parsers/index.ts +2 -2
- package/server/methods/account/parsers/loader-upgradeable.ts +14 -1
- package/server/methods/account/parsers/spl-token.ts +29 -10
- package/server/methods/account/request-airdrop.ts +44 -31
- package/server/methods/block/get-block.ts +3 -7
- package/server/methods/block/get-blocks-with-limit.ts +3 -7
- package/server/methods/block/is-blockhash-valid.ts +3 -7
- package/server/methods/get-address-lookup-table.ts +3 -7
- package/server/methods/program/get-program-accounts.ts +9 -9
- package/server/methods/program/get-token-account-balance.ts +3 -7
- package/server/methods/program/get-token-accounts-by-delegate.ts +4 -3
- package/server/methods/program/get-token-accounts-by-owner.ts +61 -35
- package/server/methods/program/get-token-largest-accounts.ts +3 -2
- package/server/methods/program/get-token-supply.ts +3 -2
- package/server/methods/solforge/index.ts +9 -6
- package/server/methods/transaction/get-parsed-transaction.ts +3 -7
- package/server/methods/transaction/get-signature-statuses.ts +14 -7
- package/server/methods/transaction/get-signatures-for-address.ts +3 -7
- package/server/methods/transaction/get-transaction.ts +167 -81
- package/server/methods/transaction/send-transaction.ts +29 -16
- package/server/methods/transaction/simulate-transaction.ts +3 -2
- package/server/rpc-server.ts +47 -34
- package/server/types.ts +9 -6
- package/server/ws-server.ts +15 -8
- package/src/api-server-entry.ts +91 -91
- package/src/cli/commands/airdrop.ts +2 -2
- package/src/cli/commands/config.ts +2 -2
- package/src/cli/commands/mint.ts +3 -3
- package/src/cli/commands/program-clone.ts +9 -11
- package/src/cli/commands/program-load.ts +3 -3
- package/src/cli/commands/rpc-start.ts +8 -5
- package/src/cli/commands/token-adopt-authority.ts +1 -1
- package/src/cli/commands/token-clone.ts +5 -6
- package/src/cli/commands/token-create.ts +5 -5
- package/src/cli/main.ts +38 -37
- package/src/cli/run-solforge.ts +20 -6
- package/src/cli/setup-wizard.ts +8 -6
- package/src/commands/add-program.ts +324 -328
- package/src/commands/init.ts +106 -106
- package/src/commands/list.ts +125 -125
- package/src/commands/mint.ts +247 -248
- package/src/commands/start.ts +837 -833
- package/src/commands/status.ts +80 -80
- package/src/commands/stop.ts +381 -382
- package/src/config/index.ts +33 -17
- package/src/config/manager.ts +150 -150
- package/src/db/index.ts +2 -2
- package/src/db/tx-store.ts +12 -8
- package/src/gui/public/app.css +1556 -1
- package/src/gui/public/build/main.css +1569 -1
- package/src/gui/server.ts +21 -22
- package/src/gui/src/api.ts +1 -1
- package/src/gui/src/app.tsx +96 -45
- package/src/gui/src/components/airdrop-mint-form.tsx +49 -19
- package/src/gui/src/components/clone-program-modal.tsx +31 -12
- package/src/gui/src/components/clone-token-modal.tsx +32 -13
- package/src/gui/src/components/modal.tsx +18 -11
- package/src/gui/src/components/programs-panel.tsx +27 -15
- package/src/gui/src/components/status-panel.tsx +32 -18
- package/src/gui/src/components/tokens-panel.tsx +25 -19
- package/src/gui/src/index.css +491 -463
- package/src/index.ts +177 -149
- package/src/rpc/start.ts +1 -1
- package/src/services/api-server.ts +494 -475
- package/src/services/port-manager.ts +164 -167
- package/src/services/process-registry.ts +144 -145
- package/src/services/program-cloner.ts +312 -312
- package/src/services/token-cloner.ts +799 -797
- package/src/services/validator.ts +288 -290
- package/src/types/config.ts +72 -72
- package/src/utils/shell.ts +75 -75
- 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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
new Response(file(fpath), {
|
|
50
|
+
headers: { ...CORS, "Content-Type": "text/css" },
|
|
51
|
+
});
|
|
53
52
|
const js = (fpath: string) =>
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
}
|
package/src/gui/src/api.ts
CHANGED
|
@@ -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:
|
|
75
|
+
let payload: unknown = null;
|
|
76
76
|
const text = await response.text();
|
|
77
77
|
if (text) {
|
|
78
78
|
try {
|
package/src/gui/src/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
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
|
|
44
|
-
|
|
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
|
|
54
|
-
|
|
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
|
|
66
|
-
|
|
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
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
174
|
+
<i
|
|
175
|
+
className={`fas fa-${sidebarOpen ? "times" : "bars"} text-white`}
|
|
176
|
+
></i>
|
|
159
177
|
</button>
|
|
160
178
|
|
|
161
179
|
{/* Sidebar Navigation */}
|
|
162
|
-
<aside
|
|
163
|
-
|
|
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
|
-
|
|
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 ===
|
|
185
|
-
?
|
|
186
|
-
:
|
|
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
|
-
|
|
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 ===
|
|
196
|
-
?
|
|
197
|
-
:
|
|
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
|
-
|
|
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 ===
|
|
207
|
-
?
|
|
208
|
-
:
|
|
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
|
-
|
|
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 ===
|
|
218
|
-
?
|
|
219
|
-
:
|
|
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">
|
|
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">
|
|
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
|
-
<
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 |
|
|
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 |
|
|
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
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
195
|
-
|
|
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">
|
|
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:
|
|
41
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|