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,183 @@
1
+ import { type ChangeEvent, useState } from "react";
2
+ import { Modal } from "./modal";
3
+
4
+ interface Props {
5
+ isOpen: boolean;
6
+ onClose: () => void;
7
+ onSubmit: (payload: {
8
+ programId: string;
9
+ endpoint?: string;
10
+ withAccounts: boolean;
11
+ accountsLimit?: number;
12
+ }) => Promise<void>;
13
+ }
14
+
15
+ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
16
+ const [programId, setProgramId] = useState("");
17
+ const [endpoint, setEndpoint] = useState("");
18
+ const [withAccounts, setWithAccounts] = useState(true);
19
+ const [accountsLimit, setAccountsLimit] = useState("100");
20
+ const [pending, setPending] = useState(false);
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ const handleSubmit = async () => {
24
+ setPending(true);
25
+ setError(null);
26
+ try {
27
+ await onSubmit({
28
+ programId: programId.trim(),
29
+ endpoint: endpoint.trim() ? endpoint.trim() : undefined,
30
+ withAccounts,
31
+ accountsLimit:
32
+ withAccounts && accountsLimit.trim()
33
+ ? Number(accountsLimit)
34
+ : undefined,
35
+ });
36
+ onClose();
37
+ setProgramId("");
38
+ setEndpoint("");
39
+ setAccountsLimit("100");
40
+ } catch (err: any) {
41
+ setError(err?.message ?? String(err));
42
+ } finally {
43
+ setPending(false);
44
+ }
45
+ };
46
+
47
+ return (
48
+ <Modal
49
+ isOpen={isOpen}
50
+ onClose={() => {
51
+ if (!pending) onClose();
52
+ }}
53
+ title="Clone Program"
54
+ icon="fa-code"
55
+ iconColor="blue"
56
+ footer={
57
+ <div className="flex justify-between items-center">
58
+ <div className="text-xs text-gray-500">
59
+ <i className="fas fa-info-circle mr-1"></i>
60
+ Clone from Solana mainnet or custom RPC
61
+ </div>
62
+ <div className="flex gap-3">
63
+ <button
64
+ type="button"
65
+ onClick={() => !pending && onClose()}
66
+ disabled={pending}
67
+ className="btn-secondary"
68
+ >
69
+ Cancel
70
+ </button>
71
+ <button
72
+ type="button"
73
+ onClick={handleSubmit}
74
+ disabled={pending || programId.trim().length === 0}
75
+ className={`btn-primary ${(pending || programId.trim().length === 0) ? 'opacity-50 cursor-not-allowed' : ''}`}
76
+ >
77
+ {pending ? (
78
+ <>
79
+ <div className="spinner"></div>
80
+ <span>Cloning Program</span>
81
+ </>
82
+ ) : (
83
+ <>
84
+ <i className="fas fa-download"></i>
85
+ <span>Clone Program</span>
86
+ </>
87
+ )}
88
+ </button>
89
+ </div>
90
+ </div>
91
+ }
92
+ >
93
+ <div className="space-y-5">
94
+ <div className="space-y-2">
95
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
96
+ Program ID *
97
+ </label>
98
+ <div className="relative">
99
+ <input
100
+ value={programId}
101
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
102
+ setProgramId(event.target.value)
103
+ }
104
+ placeholder="Enter program public key (e.g., TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)"
105
+ className="input pl-10 font-mono text-sm"
106
+ />
107
+ <i className="fas fa-fingerprint absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="space-y-2">
112
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
113
+ RPC Endpoint (Optional)
114
+ </label>
115
+ <div className="relative">
116
+ <input
117
+ value={endpoint}
118
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
119
+ setEndpoint(event.target.value)
120
+ }
121
+ placeholder="https://api.mainnet-beta.solana.com (default)"
122
+ className="input pl-10"
123
+ />
124
+ <i className="fas fa-globe absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
125
+ </div>
126
+ </div>
127
+
128
+ <div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3">
129
+ <label className="flex items-center gap-3 cursor-pointer group">
130
+ <input
131
+ type="checkbox"
132
+ checked={withAccounts}
133
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
134
+ setWithAccounts(event.target.checked)
135
+ }
136
+ className="checkbox"
137
+ />
138
+ <div>
139
+ <span className="text-sm text-white group-hover:text-purple-300 transition-colors">
140
+ Clone Program Accounts
141
+ </span>
142
+ <p className="text-xs text-gray-500 mt-0.5">
143
+ Include accounts owned by this program
144
+ </p>
145
+ </div>
146
+ </label>
147
+
148
+ {withAccounts && (
149
+ <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">
151
+ Account Limit
152
+ </label>
153
+ <div className="relative">
154
+ <input
155
+ value={accountsLimit}
156
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
157
+ setAccountsLimit(event.target.value)
158
+ }
159
+ placeholder="100"
160
+ type="number"
161
+ min="1"
162
+ max="1000"
163
+ className="input pl-10"
164
+ />
165
+ <i className="fas fa-list-ol absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
166
+ </div>
167
+ <p className="text-xs text-gray-500">
168
+ Maximum number of accounts to clone
169
+ </p>
170
+ </div>
171
+ )}
172
+ </div>
173
+
174
+ {error && (
175
+ <div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
176
+ <i className="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
177
+ <p className="text-sm text-red-300">{error}</p>
178
+ </div>
179
+ )}
180
+ </div>
181
+ </Modal>
182
+ );
183
+ }
@@ -0,0 +1,211 @@
1
+ import { type ChangeEvent, useState } from "react";
2
+ import { Modal } from "./modal";
3
+
4
+ interface Props {
5
+ isOpen: boolean;
6
+ onClose: () => void;
7
+ onSubmit: (payload: {
8
+ mint: string;
9
+ endpoint?: string;
10
+ cloneAccounts: boolean;
11
+ holders?: number;
12
+ allAccounts?: boolean;
13
+ }) => Promise<void>;
14
+ }
15
+
16
+ export function CloneTokenModal({ isOpen, onClose, onSubmit }: Props) {
17
+ const [mint, setMint] = useState("");
18
+ const [endpoint, setEndpoint] = useState("");
19
+ // Default OFF to avoid hitting public RPC rate limits by cloning holders.
20
+ const [cloneAccounts, setCloneAccounts] = useState(false);
21
+ const [holders, setHolders] = useState("20");
22
+ const [allAccounts, setAllAccounts] = useState(false);
23
+ const [pending, setPending] = useState(false);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ const handleSubmit = async () => {
27
+ setPending(true);
28
+ setError(null);
29
+ try {
30
+ await onSubmit({
31
+ mint: mint.trim(),
32
+ endpoint: endpoint.trim() ? endpoint.trim() : undefined,
33
+ cloneAccounts,
34
+ holders:
35
+ cloneAccounts && !allAccounts && holders.trim()
36
+ ? Number(holders)
37
+ : undefined,
38
+ allAccounts,
39
+ });
40
+ onClose();
41
+ setMint("");
42
+ setEndpoint("");
43
+ setHolders("20");
44
+ setAllAccounts(false);
45
+ } catch (err: any) {
46
+ setError(err?.message ?? String(err));
47
+ } finally {
48
+ setPending(false);
49
+ }
50
+ };
51
+
52
+ return (
53
+ <Modal
54
+ isOpen={isOpen}
55
+ onClose={() => {
56
+ if (!pending) onClose();
57
+ }}
58
+ title="Clone Token"
59
+ icon="fa-coins"
60
+ iconColor="amber"
61
+ footer={
62
+ <div className="flex justify-between items-center">
63
+ <div className="text-xs text-gray-500">
64
+ <i className="fas fa-info-circle mr-1"></i>
65
+ Clone SPL tokens from mainnet
66
+ </div>
67
+ <div className="flex gap-3">
68
+ <button
69
+ type="button"
70
+ onClick={() => !pending && onClose()}
71
+ disabled={pending}
72
+ className="btn-secondary"
73
+ >
74
+ Cancel
75
+ </button>
76
+ <button
77
+ type="button"
78
+ onClick={handleSubmit}
79
+ disabled={pending || mint.trim().length === 0}
80
+ className={`btn-primary ${(pending || mint.trim().length === 0) ? 'opacity-50 cursor-not-allowed' : ''}`}
81
+ >
82
+ {pending ? (
83
+ <>
84
+ <div className="spinner"></div>
85
+ <span>Cloning Token</span>
86
+ </>
87
+ ) : (
88
+ <>
89
+ <i className="fas fa-download"></i>
90
+ <span>Clone Token</span>
91
+ </>
92
+ )}
93
+ </button>
94
+ </div>
95
+ </div>
96
+ }
97
+ >
98
+ <div className="space-y-5">
99
+ <div className="space-y-2">
100
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
101
+ Mint Address *
102
+ </label>
103
+ <div className="relative">
104
+ <input
105
+ value={mint}
106
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
107
+ setMint(event.target.value)
108
+ }
109
+ placeholder="Enter token mint address (e.g., EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v)"
110
+ className="input pl-10 font-mono text-sm"
111
+ />
112
+ <i className="fas fa-coin absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
113
+ </div>
114
+ </div>
115
+
116
+ <div className="space-y-2">
117
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
118
+ RPC Endpoint (Optional)
119
+ </label>
120
+ <div className="relative">
121
+ <input
122
+ value={endpoint}
123
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
124
+ setEndpoint(event.target.value)
125
+ }
126
+ placeholder="https://api.mainnet-beta.solana.com (default)"
127
+ className="input pl-10"
128
+ />
129
+ <i className="fas fa-globe absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3">
134
+ <label className="flex items-center gap-3 cursor-pointer group">
135
+ <input
136
+ type="checkbox"
137
+ checked={cloneAccounts}
138
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
139
+ setCloneAccounts(event.target.checked)
140
+ }
141
+ className="checkbox"
142
+ />
143
+ <div>
144
+ <span className="text-sm text-white group-hover:text-purple-300 transition-colors">
145
+ Clone Token Accounts
146
+ </span>
147
+ <p className="text-xs text-gray-500 mt-0.5">
148
+ Include holder accounts for this token
149
+ </p>
150
+ </div>
151
+ </label>
152
+
153
+ {cloneAccounts && (
154
+ <div className="ml-8 space-y-4 pt-3 border-t border-white/5">
155
+ <label className="flex items-center gap-3 cursor-pointer group">
156
+ <input
157
+ type="checkbox"
158
+ checked={allAccounts}
159
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
160
+ setAllAccounts(event.target.checked)
161
+ }
162
+ className="checkbox"
163
+ />
164
+ <div>
165
+ <span className="text-sm text-white group-hover:text-purple-300 transition-colors">
166
+ Clone All Accounts
167
+ </span>
168
+ <p className="text-xs text-gray-500 mt-0.5">
169
+ Warning: This may be slow for popular tokens
170
+ </p>
171
+ </div>
172
+ </label>
173
+
174
+ {!allAccounts && (
175
+ <div className="space-y-2">
176
+ <label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider">
177
+ Top Holders Limit
178
+ </label>
179
+ <div className="relative">
180
+ <input
181
+ value={holders}
182
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
183
+ setHolders(event.target.value)
184
+ }
185
+ placeholder="20"
186
+ type="number"
187
+ min="1"
188
+ max="100"
189
+ className="input pl-10"
190
+ />
191
+ <i className="fas fa-users absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
192
+ </div>
193
+ <p className="text-xs text-gray-500">
194
+ Number of top holders to clone
195
+ </p>
196
+ </div>
197
+ )}
198
+ </div>
199
+ )}
200
+ </div>
201
+
202
+ {error && (
203
+ <div className="flex items-start gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/30">
204
+ <i className="fas fa-exclamation-circle text-red-400 mt-0.5"></i>
205
+ <p className="text-sm text-red-300">{error}</p>
206
+ </div>
207
+ )}
208
+ </div>
209
+ </Modal>
210
+ );
211
+ }
@@ -0,0 +1,127 @@
1
+ import { type ReactNode, useEffect } from "react";
2
+
3
+ interface ModalProps {
4
+ isOpen: boolean;
5
+ title: string;
6
+ onClose: () => void;
7
+ children: ReactNode;
8
+ footer?: ReactNode;
9
+ icon?: string;
10
+ iconColor?: string;
11
+ }
12
+
13
+ export function Modal({
14
+ isOpen,
15
+ title,
16
+ onClose,
17
+ children,
18
+ footer,
19
+ icon = "fa-window-maximize",
20
+ iconColor = "purple",
21
+ }: ModalProps) {
22
+ useEffect(() => {
23
+ if (!isOpen || typeof window === "undefined") return undefined;
24
+ const handler = (event: KeyboardEvent) => {
25
+ if (event.key === "Escape") onClose();
26
+ };
27
+ window.addEventListener("keydown", handler);
28
+ return () => window.removeEventListener("keydown", handler);
29
+ }, [isOpen, onClose]);
30
+
31
+ if (!isOpen) return null;
32
+
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',
38
+ };
39
+
40
+ return (
41
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 animate-fadeIn">
42
+ {/* Backdrop */}
43
+ <div
44
+ className="absolute inset-0 bg-black/80 backdrop-blur-sm"
45
+ onClick={onClose}
46
+ />
47
+
48
+ {/* Modal */}
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
+ {/* Header */}
51
+ <div className="p-6 border-b border-white/10 bg-gradient-to-r from-white/5 to-transparent">
52
+ <div className="flex items-center justify-between">
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`}>
55
+ <i className={`fas ${icon}`}></i>
56
+ </div>
57
+ <h3 className="text-xl font-bold text-white">{title}</h3>
58
+ </div>
59
+ <button
60
+ type="button"
61
+ onClick={onClose}
62
+ className="btn-icon hover:bg-red-500/20 hover:border-red-500/30 hover:text-red-400"
63
+ aria-label="Close modal"
64
+ >
65
+ <i className="fas fa-times"></i>
66
+ </button>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Content */}
71
+ <div className="p-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
72
+ {children}
73
+ </div>
74
+
75
+ {/* Footer */}
76
+ {footer && (
77
+ <div className="p-6 border-t border-white/10 bg-gradient-to-r from-white/5 to-transparent">
78
+ {footer}
79
+ </div>
80
+ )}
81
+ </div>
82
+
83
+ <style jsx>{`
84
+ @keyframes fadeIn {
85
+ from {
86
+ opacity: 0;
87
+ }
88
+ to {
89
+ opacity: 1;
90
+ }
91
+ }
92
+
93
+ @keyframes modalSlideIn {
94
+ from {
95
+ opacity: 0;
96
+ transform: scale(0.95) translateY(20px);
97
+ }
98
+ to {
99
+ opacity: 1;
100
+ transform: scale(1) translateY(0);
101
+ }
102
+ }
103
+
104
+ .animate-fadeIn {
105
+ animation: fadeIn 0.2s ease-out;
106
+ }
107
+
108
+ .animate-modalSlideIn {
109
+ animation: modalSlideIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
110
+ }
111
+
112
+ .custom-scrollbar::-webkit-scrollbar {
113
+ width: 8px;
114
+ }
115
+
116
+ .custom-scrollbar::-webkit-scrollbar-track {
117
+ background: transparent;
118
+ }
119
+
120
+ .custom-scrollbar::-webkit-scrollbar-thumb {
121
+ background: linear-gradient(180deg, var(--color-accent-primary), var(--color-accent-secondary));
122
+ border-radius: 4px;
123
+ }
124
+ `}</style>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,112 @@
1
+ import type { ProgramSummary } from "../api";
2
+
3
+ interface Props {
4
+ programs: ProgramSummary[];
5
+ loading: boolean;
6
+ onRefresh: () => void;
7
+ onAdd: () => void;
8
+ }
9
+
10
+ export function ProgramsPanel({ programs, loading, onRefresh, onAdd }: Props) {
11
+ return (
12
+ <section className="glass-panel p-6">
13
+ <header className="flex flex-wrap items-center justify-between gap-3 mb-6">
14
+ <div className="flex items-center gap-3">
15
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500/20 to-cyan-500/20 flex items-center justify-center">
16
+ <i className="fas fa-code text-blue-400"></i>
17
+ </div>
18
+ <div>
19
+ <h2 className="text-xl font-bold text-white">Programs</h2>
20
+ <p className="text-xs text-gray-500">
21
+ {programs.length} deployed programs
22
+ </p>
23
+ </div>
24
+ </div>
25
+ <div className="flex items-center gap-2">
26
+ <button
27
+ type="button"
28
+ onClick={onRefresh}
29
+ disabled={loading}
30
+ className={`btn-secondary text-sm ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
31
+ >
32
+ <i className={`fas fa-sync-alt ${loading ? 'animate-spin' : ''}`}></i>
33
+ <span>{loading ? "Refreshing" : "Refresh"}</span>
34
+ </button>
35
+ <button
36
+ type="button"
37
+ onClick={onAdd}
38
+ className="btn-primary text-sm"
39
+ >
40
+ <i className="fas fa-plus"></i>
41
+ <span>Add Program</span>
42
+ </button>
43
+ </div>
44
+ </header>
45
+
46
+ <div className="overflow-x-auto rounded-xl">
47
+ {programs.length === 0 ? (
48
+ <div className="flex flex-col items-center justify-center py-12 text-center">
49
+ <div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-600/20 to-cyan-600/20 flex items-center justify-center mb-4">
50
+ <i className="fas fa-code text-blue-500 text-2xl"></i>
51
+ </div>
52
+ <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>
54
+ </div>
55
+ ) : (
56
+ <table className="table-modern">
57
+ <thead>
58
+ <tr>
59
+ <th>Program ID</th>
60
+ <th>Owner</th>
61
+ <th>Status</th>
62
+ <th>Data Size</th>
63
+ <th>Balance</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody>
67
+ {programs.map((program, index) => (
68
+ <tr key={program.programId} style={{animationDelay: `${index * 50}ms`}} className="animate-fadeIn">
69
+ <td>
70
+ <div className="flex items-center gap-2">
71
+ <i className="fas fa-cube text-blue-400 text-xs"></i>
72
+ <span className="font-mono text-xs text-blue-300">
73
+ {program.programId.slice(0, 8)}...{program.programId.slice(-6)}
74
+ </span>
75
+ </div>
76
+ </td>
77
+ <td>
78
+ <span className="font-mono text-xs text-gray-400">
79
+ {program.owner.slice(0, 8)}...
80
+ </span>
81
+ </td>
82
+ <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>
86
+ </span>
87
+ </td>
88
+ <td>
89
+ <div className="flex items-center gap-2">
90
+ <i className="fas fa-database text-gray-400 text-xs"></i>
91
+ <span className="text-gray-300">
92
+ {(program.dataLen / 1024).toFixed(1)} KB
93
+ </span>
94
+ </div>
95
+ </td>
96
+ <td>
97
+ <div className="flex items-center gap-2">
98
+ <i className="fas fa-wallet text-gray-400 text-xs"></i>
99
+ <span className="text-gray-300">
100
+ {(Number(BigInt(program.lamports)) / 1e9).toFixed(4)} SOL
101
+ </span>
102
+ </div>
103
+ </td>
104
+ </tr>
105
+ ))}
106
+ </tbody>
107
+ </table>
108
+ )}
109
+ </div>
110
+ </section>
111
+ );
112
+ }