solforge 0.1.6 → 0.2.0
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/.agi/agi.sqlite +0 -0
- package/.claude/settings.local.json +9 -0
- package/.github/workflows/release-binaries.yml +133 -0
- package/.tmp/.787ebcdbf7b8fde8-00000000.hm +0 -0
- package/.tmp/.bffe6efebdf8aedc-00000000.hm +0 -0
- package/AGENTS.md +271 -0
- package/CLAUDE.md +106 -0
- package/PROJECT_STRUCTURE.md +124 -0
- package/README.md +367 -393
- package/SOLANA_KIT_GUIDE.md +251 -0
- package/SOLFORGE.md +119 -0
- package/biome.json +34 -0
- package/bun.lock +743 -0
- package/docs/bun-single-file-executable.md +585 -0
- package/docs/cli-plan.md +154 -0
- package/docs/data-indexing-plan.md +214 -0
- package/docs/gui-roadmap.md +202 -0
- package/drizzle/0000_friendly_millenium_guard.sql +53 -0
- package/drizzle/0001_stale_sentinels.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +329 -0
- package/drizzle/meta/0001_snapshot.json +345 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +12 -0
- package/index.ts +21 -0
- package/mint.sh +47 -0
- package/package.json +45 -69
- package/postcss.config.js +6 -0
- package/rpc-server.ts.backup +519 -0
- package/server/index.ts +5 -0
- package/server/lib/base58.ts +33 -0
- package/server/lib/faucet.ts +110 -0
- package/server/lib/spl-token.ts +57 -0
- package/server/methods/TEMPLATE.md +117 -0
- package/server/methods/account/get-account-info.ts +90 -0
- package/server/methods/account/get-balance.ts +27 -0
- package/server/methods/account/get-multiple-accounts.ts +83 -0
- package/server/methods/account/get-parsed-account-info.ts +21 -0
- package/server/methods/account/index.ts +12 -0
- package/server/methods/account/parsers/index.ts +52 -0
- package/server/methods/account/parsers/loader-upgradeable.ts +66 -0
- package/server/methods/account/parsers/spl-token.ts +237 -0
- package/server/methods/account/parsers/system.ts +4 -0
- package/server/methods/account/request-airdrop.ts +219 -0
- package/server/methods/admin/adopt-mint-authority.ts +94 -0
- package/server/methods/admin/clone-program-accounts.ts +55 -0
- package/server/methods/admin/clone-program.ts +152 -0
- package/server/methods/admin/clone-token-accounts.ts +117 -0
- package/server/methods/admin/clone-token-mint.ts +82 -0
- package/server/methods/admin/create-mint.ts +114 -0
- package/server/methods/admin/create-token-account.ts +137 -0
- package/server/methods/admin/helpers.ts +70 -0
- package/server/methods/admin/index.ts +10 -0
- package/server/methods/admin/list-mints.ts +21 -0
- package/server/methods/admin/load-program.ts +52 -0
- package/server/methods/admin/mint-to.ts +278 -0
- package/server/methods/block/get-block-height.ts +5 -0
- package/server/methods/block/get-block.ts +35 -0
- package/server/methods/block/get-blocks-with-limit.ts +23 -0
- package/server/methods/block/get-latest-blockhash.ts +12 -0
- package/server/methods/block/get-slot.ts +5 -0
- package/server/methods/block/index.ts +6 -0
- package/server/methods/block/is-blockhash-valid.ts +23 -0
- package/server/methods/epoch/get-cluster-nodes.ts +17 -0
- package/server/methods/epoch/get-epoch-info.ts +16 -0
- package/server/methods/epoch/get-epoch-schedule.ts +15 -0
- package/server/methods/epoch/get-highest-snapshot-slot.ts +9 -0
- package/server/methods/epoch/get-leader-schedule.ts +8 -0
- package/server/methods/epoch/get-max-retransmit-slot.ts +9 -0
- package/server/methods/epoch/get-max-shred-insert-slot.ts +9 -0
- package/server/methods/epoch/get-slot-leader.ts +6 -0
- package/server/methods/epoch/get-slot-leaders.ts +9 -0
- package/server/methods/epoch/get-stake-activation.ts +9 -0
- package/server/methods/epoch/get-stake-minimum-delegation.ts +9 -0
- package/server/methods/epoch/get-vote-accounts.ts +19 -0
- package/server/methods/epoch/index.ts +13 -0
- package/server/methods/epoch/minimum-ledger-slot.ts +5 -0
- package/server/methods/fee/get-fee-calculator-for-blockhash.ts +12 -0
- package/server/methods/fee/get-fee-for-message.ts +8 -0
- package/server/methods/fee/get-fee-rate-governor.ts +16 -0
- package/server/methods/fee/get-fees.ts +14 -0
- package/server/methods/fee/get-recent-prioritization-fees.ts +22 -0
- package/server/methods/fee/index.ts +5 -0
- package/server/methods/get-address-lookup-table.ts +31 -0
- package/server/methods/index.ts +265 -0
- package/server/methods/performance/get-recent-performance-samples.ts +25 -0
- package/server/methods/performance/get-transaction-count.ts +5 -0
- package/server/methods/performance/index.ts +2 -0
- package/server/methods/program/get-block-commitment.ts +9 -0
- package/server/methods/program/get-block-production.ts +14 -0
- package/server/methods/program/get-block-time.ts +21 -0
- package/server/methods/program/get-blocks.ts +11 -0
- package/server/methods/program/get-first-available-block.ts +9 -0
- package/server/methods/program/get-genesis-hash.ts +6 -0
- package/server/methods/program/get-identity.ts +6 -0
- package/server/methods/program/get-inflation-governor.ts +15 -0
- package/server/methods/program/get-inflation-rate.ts +10 -0
- package/server/methods/program/get-inflation-reward.ts +12 -0
- package/server/methods/program/get-largest-accounts.ts +8 -0
- package/server/methods/program/get-parsed-program-accounts.ts +12 -0
- package/server/methods/program/get-parsed-token-accounts-by-delegate.ts +12 -0
- package/server/methods/program/get-parsed-token-accounts-by-owner.ts +12 -0
- package/server/methods/program/get-program-accounts.ts +221 -0
- package/server/methods/program/get-supply.ts +13 -0
- package/server/methods/program/get-token-account-balance.ts +64 -0
- package/server/methods/program/get-token-accounts-by-delegate.ts +81 -0
- package/server/methods/program/get-token-accounts-by-owner.ts +390 -0
- package/server/methods/program/get-token-largest-accounts.ts +80 -0
- package/server/methods/program/get-token-supply.ts +38 -0
- package/server/methods/program/index.ts +21 -0
- package/server/methods/solforge/index.ts +155 -0
- package/server/methods/system/get-health.ts +5 -0
- package/server/methods/system/get-minimum-balance-for-rent-exemption.ts +13 -0
- package/server/methods/system/get-version.ts +9 -0
- package/server/methods/system/index.ts +3 -0
- package/server/methods/transaction/get-confirmed-transaction.ts +11 -0
- package/server/methods/transaction/get-parsed-transaction.ts +21 -0
- package/server/methods/transaction/get-signature-statuses.ts +72 -0
- package/server/methods/transaction/get-signatures-for-address.ts +45 -0
- package/server/methods/transaction/get-transaction.ts +428 -0
- package/server/methods/transaction/index.ts +7 -0
- package/server/methods/transaction/send-transaction.ts +232 -0
- package/server/methods/transaction/simulate-transaction.ts +56 -0
- package/server/rpc-server.ts +474 -0
- package/server/types.ts +74 -0
- package/server/ws-server.ts +171 -0
- package/sf.config.json +38 -0
- package/src/cli/bootstrap.ts +67 -0
- package/src/cli/commands/airdrop.ts +37 -0
- package/src/cli/commands/config.ts +39 -0
- package/src/cli/commands/mint.ts +187 -0
- package/src/cli/commands/program-clone.ts +124 -0
- package/src/cli/commands/program-load.ts +64 -0
- package/src/cli/commands/rpc-start.ts +46 -0
- package/src/cli/commands/token-adopt-authority.ts +37 -0
- package/src/cli/commands/token-clone.ts +113 -0
- package/src/cli/commands/token-create.ts +81 -0
- package/src/cli/main.ts +130 -0
- package/src/cli/run-solforge.ts +98 -0
- package/src/cli/setup-utils.ts +54 -0
- package/src/cli/setup-wizard.ts +256 -0
- package/src/cli/utils/args.ts +15 -0
- package/src/config/index.ts +130 -0
- package/src/db/index.ts +83 -0
- package/src/db/schema/accounts.ts +23 -0
- package/src/db/schema/address-signatures.ts +31 -0
- package/src/db/schema/index.ts +5 -0
- package/src/db/schema/meta-kv.ts +9 -0
- package/src/db/schema/transactions.ts +29 -0
- package/src/db/schema/tx-accounts.ts +33 -0
- package/src/db/tx-store.ts +229 -0
- package/src/gui/public/app.css +1 -0
- package/src/gui/public/index.html +19 -0
- package/src/gui/server.ts +297 -0
- package/src/gui/src/api.ts +127 -0
- package/src/gui/src/app.tsx +390 -0
- package/src/gui/src/components/airdrop-mint-form.tsx +216 -0
- package/src/gui/src/components/clone-program-modal.tsx +183 -0
- package/src/gui/src/components/clone-token-modal.tsx +211 -0
- package/src/gui/src/components/modal.tsx +127 -0
- package/src/gui/src/components/programs-panel.tsx +112 -0
- package/src/gui/src/components/status-panel.tsx +122 -0
- package/src/gui/src/components/tokens-panel.tsx +116 -0
- package/src/gui/src/hooks/use-interval.ts +17 -0
- package/src/gui/src/index.css +529 -0
- package/src/gui/src/main.tsx +17 -0
- package/src/migrations-bundled.ts +17 -0
- package/src/rpc/start.ts +44 -0
- package/tailwind.config.js +27 -0
- package/test-client.ts +120 -0
- package/tmp/inspect-html.ts +4 -0
- package/tmp/response-test.ts +5 -0
- package/tmp/test-html.ts +5 -0
- package/tmp/test-server.ts +13 -0
- package/tsconfig.json +24 -23
- package/LICENSE +0 -21
- package/scripts/postinstall.cjs +0 -103
- package/src/api-server-entry.ts +0 -109
- package/src/commands/add-program.ts +0 -337
- package/src/commands/init.ts +0 -122
- package/src/commands/list.ts +0 -136
- package/src/commands/mint.ts +0 -336
- package/src/commands/start.ts +0 -878
- package/src/commands/status.ts +0 -99
- package/src/commands/stop.ts +0 -406
- package/src/config/manager.ts +0 -157
- package/src/index.ts +0 -188
- package/src/services/api-server.ts +0 -532
- package/src/services/port-manager.ts +0 -177
- package/src/services/process-registry.ts +0 -154
- package/src/services/program-cloner.ts +0 -317
- package/src/services/token-cloner.ts +0 -809
- package/src/services/validator.ts +0 -295
- package/src/types/config.ts +0 -110
- package/src/utils/shell.ts +0 -110
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|