solforge 0.2.5 → 0.2.7
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/package.json +1 -1
- package/scripts/postinstall.cjs +3 -3
- package/server/lib/base58.ts +1 -1
- package/server/lib/instruction-parser.ts +242 -0
- 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 +122 -86
- package/server/methods/admin/mint-to.ts +11 -38
- 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 +54 -33
- 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 +434 -287
- package/server/methods/transaction/inner-instructions.test.ts +63 -0
- package/server/methods/transaction/send-transaction.ts +248 -56
- package/server/methods/transaction/simulate-transaction.ts +3 -2
- package/server/rpc-server.ts +98 -61
- package/server/types.ts +65 -30
- package/server/ws-server.ts +11 -7
- package/src/api-server-entry.ts +5 -5
- 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 +7 -7
- 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 +33 -36
- package/src/cli/run-solforge.ts +3 -3
- package/src/cli/setup-wizard.ts +8 -6
- package/src/commands/add-program.ts +1 -1
- package/src/commands/init.ts +2 -2
- package/src/commands/mint.ts +5 -6
- package/src/commands/start.ts +10 -9
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +1 -1
- package/src/config/index.ts +33 -17
- package/src/config/manager.ts +3 -3
- package/src/db/index.ts +2 -2
- package/src/db/schema/index.ts +1 -0
- package/src/db/schema/transactions.ts +29 -22
- package/src/db/schema/tx-account-states.ts +21 -0
- package/src/db/tx-store.ts +113 -76
- package/src/gui/public/app.css +13 -13
- package/src/gui/server.ts +1 -1
- package/src/gui/src/api.ts +1 -1
- package/src/gui/src/app.tsx +49 -17
- package/src/gui/src/components/airdrop-mint-form.tsx +32 -8
- package/src/gui/src/components/clone-program-modal.tsx +25 -6
- package/src/gui/src/components/clone-token-modal.tsx +25 -6
- package/src/gui/src/components/modal.tsx +6 -1
- package/src/gui/src/components/status-panel.tsx +1 -1
- package/src/index.ts +19 -6
- package/src/migrations-bundled.ts +8 -2
- package/src/services/api-server.ts +41 -19
- package/src/services/port-manager.ts +7 -10
- package/src/services/process-registry.ts +4 -5
- package/src/services/program-cloner.ts +4 -4
- package/src/services/token-cloner.ts +4 -4
- package/src/services/validator.ts +2 -4
- package/src/types/config.ts +2 -2
- package/src/utils/shell.ts +1 -1
- package/src/utils/token-loader.ts +2 -2
package/src/db/tx-store.ts
CHANGED
|
@@ -4,27 +4,36 @@ import { accounts } from "./schema/accounts";
|
|
|
4
4
|
import { addressSignatures } from "./schema/address-signatures";
|
|
5
5
|
import { transactions } from "./schema/transactions";
|
|
6
6
|
import { txAccounts } from "./schema/tx-accounts";
|
|
7
|
+
import { txAccountStates } from "./schema/tx-account-states";
|
|
7
8
|
|
|
8
9
|
export type InsertTxBundle = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
signature: string;
|
|
11
|
+
slot: number;
|
|
12
|
+
blockTime?: number;
|
|
13
|
+
version: 0 | "legacy";
|
|
14
|
+
fee: number;
|
|
15
|
+
err: unknown | null;
|
|
16
|
+
rawBase64: string;
|
|
17
|
+
preBalances: number[];
|
|
18
|
+
postBalances: number[];
|
|
19
|
+
logs: string[];
|
|
20
|
+
innerInstructions?: unknown[];
|
|
21
|
+
computeUnits?: number | bigint | null;
|
|
22
|
+
returnData?: { programId: string; dataBase64: string } | null;
|
|
23
|
+
accounts: Array<{
|
|
24
|
+
address: string;
|
|
25
|
+
index: number;
|
|
26
|
+
signer: boolean;
|
|
27
|
+
writable: boolean;
|
|
28
|
+
programIdIndex?: number;
|
|
29
|
+
}>;
|
|
30
|
+
preTokenBalances?: unknown[];
|
|
31
|
+
postTokenBalances?: unknown[];
|
|
32
|
+
accountStates?: Array<{
|
|
33
|
+
address: string;
|
|
34
|
+
pre?: Partial<AccountSnapshot> | null;
|
|
35
|
+
post?: Partial<AccountSnapshot> | null;
|
|
36
|
+
}>;
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
export type AccountSnapshot = {
|
|
@@ -39,57 +48,81 @@ export type AccountSnapshot = {
|
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
export class TxStore {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
51
|
+
async insertTransactionBundle(bundle: InsertTxBundle): Promise<void> {
|
|
52
|
+
const errJson = bundle.err ? JSON.stringify(bundle.err) : null;
|
|
53
|
+
await db.transaction(async (tx) => {
|
|
54
|
+
await tx
|
|
55
|
+
.insert(transactions)
|
|
56
|
+
.values({
|
|
57
|
+
signature: bundle.signature,
|
|
58
|
+
slot: bundle.slot,
|
|
59
|
+
blockTime: bundle.blockTime ?? null,
|
|
60
|
+
version: String(bundle.version),
|
|
61
|
+
errJson,
|
|
62
|
+
fee: bundle.fee,
|
|
63
|
+
rawBase64: bundle.rawBase64,
|
|
64
|
+
preBalancesJson: JSON.stringify(bundle.preBalances ?? []),
|
|
65
|
+
postBalancesJson: JSON.stringify(bundle.postBalances ?? []),
|
|
66
|
+
logsJson: JSON.stringify(bundle.logs ?? []),
|
|
67
|
+
preTokenBalancesJson: JSON.stringify(bundle.preTokenBalances ?? []),
|
|
68
|
+
postTokenBalancesJson: JSON.stringify(bundle.postTokenBalances ?? []),
|
|
69
|
+
innerInstructionsJson: JSON.stringify(bundle.innerInstructions ?? []),
|
|
70
|
+
computeUnits:
|
|
71
|
+
bundle.computeUnits == null
|
|
72
|
+
? null
|
|
73
|
+
: Number(bundle.computeUnits),
|
|
74
|
+
returnDataProgramId: bundle.returnData?.programId ?? null,
|
|
75
|
+
returnDataBase64: bundle.returnData?.dataBase64 ?? null,
|
|
76
|
+
})
|
|
77
|
+
.onConflictDoNothing();
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(bundle.accounts) && bundle.accounts.length > 0) {
|
|
80
|
+
await tx
|
|
81
|
+
.insert(txAccounts)
|
|
82
|
+
.values(
|
|
83
|
+
bundle.accounts.map((a) => ({
|
|
84
|
+
signature: bundle.signature,
|
|
85
|
+
accountIndex: a.index,
|
|
86
|
+
address: a.address,
|
|
87
|
+
signer: a.signer ? 1 : 0,
|
|
88
|
+
writable: a.writable ? 1 : 0,
|
|
89
|
+
programIdIndex: a.programIdIndex ?? null,
|
|
90
|
+
})),
|
|
91
|
+
)
|
|
92
|
+
.onConflictDoNothing();
|
|
93
|
+
|
|
94
|
+
await tx
|
|
95
|
+
.insert(addressSignatures)
|
|
96
|
+
.values(
|
|
97
|
+
bundle.accounts.map((a) => ({
|
|
98
|
+
address: a.address,
|
|
99
|
+
signature: bundle.signature,
|
|
100
|
+
slot: bundle.slot,
|
|
101
|
+
err: errJson ? 1 : 0,
|
|
102
|
+
blockTime: bundle.blockTime ?? null,
|
|
103
|
+
})),
|
|
104
|
+
)
|
|
105
|
+
.onConflictDoNothing();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
Array.isArray(bundle.accountStates) &&
|
|
110
|
+
bundle.accountStates.length > 0
|
|
111
|
+
) {
|
|
112
|
+
await tx
|
|
113
|
+
.insert(txAccountStates)
|
|
114
|
+
.values(
|
|
115
|
+
bundle.accountStates.map((s) => ({
|
|
116
|
+
signature: bundle.signature,
|
|
117
|
+
address: s.address,
|
|
118
|
+
preJson: s.pre ? JSON.stringify(s.pre) : null,
|
|
119
|
+
postJson: s.post ? JSON.stringify(s.post) : null,
|
|
120
|
+
})),
|
|
121
|
+
)
|
|
122
|
+
.onConflictDoNothing();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
93
126
|
|
|
94
127
|
async upsertAccounts(snapshots: AccountSnapshot[]): Promise<void> {
|
|
95
128
|
if (!Array.isArray(snapshots) || snapshots.length === 0) return;
|
|
@@ -133,7 +166,7 @@ export class TxStore {
|
|
|
133
166
|
|
|
134
167
|
async getStatuses(signatures: string[]) {
|
|
135
168
|
if (!Array.isArray(signatures) || signatures.length === 0)
|
|
136
|
-
return new Map<string, { slot: number; err:
|
|
169
|
+
return new Map<string, { slot: number; err: unknown | null }>();
|
|
137
170
|
const results = await db
|
|
138
171
|
.select({
|
|
139
172
|
signature: transactions.signature,
|
|
@@ -142,7 +175,7 @@ export class TxStore {
|
|
|
142
175
|
})
|
|
143
176
|
.from(transactions)
|
|
144
177
|
.where(inArraySafe(transactions.signature, signatures));
|
|
145
|
-
const map = new Map<string, { slot: number; err:
|
|
178
|
+
const map = new Map<string, { slot: number; err: unknown | null }>();
|
|
146
179
|
for (const r of results)
|
|
147
180
|
map.set(r.signature, {
|
|
148
181
|
slot: Number(r.slot),
|
|
@@ -167,7 +200,7 @@ export class TxStore {
|
|
|
167
200
|
}
|
|
168
201
|
const limit = Math.min(Math.max(opts.limit ?? 1000, 1), 1000);
|
|
169
202
|
|
|
170
|
-
const whereClauses = [eq(addressSignatures.address, address)] as
|
|
203
|
+
const whereClauses = [eq(addressSignatures.address, address)] as unknown[];
|
|
171
204
|
if (typeof beforeSlot === "number")
|
|
172
205
|
whereClauses.push(lt(addressSignatures.slot, beforeSlot));
|
|
173
206
|
if (typeof untilSlot === "number")
|
|
@@ -216,7 +249,7 @@ export class TxStore {
|
|
|
216
249
|
}
|
|
217
250
|
}
|
|
218
251
|
|
|
219
|
-
function safeParse<T =
|
|
252
|
+
function safeParse<T = unknown>(s: string): T | null {
|
|
220
253
|
try {
|
|
221
254
|
return JSON.parse(s) as T;
|
|
222
255
|
} catch {
|
|
@@ -224,6 +257,10 @@ function safeParse<T = any>(s: string): T | null {
|
|
|
224
257
|
}
|
|
225
258
|
}
|
|
226
259
|
|
|
227
|
-
function inArraySafe<T>(col:
|
|
228
|
-
return arr.length > 0
|
|
260
|
+
function inArraySafe<T>(col: unknown, arr: T[]) {
|
|
261
|
+
return arr.length > 0
|
|
262
|
+
? // biome-ignore lint/suspicious/noExplicitAny: Drizzle generic typing workaround
|
|
263
|
+
(inArray as unknown as (c: unknown, a: T[]) => any)(col, arr)
|
|
264
|
+
: // biome-ignore lint/suspicious/noExplicitAny: Force an always-false predicate without over-constraining types
|
|
265
|
+
eq(col as any, "__never__");
|
|
229
266
|
}
|
package/src/gui/public/app.css
CHANGED
|
@@ -442,11 +442,11 @@ video {
|
|
|
442
442
|
line-height: 1.25rem;
|
|
443
443
|
}
|
|
444
444
|
.\!input {
|
|
445
|
-
background: var(--color-bg-surface)
|
|
446
|
-
border: 1px solid var(--color-border-subtle)
|
|
447
|
-
color: var(--color-text-primary)
|
|
448
|
-
transition: var(--transition-base)
|
|
449
|
-
font-family: Inter, sans-serif
|
|
445
|
+
background: var(--color-bg-surface);
|
|
446
|
+
border: 1px solid var(--color-border-subtle);
|
|
447
|
+
color: var(--color-text-primary);
|
|
448
|
+
transition: var(--transition-base);
|
|
449
|
+
font-family: Inter, sans-serif;
|
|
450
450
|
}
|
|
451
451
|
.input {
|
|
452
452
|
background: var(--color-bg-surface);
|
|
@@ -456,18 +456,18 @@ video {
|
|
|
456
456
|
font-family: Inter, sans-serif;
|
|
457
457
|
}
|
|
458
458
|
.\!input:hover {
|
|
459
|
-
background: var(--color-bg-elevated)
|
|
460
|
-
border-color: var(--color-border-default)
|
|
459
|
+
background: var(--color-bg-elevated);
|
|
460
|
+
border-color: var(--color-border-default);
|
|
461
461
|
}
|
|
462
462
|
.input:hover {
|
|
463
463
|
background: var(--color-bg-elevated);
|
|
464
464
|
border-color: var(--color-border-default);
|
|
465
465
|
}
|
|
466
466
|
.\!input:focus {
|
|
467
|
-
outline: none
|
|
468
|
-
border-color: var(--color-accent-primary)
|
|
469
|
-
box-shadow: 0 0 0 3px var(--color-accent-glow)
|
|
470
|
-
background: var(--color-bg-elevated)
|
|
467
|
+
outline: none;
|
|
468
|
+
border-color: var(--color-accent-primary);
|
|
469
|
+
box-shadow: 0 0 0 3px var(--color-accent-glow);
|
|
470
|
+
background: var(--color-bg-elevated);
|
|
471
471
|
}
|
|
472
472
|
.input:focus {
|
|
473
473
|
outline: none;
|
|
@@ -476,10 +476,10 @@ video {
|
|
|
476
476
|
background: var(--color-bg-elevated);
|
|
477
477
|
}
|
|
478
478
|
.\!input::-moz-placeholder {
|
|
479
|
-
color: var(--color-text-muted)
|
|
479
|
+
color: var(--color-text-muted);
|
|
480
480
|
}
|
|
481
481
|
.\!input::placeholder {
|
|
482
|
-
color: var(--color-text-muted)
|
|
482
|
+
color: var(--color-text-muted);
|
|
483
483
|
}
|
|
484
484
|
.input::-moz-placeholder {
|
|
485
485
|
color: var(--color-text-muted);
|
package/src/gui/server.ts
CHANGED
|
@@ -107,7 +107,7 @@ export function startGuiServer(opts: GuiStartOptions = {}) {
|
|
|
107
107
|
const rpcServer = opts.rpcServer;
|
|
108
108
|
const rpcUrl = `http://${host}:${rpcPort}`;
|
|
109
109
|
|
|
110
|
-
const callRpc = async (method: string, params:
|
|
110
|
+
const callRpc = async (method: string, params: unknown[] = []) => {
|
|
111
111
|
if (!rpcServer) throw new HttpError(503, "RPC server not available");
|
|
112
112
|
const response: JsonRpcResponse = await rpcServer.handleRequest({
|
|
113
113
|
jsonrpc: "2.0",
|
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,6 +166,7 @@ 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"
|
|
@@ -183,6 +199,7 @@ export function App() {
|
|
|
183
199
|
{/* Navigation Items */}
|
|
184
200
|
<nav className="space-y-2">
|
|
185
201
|
<button
|
|
202
|
+
type="button"
|
|
186
203
|
onClick={() => scrollToSection("status")}
|
|
187
204
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
|
|
188
205
|
activeSection === "status"
|
|
@@ -194,6 +211,7 @@ export function App() {
|
|
|
194
211
|
<span className="font-medium">Network Status</span>
|
|
195
212
|
</button>
|
|
196
213
|
<button
|
|
214
|
+
type="button"
|
|
197
215
|
onClick={() => scrollToSection("actions")}
|
|
198
216
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
|
|
199
217
|
activeSection === "actions"
|
|
@@ -205,6 +223,7 @@ export function App() {
|
|
|
205
223
|
<span className="font-medium">Quick Actions</span>
|
|
206
224
|
</button>
|
|
207
225
|
<button
|
|
226
|
+
type="button"
|
|
208
227
|
onClick={() => scrollToSection("programs")}
|
|
209
228
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
|
|
210
229
|
activeSection === "programs"
|
|
@@ -216,6 +235,7 @@ export function App() {
|
|
|
216
235
|
<span className="font-medium">Programs</span>
|
|
217
236
|
</button>
|
|
218
237
|
<button
|
|
238
|
+
type="button"
|
|
219
239
|
onClick={() => scrollToSection("tokens")}
|
|
220
240
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all text-left ${
|
|
221
241
|
activeSection === "tokens"
|
|
@@ -250,9 +270,16 @@ export function App() {
|
|
|
250
270
|
|
|
251
271
|
{/* Overlay for mobile */}
|
|
252
272
|
{sidebarOpen && (
|
|
253
|
-
<
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
254
275
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-30 lg:hidden"
|
|
276
|
+
aria-label="Close sidebar overlay"
|
|
255
277
|
onClick={() => setSidebarOpen(false)}
|
|
278
|
+
onKeyDown={(e) => {
|
|
279
|
+
if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
|
|
280
|
+
setSidebarOpen(false);
|
|
281
|
+
}
|
|
282
|
+
}}
|
|
256
283
|
/>
|
|
257
284
|
)}
|
|
258
285
|
|
|
@@ -270,7 +297,11 @@ export function App() {
|
|
|
270
297
|
Manage your local Solana development environment
|
|
271
298
|
</p>
|
|
272
299
|
</div>
|
|
273
|
-
<button
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={loadStatus}
|
|
303
|
+
className="btn-secondary"
|
|
304
|
+
>
|
|
274
305
|
<i
|
|
275
306
|
className={`fas fa-sync-alt ${loadingStatus ? "animate-spin" : ""}`}
|
|
276
307
|
></i>
|
|
@@ -286,6 +317,7 @@ export function App() {
|
|
|
286
317
|
<p className="text-sm text-red-300">{bannerError}</p>
|
|
287
318
|
</div>
|
|
288
319
|
<button
|
|
320
|
+
type="button"
|
|
289
321
|
onClick={() => setBannerError(null)}
|
|
290
322
|
className="text-red-400 hover:text-red-300"
|
|
291
323
|
aria-label="Close error"
|
|
@@ -309,7 +341,7 @@ export function App() {
|
|
|
309
341
|
</div>
|
|
310
342
|
|
|
311
343
|
{/* Status Panel */}
|
|
312
|
-
<div id=
|
|
344
|
+
<div id={sectionIds.status} className="animate-fadeIn scroll-mt-24">
|
|
313
345
|
<StatusPanel
|
|
314
346
|
status={status}
|
|
315
347
|
loading={loadingStatus}
|
|
@@ -319,7 +351,7 @@ export function App() {
|
|
|
319
351
|
|
|
320
352
|
{/* Quick Actions - Optional */}
|
|
321
353
|
<div
|
|
322
|
-
id=
|
|
354
|
+
id={sectionIds.actions}
|
|
323
355
|
className="glass-panel p-6 animate-fadeIn scroll-mt-24"
|
|
324
356
|
style={{ animationDelay: "0.1s" }}
|
|
325
357
|
>
|
|
@@ -333,7 +365,7 @@ export function App() {
|
|
|
333
365
|
{/* Programs and Tokens Stacked */}
|
|
334
366
|
<div className="space-y-6">
|
|
335
367
|
<div
|
|
336
|
-
id=
|
|
368
|
+
id={sectionIds.programs}
|
|
337
369
|
className="animate-fadeIn scroll-mt-24"
|
|
338
370
|
style={{ animationDelay: "0.2s" }}
|
|
339
371
|
>
|
|
@@ -345,7 +377,7 @@ export function App() {
|
|
|
345
377
|
/>
|
|
346
378
|
</div>
|
|
347
379
|
<div
|
|
348
|
-
id=
|
|
380
|
+
id={sectionIds.tokens}
|
|
349
381
|
className="animate-fadeIn scroll-mt-24"
|
|
350
382
|
style={{ animationDelay: "0.3s" }}
|
|
351
383
|
>
|
|
@@ -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
|
}
|
|
@@ -116,11 +128,15 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
|
|
|
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)
|
|
@@ -133,11 +149,15 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
|
|
|
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)
|
|
@@ -155,11 +175,15 @@ export function AirdropMintForm({ tokens, onAirdrop, onMint }: Props) {
|
|
|
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)
|
|
@@ -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
|
}
|
|
@@ -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)
|
|
@@ -109,11 +120,15 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
|
|
|
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)
|
|
@@ -147,11 +162,15 @@ export function CloneProgramModal({ isOpen, onClose, onSubmit }: Props) {
|
|
|
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)
|