solforge 0.1.7 → 0.2.1

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 (151) hide show
  1. package/README.md +367 -393
  2. package/docs/API.md +379 -0
  3. package/docs/CONFIGURATION.md +407 -0
  4. package/docs/bun-single-file-executable.md +585 -0
  5. package/docs/cli-plan.md +154 -0
  6. package/docs/data-indexing-plan.md +214 -0
  7. package/docs/gui-roadmap.md +202 -0
  8. package/package.json +38 -51
  9. package/server/index.ts +5 -0
  10. package/server/lib/base58.ts +33 -0
  11. package/server/lib/faucet.ts +110 -0
  12. package/server/lib/spl-token.ts +57 -0
  13. package/server/methods/TEMPLATE.md +117 -0
  14. package/server/methods/account/get-account-info.ts +90 -0
  15. package/server/methods/account/get-balance.ts +27 -0
  16. package/server/methods/account/get-multiple-accounts.ts +83 -0
  17. package/server/methods/account/get-parsed-account-info.ts +21 -0
  18. package/server/methods/account/index.ts +12 -0
  19. package/server/methods/account/parsers/index.ts +52 -0
  20. package/server/methods/account/parsers/loader-upgradeable.ts +66 -0
  21. package/server/methods/account/parsers/spl-token.ts +237 -0
  22. package/server/methods/account/parsers/system.ts +4 -0
  23. package/server/methods/account/request-airdrop.ts +219 -0
  24. package/server/methods/admin/adopt-mint-authority.ts +94 -0
  25. package/server/methods/admin/clone-program-accounts.ts +55 -0
  26. package/server/methods/admin/clone-program.ts +152 -0
  27. package/server/methods/admin/clone-token-accounts.ts +117 -0
  28. package/server/methods/admin/clone-token-mint.ts +82 -0
  29. package/server/methods/admin/create-mint.ts +114 -0
  30. package/server/methods/admin/create-token-account.ts +137 -0
  31. package/server/methods/admin/helpers.ts +70 -0
  32. package/server/methods/admin/index.ts +10 -0
  33. package/server/methods/admin/list-mints.ts +21 -0
  34. package/server/methods/admin/load-program.ts +52 -0
  35. package/server/methods/admin/mint-to.ts +278 -0
  36. package/server/methods/block/get-block-height.ts +5 -0
  37. package/server/methods/block/get-block.ts +35 -0
  38. package/server/methods/block/get-blocks-with-limit.ts +23 -0
  39. package/server/methods/block/get-latest-blockhash.ts +12 -0
  40. package/server/methods/block/get-slot.ts +5 -0
  41. package/server/methods/block/index.ts +6 -0
  42. package/server/methods/block/is-blockhash-valid.ts +23 -0
  43. package/server/methods/epoch/get-cluster-nodes.ts +17 -0
  44. package/server/methods/epoch/get-epoch-info.ts +16 -0
  45. package/server/methods/epoch/get-epoch-schedule.ts +15 -0
  46. package/server/methods/epoch/get-highest-snapshot-slot.ts +9 -0
  47. package/server/methods/epoch/get-leader-schedule.ts +8 -0
  48. package/server/methods/epoch/get-max-retransmit-slot.ts +9 -0
  49. package/server/methods/epoch/get-max-shred-insert-slot.ts +9 -0
  50. package/server/methods/epoch/get-slot-leader.ts +6 -0
  51. package/server/methods/epoch/get-slot-leaders.ts +9 -0
  52. package/server/methods/epoch/get-stake-activation.ts +9 -0
  53. package/server/methods/epoch/get-stake-minimum-delegation.ts +9 -0
  54. package/server/methods/epoch/get-vote-accounts.ts +19 -0
  55. package/server/methods/epoch/index.ts +13 -0
  56. package/server/methods/epoch/minimum-ledger-slot.ts +5 -0
  57. package/server/methods/fee/get-fee-calculator-for-blockhash.ts +12 -0
  58. package/server/methods/fee/get-fee-for-message.ts +8 -0
  59. package/server/methods/fee/get-fee-rate-governor.ts +16 -0
  60. package/server/methods/fee/get-fees.ts +14 -0
  61. package/server/methods/fee/get-recent-prioritization-fees.ts +22 -0
  62. package/server/methods/fee/index.ts +5 -0
  63. package/server/methods/get-address-lookup-table.ts +31 -0
  64. package/server/methods/index.ts +265 -0
  65. package/server/methods/performance/get-recent-performance-samples.ts +25 -0
  66. package/server/methods/performance/get-transaction-count.ts +5 -0
  67. package/server/methods/performance/index.ts +2 -0
  68. package/server/methods/program/get-block-commitment.ts +9 -0
  69. package/server/methods/program/get-block-production.ts +14 -0
  70. package/server/methods/program/get-block-time.ts +21 -0
  71. package/server/methods/program/get-blocks.ts +11 -0
  72. package/server/methods/program/get-first-available-block.ts +9 -0
  73. package/server/methods/program/get-genesis-hash.ts +6 -0
  74. package/server/methods/program/get-identity.ts +6 -0
  75. package/server/methods/program/get-inflation-governor.ts +15 -0
  76. package/server/methods/program/get-inflation-rate.ts +10 -0
  77. package/server/methods/program/get-inflation-reward.ts +12 -0
  78. package/server/methods/program/get-largest-accounts.ts +8 -0
  79. package/server/methods/program/get-parsed-program-accounts.ts +12 -0
  80. package/server/methods/program/get-parsed-token-accounts-by-delegate.ts +12 -0
  81. package/server/methods/program/get-parsed-token-accounts-by-owner.ts +12 -0
  82. package/server/methods/program/get-program-accounts.ts +221 -0
  83. package/server/methods/program/get-supply.ts +13 -0
  84. package/server/methods/program/get-token-account-balance.ts +64 -0
  85. package/server/methods/program/get-token-accounts-by-delegate.ts +81 -0
  86. package/server/methods/program/get-token-accounts-by-owner.ts +390 -0
  87. package/server/methods/program/get-token-largest-accounts.ts +80 -0
  88. package/server/methods/program/get-token-supply.ts +38 -0
  89. package/server/methods/program/index.ts +21 -0
  90. package/server/methods/solforge/index.ts +155 -0
  91. package/server/methods/system/get-health.ts +5 -0
  92. package/server/methods/system/get-minimum-balance-for-rent-exemption.ts +13 -0
  93. package/server/methods/system/get-version.ts +9 -0
  94. package/server/methods/system/index.ts +3 -0
  95. package/server/methods/transaction/get-confirmed-transaction.ts +11 -0
  96. package/server/methods/transaction/get-parsed-transaction.ts +21 -0
  97. package/server/methods/transaction/get-signature-statuses.ts +72 -0
  98. package/server/methods/transaction/get-signatures-for-address.ts +45 -0
  99. package/server/methods/transaction/get-transaction.ts +428 -0
  100. package/server/methods/transaction/index.ts +7 -0
  101. package/server/methods/transaction/send-transaction.ts +232 -0
  102. package/server/methods/transaction/simulate-transaction.ts +56 -0
  103. package/server/rpc-server.ts +474 -0
  104. package/server/types.ts +74 -0
  105. package/server/ws-server.ts +171 -0
  106. package/src/cli/bootstrap.ts +67 -0
  107. package/src/cli/commands/airdrop.ts +37 -0
  108. package/src/cli/commands/config.ts +39 -0
  109. package/src/cli/commands/mint.ts +187 -0
  110. package/src/cli/commands/program-clone.ts +124 -0
  111. package/src/cli/commands/program-load.ts +64 -0
  112. package/src/cli/commands/rpc-start.ts +46 -0
  113. package/src/cli/commands/token-adopt-authority.ts +37 -0
  114. package/src/cli/commands/token-clone.ts +113 -0
  115. package/src/cli/commands/token-create.ts +81 -0
  116. package/src/cli/main.ts +130 -0
  117. package/src/cli/run-solforge.ts +98 -0
  118. package/src/cli/setup-utils.ts +54 -0
  119. package/src/cli/setup-wizard.ts +256 -0
  120. package/src/cli/utils/args.ts +15 -0
  121. package/src/config/index.ts +130 -0
  122. package/src/db/index.ts +83 -0
  123. package/src/db/schema/accounts.ts +23 -0
  124. package/src/db/schema/address-signatures.ts +31 -0
  125. package/src/db/schema/index.ts +5 -0
  126. package/src/db/schema/meta-kv.ts +9 -0
  127. package/src/db/schema/transactions.ts +29 -0
  128. package/src/db/schema/tx-accounts.ts +33 -0
  129. package/src/db/tx-store.ts +229 -0
  130. package/src/gui/public/app.css +1 -0
  131. package/src/gui/public/build/main.css +1 -0
  132. package/src/gui/public/build/main.js +303 -0
  133. package/src/gui/public/build/main.js.txt +231 -0
  134. package/src/gui/public/index.html +19 -0
  135. package/src/gui/server.ts +297 -0
  136. package/src/gui/src/api.ts +127 -0
  137. package/src/gui/src/app.tsx +390 -0
  138. package/src/gui/src/components/airdrop-mint-form.tsx +216 -0
  139. package/src/gui/src/components/clone-program-modal.tsx +183 -0
  140. package/src/gui/src/components/clone-token-modal.tsx +211 -0
  141. package/src/gui/src/components/modal.tsx +127 -0
  142. package/src/gui/src/components/programs-panel.tsx +112 -0
  143. package/src/gui/src/components/status-panel.tsx +122 -0
  144. package/src/gui/src/components/tokens-panel.tsx +116 -0
  145. package/src/gui/src/hooks/use-interval.ts +17 -0
  146. package/src/gui/src/index.css +529 -0
  147. package/src/gui/src/main.tsx +17 -0
  148. package/src/migrations-bundled.ts +17 -0
  149. package/src/rpc/start.ts +44 -0
  150. package/scripts/postinstall.cjs +0 -103
  151. package/tsconfig.json +0 -28
@@ -0,0 +1,390 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import {
3
+ type ApiConfig,
4
+ type ApiStatus,
5
+ cloneProgram,
6
+ cloneToken,
7
+ fetchConfig,
8
+ fetchPrograms,
9
+ fetchStatus,
10
+ fetchTokens,
11
+ type ProgramSummary,
12
+ submitAirdrop,
13
+ submitMint,
14
+ type TokenSummary,
15
+ } from "./api";
16
+ import { AirdropMintForm } from "./components/airdrop-mint-form";
17
+ import { CloneProgramModal } from "./components/clone-program-modal";
18
+ import { CloneTokenModal } from "./components/clone-token-modal";
19
+ import { ProgramsPanel } from "./components/programs-panel";
20
+ import { StatusPanel } from "./components/status-panel";
21
+ import { TokensPanel } from "./components/tokens-panel";
22
+ import { useInterval } from "./hooks/use-interval";
23
+
24
+ export function App() {
25
+ const [config, setConfig] = useState<ApiConfig | null>(null);
26
+ const [status, setStatus] = useState<ApiStatus | null>(null);
27
+ const [programs, setPrograms] = useState<ProgramSummary[]>([]);
28
+ const [tokens, setTokens] = useState<TokenSummary[]>([]);
29
+ const [loadingStatus, setLoadingStatus] = useState(false);
30
+ const [loadingPrograms, setLoadingPrograms] = useState(false);
31
+ const [loadingTokens, setLoadingTokens] = useState(false);
32
+ const [programModalOpen, setProgramModalOpen] = useState(false);
33
+ const [tokenModalOpen, setTokenModalOpen] = useState(false);
34
+ const [bannerError, setBannerError] = useState<string | null>(null);
35
+ const [sidebarOpen, setSidebarOpen] = useState(false);
36
+ const [activeSection, setActiveSection] = useState("status");
37
+
38
+ const loadConfig = useCallback(async () => {
39
+ try {
40
+ const cfg = await fetchConfig();
41
+ setConfig(cfg);
42
+ setBannerError(null);
43
+ } catch (error: any) {
44
+ setBannerError(error?.message ?? String(error));
45
+ }
46
+ }, []);
47
+
48
+ const loadStatus = useCallback(async () => {
49
+ setLoadingStatus(true);
50
+ try {
51
+ const data = await fetchStatus();
52
+ setStatus(data);
53
+ } catch (error: any) {
54
+ setBannerError(error?.message ?? String(error));
55
+ } finally {
56
+ setLoadingStatus(false);
57
+ }
58
+ }, []);
59
+
60
+ const loadPrograms = useCallback(async () => {
61
+ setLoadingPrograms(true);
62
+ try {
63
+ const data = await fetchPrograms();
64
+ setPrograms(data);
65
+ } catch (error: any) {
66
+ setBannerError(error?.message ?? String(error));
67
+ } finally {
68
+ setLoadingPrograms(false);
69
+ }
70
+ }, []);
71
+
72
+ const loadTokens = useCallback(async () => {
73
+ setLoadingTokens(true);
74
+ try {
75
+ const data = await fetchTokens();
76
+ setTokens(data);
77
+ } catch (error: any) {
78
+ setBannerError(error?.message ?? String(error));
79
+ } finally {
80
+ setLoadingTokens(false);
81
+ }
82
+ }, []);
83
+
84
+ useEffect(() => {
85
+ loadConfig();
86
+ loadStatus();
87
+ loadPrograms();
88
+ loadTokens();
89
+ }, [loadConfig, loadStatus, loadPrograms, loadTokens]);
90
+
91
+ useInterval(loadStatus, 5_000);
92
+
93
+ const onAirdrop = useCallback(
94
+ async (address: string, lamports: string) => {
95
+ const result = await submitAirdrop({ address, lamports });
96
+ await loadStatus();
97
+ return result.signature;
98
+ },
99
+ [loadStatus],
100
+ );
101
+
102
+ const onMint = useCallback(
103
+ async (mint: string, owner: string, amountRaw: string) => {
104
+ const result = await submitMint({ mint, owner, amountRaw });
105
+ await Promise.all([loadStatus(), loadTokens()]);
106
+ if (result && typeof result === "object" && "signature" in result) {
107
+ return (result as { signature?: string }).signature;
108
+ }
109
+ return undefined;
110
+ },
111
+ [loadStatus, loadTokens],
112
+ );
113
+
114
+ const openProgramModal = () => setProgramModalOpen(true);
115
+ const openTokenModal = () => setTokenModalOpen(true);
116
+
117
+ const handleCloneProgram = useCallback(
118
+ async (payload: {
119
+ programId: string;
120
+ endpoint?: string;
121
+ withAccounts: boolean;
122
+ accountsLimit?: number;
123
+ }) => {
124
+ await cloneProgram(payload);
125
+ await loadPrograms();
126
+ },
127
+ [loadPrograms],
128
+ );
129
+
130
+ const handleCloneToken = useCallback(
131
+ async (payload: {
132
+ mint: string;
133
+ endpoint?: string;
134
+ cloneAccounts: boolean;
135
+ holders?: number;
136
+ allAccounts?: boolean;
137
+ }) => {
138
+ await cloneToken(payload);
139
+ await loadTokens();
140
+ },
141
+ [loadTokens],
142
+ );
143
+
144
+ const scrollToSection = (sectionId: string) => {
145
+ setActiveSection(sectionId);
146
+ document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' });
147
+ setSidebarOpen(false);
148
+ };
149
+
150
+ return (
151
+ <div className="min-h-screen relative">
152
+ {/* Mobile Menu Button */}
153
+ <button
154
+ onClick={() => setSidebarOpen(!sidebarOpen)}
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
+ aria-label="Menu"
157
+ >
158
+ <i className={`fas fa-${sidebarOpen ? 'times' : 'bars'} text-white`}></i>
159
+ </button>
160
+
161
+ {/* 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
+ }`}>
165
+ <div className="p-6 space-y-8">
166
+ {/* Logo */}
167
+ <div className="flex items-center gap-3">
168
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-600 to-violet-600 flex items-center justify-center shadow-lg">
169
+ <i className="fas fa-fire text-white text-xl"></i>
170
+ </div>
171
+ <div>
172
+ <h1 className="text-xl font-bold bg-gradient-to-r from-purple-400 to-violet-400 bg-clip-text text-transparent">
173
+ SolForge
174
+ </h1>
175
+ <p className="text-xs text-gray-500">Development Suite</p>
176
+ </div>
177
+ </div>
178
+
179
+ {/* Navigation Items */}
180
+ <nav className="space-y-2">
181
+ <button
182
+ onClick={() => scrollToSection('status')}
183
+ 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'
187
+ }`}
188
+ >
189
+ <i className="fas fa-server w-5"></i>
190
+ <span className="font-medium">Network Status</span>
191
+ </button>
192
+ <button
193
+ onClick={() => scrollToSection('actions')}
194
+ 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'
198
+ }`}
199
+ >
200
+ <i className="fas fa-paper-plane w-5"></i>
201
+ <span className="font-medium">Quick Actions</span>
202
+ </button>
203
+ <button
204
+ onClick={() => scrollToSection('programs')}
205
+ 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'
209
+ }`}
210
+ >
211
+ <i className="fas fa-code w-5"></i>
212
+ <span className="font-medium">Programs</span>
213
+ </button>
214
+ <button
215
+ onClick={() => scrollToSection('tokens')}
216
+ 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'
220
+ }`}
221
+ >
222
+ <i className="fas fa-coins w-5"></i>
223
+ <span className="font-medium">Tokens</span>
224
+ </button>
225
+ </nav>
226
+
227
+ {/* Quick Stats */}
228
+ {config && (
229
+ <div className="space-y-3">
230
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Connection</h3>
231
+ <div className="p-3 rounded-xl bg-white/5 border border-white/10">
232
+ <div className="flex items-center gap-2 mb-2">
233
+ <span className="status-dot online"></span>
234
+ <span className="text-xs text-gray-400">Connected</span>
235
+ </div>
236
+ <p className="text-xs text-gray-500 font-mono break-all">{config.rpcUrl}</p>
237
+ </div>
238
+ </div>
239
+ )}
240
+ </div>
241
+ </aside>
242
+
243
+ {/* Overlay for mobile */}
244
+ {sidebarOpen && (
245
+ <div
246
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-30 lg:hidden"
247
+ onClick={() => setSidebarOpen(false)}
248
+ />
249
+ )}
250
+
251
+ {/* Main Content */}
252
+ <main className={`transition-all duration-300 lg:ml-72 p-4 md:p-8`}>
253
+ <div className="max-w-7xl mx-auto space-y-6">
254
+ {/* Header - Only show on desktop */}
255
+ <header className="glass-panel p-6 animate-fadeIn hidden md:block">
256
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
257
+ <div>
258
+ <h2 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-violet-400 bg-clip-text text-transparent">
259
+ SolForge Dashboard
260
+ </h2>
261
+ <p className="text-gray-400 mt-1">
262
+ Manage your local Solana development environment
263
+ </p>
264
+ </div>
265
+ <button
266
+ onClick={loadStatus}
267
+ className="btn-secondary"
268
+ >
269
+ <i className={`fas fa-sync-alt ${loadingStatus ? 'animate-spin' : ''}`}></i>
270
+ <span>Refresh All</span>
271
+ </button>
272
+ </div>
273
+
274
+ {/* Error Banner */}
275
+ {bannerError && (
276
+ <div className="mt-6 p-4 rounded-xl bg-gradient-to-r from-red-500/10 to-pink-500/10 border border-red-500/30 flex items-start gap-3 animate-slideIn">
277
+ <i className="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
278
+ <div className="flex-1">
279
+ <p className="text-sm text-red-300">{bannerError}</p>
280
+ </div>
281
+ <button
282
+ onClick={() => setBannerError(null)}
283
+ className="text-red-400 hover:text-red-300"
284
+ aria-label="Close error"
285
+ >
286
+ <i className="fas fa-times"></i>
287
+ </button>
288
+ </div>
289
+ )}
290
+ </header>
291
+
292
+ {/* Mobile Header - Simpler version */}
293
+ <div className="md:hidden mb-4">
294
+ <h2 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-violet-400 bg-clip-text text-transparent">
295
+ SolForge
296
+ </h2>
297
+ {bannerError && (
298
+ <div className="mt-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
299
+ <p className="text-xs text-red-300">{bannerError}</p>
300
+ </div>
301
+ )}
302
+ </div>
303
+
304
+ {/* Status Panel */}
305
+ <div id="status" className="animate-fadeIn scroll-mt-24">
306
+ <StatusPanel
307
+ status={status}
308
+ loading={loadingStatus}
309
+ onRefresh={loadStatus}
310
+ />
311
+ </div>
312
+
313
+ {/* Quick Actions - Optional */}
314
+ <div id="actions" className="glass-panel p-6 animate-fadeIn scroll-mt-24" style={{animationDelay: '0.1s'}}>
315
+ <AirdropMintForm
316
+ tokens={tokens}
317
+ onAirdrop={onAirdrop}
318
+ onMint={onMint}
319
+ />
320
+ </div>
321
+
322
+ {/* Programs and Tokens Stacked */}
323
+ <div className="space-y-6">
324
+ <div id="programs" className="animate-fadeIn scroll-mt-24" style={{animationDelay: '0.2s'}}>
325
+ <ProgramsPanel
326
+ programs={programs}
327
+ loading={loadingPrograms}
328
+ onRefresh={loadPrograms}
329
+ onAdd={openProgramModal}
330
+ />
331
+ </div>
332
+ <div id="tokens" className="animate-fadeIn scroll-mt-24" style={{animationDelay: '0.3s'}}>
333
+ <TokensPanel
334
+ tokens={tokens}
335
+ loading={loadingTokens}
336
+ onRefresh={loadTokens}
337
+ onAdd={openTokenModal}
338
+ />
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </main>
343
+
344
+ {/* Modals */}
345
+ <CloneProgramModal
346
+ isOpen={programModalOpen}
347
+ onClose={() => setProgramModalOpen(false)}
348
+ onSubmit={handleCloneProgram}
349
+ />
350
+ <CloneTokenModal
351
+ isOpen={tokenModalOpen}
352
+ onClose={() => setTokenModalOpen(false)}
353
+ onSubmit={handleCloneToken}
354
+ />
355
+
356
+ <style jsx>{`
357
+ @keyframes fadeIn {
358
+ from {
359
+ opacity: 0;
360
+ transform: translateY(20px);
361
+ }
362
+ to {
363
+ opacity: 1;
364
+ transform: translateY(0);
365
+ }
366
+ }
367
+
368
+ @keyframes slideIn {
369
+ from {
370
+ opacity: 0;
371
+ transform: translateX(-20px);
372
+ }
373
+ to {
374
+ opacity: 1;
375
+ transform: translateX(0);
376
+ }
377
+ }
378
+
379
+ .animate-fadeIn {
380
+ animation: fadeIn 0.6s ease-out forwards;
381
+ opacity: 0;
382
+ }
383
+
384
+ .animate-slideIn {
385
+ animation: slideIn 0.4s ease-out forwards;
386
+ }
387
+ `}</style>
388
+ </div>
389
+ );
390
+ }
@@ -0,0 +1,216 @@
1
+ import { type ChangeEvent, type FormEvent, useMemo, useState } from "react";
2
+ import type { TokenSummary } from "../api";
3
+
4
+ interface Props {
5
+ tokens: TokenSummary[];
6
+ onAirdrop: (address: string, lamports: string) => Promise<string | void>;
7
+ onMint: (
8
+ mint: string,
9
+ owner: string,
10
+ amountRaw: string,
11
+ ) => Promise<string | void>;
12
+ }
13
+
14
+ const SOL_OPTION = {
15
+ value: "SOL",
16
+ label: "SOL (Lamports)",
17
+ decimals: 9,
18
+ } as const;
19
+
20
+ const BIGINT_TEN = 10n;
21
+
22
+ function toBaseUnits(rawInput: string, decimals: number) {
23
+ const input = rawInput.trim();
24
+ if (!input) throw new Error("Amount is required");
25
+ const negative = input.startsWith("-");
26
+ if (negative) throw new Error("Amount must be positive");
27
+ const [wholeRaw = "0", fracRaw = ""] = input.split(".");
28
+ const whole = wholeRaw.replace(/[^0-9]/g, "") || "0";
29
+ const fracClean = fracRaw.replace(/[^0-9]/g, "");
30
+ if (fracClean.length > decimals)
31
+ throw new Error(`Too many decimal places (max ${decimals})`);
32
+ const scale = BIGINT_TEN ** BigInt(decimals);
33
+ const wholeValue = BigInt(whole);
34
+ const fracPadded = decimals === 0 ? "0" : fracClean.padEnd(decimals, "0");
35
+ const fracValue = BigInt(fracPadded || "0");
36
+ const total = wholeValue * scale + fracValue;
37
+ if (total <= 0n) throw new Error("Amount must be greater than zero");
38
+ return total.toString();
39
+ }
40
+
41
+ function formatTokenLabel(token: TokenSummary) {
42
+ const suffix = token.mintAuthority
43
+ ? `Authority ${token.mintAuthority.slice(0, 6)}…`
44
+ : "No authority";
45
+ return `${token.mint.slice(0, 6)}…${token.mint.slice(-4)} · ${token.decimals} dec · ${suffix}`;
46
+ }
47
+
48
+ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
49
+ const [asset, setAsset] = useState<string>(SOL_OPTION.value);
50
+ const [recipient, setRecipient] = useState<string>("");
51
+ const [amount, setAmount] = useState<string>("1");
52
+ const [pending, setPending] = useState(false);
53
+ const [error, setError] = useState<string | null>(null);
54
+ const [message, setMessage] = useState<string | null>(null);
55
+
56
+ const options = useMemo(() => {
57
+ const tokenOpts = tokens.map((token) => ({
58
+ value: token.mint,
59
+ label: formatTokenLabel(token),
60
+ decimals: token.decimals,
61
+ }));
62
+ return [SOL_OPTION, ...tokenOpts];
63
+ }, [tokens]);
64
+
65
+ const selected = options.find((opt) => opt.value === asset) ?? SOL_OPTION;
66
+
67
+ const submit = async () => {
68
+ if (!recipient.trim()) throw new Error("Recipient address is required");
69
+ const canonicalRecipient = recipient.trim();
70
+ if (asset === SOL_OPTION.value) {
71
+ const lamports = toBaseUnits(amount, SOL_OPTION.decimals);
72
+ const signature = await onAirdrop(canonicalRecipient, lamports);
73
+ return signature
74
+ ? `Airdrop signature: ${signature}`
75
+ : "Airdrop submitted";
76
+ }
77
+ const raw = toBaseUnits(amount, selected.decimals);
78
+ const signature = await onMint(asset, canonicalRecipient, raw);
79
+ return signature ? `Mint signature: ${signature}` : "Mint submitted";
80
+ };
81
+
82
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
83
+ event.preventDefault();
84
+ setPending(true);
85
+ setError(null);
86
+ setMessage(null);
87
+ try {
88
+ const note = await submit();
89
+ setMessage(note);
90
+ } catch (err: any) {
91
+ setError(err?.message ?? String(err));
92
+ } finally {
93
+ setPending(false);
94
+ }
95
+ };
96
+
97
+ return (
98
+ <form onSubmit={handleSubmit}>
99
+ <div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4 mb-6">
100
+ <div className="flex items-center gap-3">
101
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500/20 to-purple-500/20 flex items-center justify-center">
102
+ <i className="fas fa-paper-plane text-violet-400"></i>
103
+ </div>
104
+ <div>
105
+ <h2 className="text-xl font-bold text-white">Quick Actions</h2>
106
+ <p className="text-xs text-gray-500">Airdrop SOL or mint tokens</p>
107
+ </div>
108
+ </div>
109
+ <div className="flex items-center gap-2">
110
+ <span className="badge badge-info">
111
+ <i className="fas fa-bolt text-xs"></i>
112
+ <span>Faucet Powered</span>
113
+ </span>
114
+ </div>
115
+ </div>
116
+
117
+ <div className="grid gap-4 lg:grid-cols-3">
118
+ <div className="space-y-2">
119
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
120
+ Recipient Address
121
+ </label>
122
+ <div className="relative">
123
+ <input
124
+ value={recipient}
125
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
126
+ setRecipient(event.target.value)
127
+ }
128
+ placeholder="Enter Solana public key"
129
+ className="input pl-10"
130
+ />
131
+ <i className="fas fa-user absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
132
+ </div>
133
+ </div>
134
+
135
+ <div className="space-y-2">
136
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
137
+ Asset
138
+ </label>
139
+ <div className="relative">
140
+ <select
141
+ value={asset}
142
+ onChange={(event: ChangeEvent<HTMLSelectElement>) =>
143
+ setAsset(event.target.value)
144
+ }
145
+ className="select pl-10 appearance-none"
146
+ >
147
+ {options.map((opt) => (
148
+ <option key={opt.value} value={opt.value}>
149
+ {opt.label}
150
+ </option>
151
+ ))}
152
+ </select>
153
+ <i className="fas fa-coins absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
154
+ </div>
155
+ </div>
156
+
157
+ <div className="space-y-2">
158
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
159
+ Amount
160
+ </label>
161
+ <div className="relative">
162
+ <input
163
+ value={amount}
164
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
165
+ setAmount(event.target.value)
166
+ }
167
+ placeholder="1.0"
168
+ inputMode="decimal"
169
+ className="input pl-10"
170
+ />
171
+ <i className="fas fa-calculator absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
172
+ </div>
173
+ <p className="text-xs text-gray-500">
174
+ {asset === SOL_OPTION.value
175
+ ? "In SOL (9 decimals)"
176
+ : `In tokens (${selected.decimals} decimals)`}
177
+ </p>
178
+ </div>
179
+ </div>
180
+
181
+ <div className="mt-6 flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
182
+ <button
183
+ type="submit"
184
+ disabled={pending}
185
+ className={`btn-primary flex-1 sm:flex-initial ${pending ? 'opacity-50 cursor-not-allowed' : ''}`}
186
+ >
187
+ {pending ? (
188
+ <>
189
+ <div className="spinner"></div>
190
+ <span>Processing</span>
191
+ </>
192
+ ) : (
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>
196
+ </>
197
+ )}
198
+ </button>
199
+
200
+ {error && (
201
+ <div className="flex-1 flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
202
+ <i className="fas fa-exclamation-circle text-red-400"></i>
203
+ <p className="text-sm text-red-300">{error}</p>
204
+ </div>
205
+ )}
206
+
207
+ {message && (
208
+ <div className="flex-1 flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/30">
209
+ <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>
211
+ </div>
212
+ )}
213
+ </div>
214
+ </form>
215
+ );
216
+ }