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,256 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { defaultConfig, type SolforgeConfig } from "../config";
|
|
3
|
+
import {
|
|
4
|
+
cancelSetup,
|
|
5
|
+
collectCustomEntries,
|
|
6
|
+
ensure,
|
|
7
|
+
validatePort,
|
|
8
|
+
validatePositiveNumber,
|
|
9
|
+
validatePubkey,
|
|
10
|
+
} from "./setup-utils";
|
|
11
|
+
|
|
12
|
+
const TOKEN_PRESETS = [
|
|
13
|
+
{
|
|
14
|
+
value: "usdc",
|
|
15
|
+
label: "USDC",
|
|
16
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: "usdt",
|
|
20
|
+
label: "USDT",
|
|
21
|
+
mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const PROGRAM_PRESETS = [
|
|
25
|
+
{
|
|
26
|
+
value: "jupiter",
|
|
27
|
+
label: "Jupiter",
|
|
28
|
+
programId: "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
value: "pump",
|
|
32
|
+
label: "Pump core",
|
|
33
|
+
programId: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: "pump-amm",
|
|
37
|
+
label: "Pump AMM",
|
|
38
|
+
programId: "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: "pump-fees",
|
|
42
|
+
label: "Pump fees",
|
|
43
|
+
programId: "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
export async function runSetupWizard(existing: SolforgeConfig = defaultConfig) {
|
|
47
|
+
const base: SolforgeConfig = JSON.parse(JSON.stringify(existing));
|
|
48
|
+
|
|
49
|
+
const rpcPort = Number(
|
|
50
|
+
ensure(
|
|
51
|
+
await p.text({
|
|
52
|
+
message: "RPC port",
|
|
53
|
+
initialValue: String(
|
|
54
|
+
base.server.rpcPort ?? defaultConfig.server.rpcPort,
|
|
55
|
+
),
|
|
56
|
+
validate: validatePort,
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const wsPort = Number(
|
|
62
|
+
ensure(
|
|
63
|
+
await p.text({
|
|
64
|
+
message: "WebSocket port",
|
|
65
|
+
initialValue: String(base.server.wsPort ?? rpcPort + 1),
|
|
66
|
+
validate: validatePort,
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const guiEnabledResp = await p.confirm({
|
|
72
|
+
message: "Enable GUI server?",
|
|
73
|
+
initialValue: base.gui?.enabled ?? defaultConfig.gui.enabled,
|
|
74
|
+
});
|
|
75
|
+
if (p.isCancel(guiEnabledResp)) cancelSetup();
|
|
76
|
+
const guiEnabled = guiEnabledResp !== false;
|
|
77
|
+
|
|
78
|
+
let guiPort = base.gui?.port ?? defaultConfig.gui.port;
|
|
79
|
+
if (guiEnabled) {
|
|
80
|
+
guiPort = Number(
|
|
81
|
+
ensure(
|
|
82
|
+
await p.text({
|
|
83
|
+
message: "GUI port",
|
|
84
|
+
initialValue: String(guiPort ?? defaultConfig.gui.port),
|
|
85
|
+
validate: validatePort,
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const endpoint = ensure(
|
|
92
|
+
await p.text({
|
|
93
|
+
message: "Source RPC endpoint for cloning",
|
|
94
|
+
initialValue: base.clone.endpoint || defaultConfig.clone.endpoint,
|
|
95
|
+
placeholder: defaultConfig.clone.endpoint,
|
|
96
|
+
validate: (value) => (value ? undefined : "Endpoint is required"),
|
|
97
|
+
}),
|
|
98
|
+
).trim();
|
|
99
|
+
|
|
100
|
+
const tokenSelection = ensure(
|
|
101
|
+
await p.multiselect({
|
|
102
|
+
message: "Which tokens should be cloned into LiteSVM?",
|
|
103
|
+
options: [
|
|
104
|
+
...TOKEN_PRESETS.map((token) => ({
|
|
105
|
+
value: token.value,
|
|
106
|
+
label: `${token.label} (${token.mint})`,
|
|
107
|
+
})),
|
|
108
|
+
{ value: "__custom__", label: "Add custom token mint" },
|
|
109
|
+
],
|
|
110
|
+
initialValues: TOKEN_PRESETS.filter((preset) =>
|
|
111
|
+
base.clone.tokens?.includes(preset.mint),
|
|
112
|
+
).map((preset) => preset.value),
|
|
113
|
+
required: false,
|
|
114
|
+
}),
|
|
115
|
+
) as string[];
|
|
116
|
+
let tokenSeed: string[] = [];
|
|
117
|
+
if ((base.clone.tokens?.length ?? 0) > 0) {
|
|
118
|
+
const keep = await p.confirm({
|
|
119
|
+
message: `Keep existing token list (${base.clone.tokens.length})?`,
|
|
120
|
+
initialValue: true,
|
|
121
|
+
});
|
|
122
|
+
if (p.isCancel(keep)) cancelSetup();
|
|
123
|
+
if (keep) tokenSeed = [...base.clone.tokens];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tokens = await resolveTokens(tokenSelection, tokenSeed);
|
|
127
|
+
|
|
128
|
+
const programSelection = ensure(
|
|
129
|
+
await p.multiselect({
|
|
130
|
+
message: "Clone any on-chain programs?",
|
|
131
|
+
options: [
|
|
132
|
+
...PROGRAM_PRESETS.map((program) => ({
|
|
133
|
+
value: program.value,
|
|
134
|
+
label: `${program.label} (${program.programId})`,
|
|
135
|
+
})),
|
|
136
|
+
{ value: "__custom__", label: "Add custom program" },
|
|
137
|
+
],
|
|
138
|
+
initialValues: PROGRAM_PRESETS.filter((preset) =>
|
|
139
|
+
base.clone.programs?.includes(preset.programId),
|
|
140
|
+
).map((preset) => preset.value),
|
|
141
|
+
required: false,
|
|
142
|
+
}),
|
|
143
|
+
) as string[];
|
|
144
|
+
let programSeed: string[] = [];
|
|
145
|
+
if ((base.clone.programs?.length ?? 0) > 0) {
|
|
146
|
+
const keepPrograms = await p.confirm({
|
|
147
|
+
message: `Keep existing program list (${base.clone.programs.length})?`,
|
|
148
|
+
initialValue: true,
|
|
149
|
+
});
|
|
150
|
+
if (p.isCancel(keepPrograms)) cancelSetup();
|
|
151
|
+
if (keepPrograms) programSeed = [...base.clone.programs];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const programs = await resolvePrograms(programSelection, programSeed);
|
|
155
|
+
|
|
156
|
+
const airdrops = await collectAirdrops(base.bootstrap?.airdrops ?? []);
|
|
157
|
+
|
|
158
|
+
base.server.rpcPort = rpcPort;
|
|
159
|
+
base.server.wsPort = wsPort;
|
|
160
|
+
base.gui = { enabled: guiEnabled, port: guiPort ?? defaultConfig.gui.port };
|
|
161
|
+
base.clone.endpoint = endpoint;
|
|
162
|
+
base.clone.tokens = tokens;
|
|
163
|
+
base.clone.programs = programs;
|
|
164
|
+
base.bootstrap = { airdrops };
|
|
165
|
+
|
|
166
|
+
return base;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function resolveTokens(selections: string[], existing: string[] = []) {
|
|
170
|
+
const set = new Set(existing);
|
|
171
|
+
for (const selection of selections) {
|
|
172
|
+
if (selection === "__custom__") {
|
|
173
|
+
(await collectCustomEntries("token mint address")).forEach((value) =>
|
|
174
|
+
set.add(value),
|
|
175
|
+
);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const preset = TOKEN_PRESETS.find((token) => token.value === selection);
|
|
179
|
+
if (!preset) continue;
|
|
180
|
+
const mint = ensure(
|
|
181
|
+
await p.text({
|
|
182
|
+
message: `Mint address for ${preset.label}`,
|
|
183
|
+
initialValue: preset.mint,
|
|
184
|
+
validate: validatePubkey,
|
|
185
|
+
}),
|
|
186
|
+
).trim();
|
|
187
|
+
set.add(mint);
|
|
188
|
+
}
|
|
189
|
+
return Array.from(set);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function resolvePrograms(selections: string[], existing: string[] = []) {
|
|
193
|
+
const set = new Set(existing);
|
|
194
|
+
for (const selection of selections) {
|
|
195
|
+
if (selection === "__custom__") {
|
|
196
|
+
(await collectCustomEntries("program id")).forEach((value) =>
|
|
197
|
+
set.add(value),
|
|
198
|
+
);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const preset = PROGRAM_PRESETS.find(
|
|
202
|
+
(program) => program.value === selection,
|
|
203
|
+
);
|
|
204
|
+
if (!preset) continue;
|
|
205
|
+
const programId = ensure(
|
|
206
|
+
await p.text({
|
|
207
|
+
message: `Program ID for ${preset.label}`,
|
|
208
|
+
initialValue: preset.programId,
|
|
209
|
+
validate: validatePubkey,
|
|
210
|
+
}),
|
|
211
|
+
).trim();
|
|
212
|
+
set.add(programId);
|
|
213
|
+
}
|
|
214
|
+
return Array.from(set);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function collectAirdrops(
|
|
218
|
+
existing: Array<{ address: string; amountSol: number }>,
|
|
219
|
+
) {
|
|
220
|
+
const entries: Array<{ address: string; amountSol: number }> = [];
|
|
221
|
+
if (existing.length > 0) {
|
|
222
|
+
const keep = await p.confirm({
|
|
223
|
+
message: `Keep existing airdrop recipients (${existing.length})?`,
|
|
224
|
+
initialValue: true,
|
|
225
|
+
});
|
|
226
|
+
if (p.isCancel(keep)) cancelSetup();
|
|
227
|
+
if (keep) entries.push(...existing);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
while (true) {
|
|
231
|
+
const address = await p.text({
|
|
232
|
+
message:
|
|
233
|
+
entries.length === 0
|
|
234
|
+
? "Airdrop recipient address (leave blank to skip)"
|
|
235
|
+
: "Add another airdrop recipient (leave blank to finish)",
|
|
236
|
+
});
|
|
237
|
+
if (p.isCancel(address)) cancelSetup();
|
|
238
|
+
const trimmed = typeof address === "string" ? address.trim() : "";
|
|
239
|
+
if (!trimmed) break;
|
|
240
|
+
const error = validatePubkey(trimmed);
|
|
241
|
+
if (error) {
|
|
242
|
+
p.log.error(error);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const amount = ensure(
|
|
246
|
+
await p.text({
|
|
247
|
+
message: `Amount of SOL to airdrop to ${trimmed}`,
|
|
248
|
+
initialValue: "100",
|
|
249
|
+
validate: validatePositiveNumber,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
entries.push({ address: trimmed, amountSol: Number(amount) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return entries;
|
|
256
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function parseFlags(args: string[]) {
|
|
2
|
+
const flags: Record<string, string | boolean> = {};
|
|
3
|
+
const rest: string[] = [];
|
|
4
|
+
for (let i = 0; i < args.length; i++) {
|
|
5
|
+
const a = args[i];
|
|
6
|
+
if (a.startsWith("--")) {
|
|
7
|
+
const [k, v] = a.slice(2).split("=");
|
|
8
|
+
if (typeof v === "string" && v.length > 0) flags[k] = v;
|
|
9
|
+
else if (i + 1 < args.length && !args[i + 1].startsWith("-"))
|
|
10
|
+
flags[k] = args[++i];
|
|
11
|
+
else flags[k] = true;
|
|
12
|
+
} else rest.push(a);
|
|
13
|
+
}
|
|
14
|
+
return { flags, rest };
|
|
15
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SolforgeConfig {
|
|
5
|
+
server: {
|
|
6
|
+
rpcPort: number;
|
|
7
|
+
wsPort: number;
|
|
8
|
+
db: { mode: "ephemeral" | "persistent"; path: string };
|
|
9
|
+
};
|
|
10
|
+
svm: { initialLamports: string; faucetSOL: number };
|
|
11
|
+
clone: {
|
|
12
|
+
endpoint: string;
|
|
13
|
+
programs: string[];
|
|
14
|
+
tokens: string[];
|
|
15
|
+
programAccounts: Array<{
|
|
16
|
+
programId: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
filters?: unknown[];
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
gui: { enabled: boolean; port: number };
|
|
22
|
+
bootstrap: {
|
|
23
|
+
airdrops: Array<{ address: string; amountSol: number }>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const defaultConfig: SolforgeConfig = {
|
|
28
|
+
server: {
|
|
29
|
+
rpcPort: 8899,
|
|
30
|
+
wsPort: 8900,
|
|
31
|
+
db: { mode: "ephemeral", path: ".solforge/db.db" },
|
|
32
|
+
},
|
|
33
|
+
svm: { initialLamports: "1000000000000000", faucetSOL: 1000 },
|
|
34
|
+
clone: {
|
|
35
|
+
endpoint: "https://api.mainnet-beta.solana.com",
|
|
36
|
+
programs: [],
|
|
37
|
+
tokens: [],
|
|
38
|
+
programAccounts: [],
|
|
39
|
+
},
|
|
40
|
+
gui: { enabled: true, port: 42069 },
|
|
41
|
+
bootstrap: { airdrops: [] },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export async function readConfig(path?: string): Promise<SolforgeConfig> {
|
|
45
|
+
const p = path || "sf.config.json";
|
|
46
|
+
try {
|
|
47
|
+
const t = await Bun.file(p).text();
|
|
48
|
+
const json = JSON.parse(t);
|
|
49
|
+
return deepMerge(defaultConfig, json);
|
|
50
|
+
} catch {
|
|
51
|
+
return defaultConfig;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function writeDefaultConfig(opts: { force?: boolean } = {}) {
|
|
56
|
+
const p = "sf.config.json";
|
|
57
|
+
try {
|
|
58
|
+
if (!opts.force)
|
|
59
|
+
await Bun.file(p)
|
|
60
|
+
.text()
|
|
61
|
+
.then(() => {
|
|
62
|
+
throw new Error("exists");
|
|
63
|
+
});
|
|
64
|
+
} catch {}
|
|
65
|
+
const dir = dirname(p);
|
|
66
|
+
if (dir && dir !== ".") {
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
writeFileSync(p, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function writeConfig(
|
|
75
|
+
config: SolforgeConfig,
|
|
76
|
+
path = "sf.config.json",
|
|
77
|
+
) {
|
|
78
|
+
const dir = dirname(path);
|
|
79
|
+
if (dir && dir !== ".") {
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(dir, { recursive: true });
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
await Bun.write(path, JSON.stringify(config, null, 2) + "\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getConfigValue(cfg: any, path?: string) {
|
|
88
|
+
if (!path) return cfg;
|
|
89
|
+
return path.split(".").reduce((o, k) => (o ? o[k] : undefined), cfg);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function setConfigValue<T extends Record<string, any>>(
|
|
93
|
+
cfg: T,
|
|
94
|
+
path: string,
|
|
95
|
+
value: any,
|
|
96
|
+
): T {
|
|
97
|
+
const parts = path.split(".");
|
|
98
|
+
let node: any = cfg;
|
|
99
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
100
|
+
const k = parts[i];
|
|
101
|
+
if (!node[k] || typeof node[k] !== "object") node[k] = {};
|
|
102
|
+
node = node[k];
|
|
103
|
+
}
|
|
104
|
+
node[parts[parts.length - 1]] = coerceValue(value);
|
|
105
|
+
return cfg;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function coerceValue(v: any) {
|
|
109
|
+
if (v === "true") return true;
|
|
110
|
+
if (v === "false") return false;
|
|
111
|
+
if (v !== "" && !isNaN(Number(v))) return Number(v);
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(v);
|
|
114
|
+
} catch {
|
|
115
|
+
return v;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function deepMerge<T>(a: T, b: Partial<T>): T {
|
|
120
|
+
if (Array.isArray(a) || Array.isArray(b)) return (b as any) ?? (a as any);
|
|
121
|
+
if (typeof a === "object" && typeof b === "object" && a && b) {
|
|
122
|
+
const out: any = { ...a };
|
|
123
|
+
for (const [k, v] of Object.entries(b)) {
|
|
124
|
+
const ak = (a as any)[k];
|
|
125
|
+
out[k] = deepMerge(ak, v as any);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
return (b as any) ?? (a as any);
|
|
130
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Database from "bun:sqlite";
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
6
|
+
import { bundledMigrations } from "../migrations-bundled";
|
|
7
|
+
import * as schema from "./schema";
|
|
8
|
+
|
|
9
|
+
// DB path is configurable; default to project-local hidden folder
|
|
10
|
+
const DB_PATH = process.env.SOLFORGE_DB_PATH || ".solforge/db.db";
|
|
11
|
+
|
|
12
|
+
// Ensure directory exists
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
15
|
+
} catch {}
|
|
16
|
+
|
|
17
|
+
// Ephemeral by default (on-disk SQLite, cleared on each start).
|
|
18
|
+
// Set SOLFORGE_DB_MODE=persistent or SOLFORGE_DB_PERSIST=1 for persistent DB.
|
|
19
|
+
const PERSIST =
|
|
20
|
+
process.env.SOLFORGE_DB_MODE === "persistent" ||
|
|
21
|
+
process.env.SOLFORGE_DB_PERSIST === "1";
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
24
|
+
} catch {}
|
|
25
|
+
if (!PERSIST && DB_PATH !== ":memory:") {
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(DB_PATH)) unlinkSync(DB_PATH);
|
|
28
|
+
} catch {}
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(DB_PATH + "-wal")) unlinkSync(DB_PATH + "-wal");
|
|
31
|
+
} catch {}
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(DB_PATH + "-shm")) unlinkSync(DB_PATH + "-shm");
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create SQLite connection and apply performance PRAGMAs
|
|
38
|
+
export const sqlite = new Database(DB_PATH);
|
|
39
|
+
try {
|
|
40
|
+
// Use DELETE journal to avoid VNODE/WAL issues across environments
|
|
41
|
+
sqlite.exec("PRAGMA journal_mode=DELETE;");
|
|
42
|
+
sqlite.exec("PRAGMA synchronous=NORMAL;");
|
|
43
|
+
sqlite.exec("PRAGMA temp_store=MEMORY;");
|
|
44
|
+
sqlite.exec("PRAGMA busy_timeout=1000;");
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
// Drizzle database instance with typed schema
|
|
48
|
+
export const db = drizzle(sqlite, { schema });
|
|
49
|
+
|
|
50
|
+
export type { SQLiteDatabase } from "drizzle-orm/sqlite-core";
|
|
51
|
+
export * as dbSchema from "./schema";
|
|
52
|
+
|
|
53
|
+
// Run Drizzle migrations on app start (Bun + SQLite)
|
|
54
|
+
const migrationsFolder = process.env.DRIZZLE_MIGRATIONS || "drizzle";
|
|
55
|
+
try {
|
|
56
|
+
// Prefer folder-based migrations when available (dev/uncompiled)
|
|
57
|
+
if (existsSync(migrationsFolder)) {
|
|
58
|
+
await migrate(db, { migrationsFolder });
|
|
59
|
+
console.log("✅ Database migrations completed (folder)");
|
|
60
|
+
} else {
|
|
61
|
+
// Bundled mode: apply embedded SQL files if schema isn't present
|
|
62
|
+
const haveTx = sqlite
|
|
63
|
+
.query(
|
|
64
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='transactions'",
|
|
65
|
+
)
|
|
66
|
+
.get() as { name?: string } | undefined;
|
|
67
|
+
if (!haveTx?.name) {
|
|
68
|
+
for (const m of bundledMigrations) {
|
|
69
|
+
try {
|
|
70
|
+
const sql = await Bun.file(m.path).text();
|
|
71
|
+
sqlite.exec(sql);
|
|
72
|
+
console.log(`✅ Applied bundled migration: ${m.name}`);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(`❌ Failed bundled migration: ${m.name}`, e);
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log("✅ Database migrations completed (bundled)");
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("❌ Database migration failed:", error);
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const accounts = sqliteTable(
|
|
4
|
+
"accounts",
|
|
5
|
+
{
|
|
6
|
+
address: text("address").primaryKey(),
|
|
7
|
+
lamports: integer("lamports").notNull(),
|
|
8
|
+
ownerProgram: text("owner_program").notNull(),
|
|
9
|
+
executable: integer("executable").notNull(), // 0 or 1
|
|
10
|
+
rentEpoch: integer("rent_epoch").notNull(),
|
|
11
|
+
dataLen: integer("data_len").notNull(),
|
|
12
|
+
// Optional raw bytes; disabled by default in runtime
|
|
13
|
+
dataBase64: text("data_base64"),
|
|
14
|
+
lastSlot: integer("last_slot").notNull(),
|
|
15
|
+
},
|
|
16
|
+
(t) => ({
|
|
17
|
+
ownerIdx: index("idx_accounts_owner").on(t.ownerProgram),
|
|
18
|
+
lastSlotIdx: index("idx_accounts_last_slot").on(t.lastSlot),
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export type AccountRow = typeof accounts.$inferSelect;
|
|
23
|
+
export type NewAccountRow = typeof accounts.$inferInsert;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
integer,
|
|
4
|
+
primaryKey,
|
|
5
|
+
sqliteTable,
|
|
6
|
+
text,
|
|
7
|
+
} from "drizzle-orm/sqlite-core";
|
|
8
|
+
|
|
9
|
+
export const addressSignatures = sqliteTable(
|
|
10
|
+
"address_signatures",
|
|
11
|
+
{
|
|
12
|
+
address: text("address").notNull(),
|
|
13
|
+
signature: text("signature").notNull(),
|
|
14
|
+
slot: integer("slot").notNull(),
|
|
15
|
+
err: integer("err").notNull(), // 0 or 1
|
|
16
|
+
blockTime: integer("block_time"),
|
|
17
|
+
},
|
|
18
|
+
(t) => ({
|
|
19
|
+
pk: primaryKey({
|
|
20
|
+
columns: [t.address, t.signature],
|
|
21
|
+
name: "pk_address_signatures",
|
|
22
|
+
}),
|
|
23
|
+
addressSlotIdx: index("idx_address_signatures_addr_slot").on(
|
|
24
|
+
t.address,
|
|
25
|
+
t.slot,
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export type AddressSignatureRow = typeof addressSignatures.$inferSelect;
|
|
31
|
+
export type NewAddressSignatureRow = typeof addressSignatures.$inferInsert;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const metaKv = sqliteTable("meta_kv", {
|
|
4
|
+
key: text("key").primaryKey(),
|
|
5
|
+
value: text("value").notNull(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type MetaKvRow = typeof metaKv.$inferSelect;
|
|
9
|
+
export type NewMetaKvRow = typeof metaKv.$inferInsert;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const transactions = sqliteTable(
|
|
4
|
+
"transactions",
|
|
5
|
+
{
|
|
6
|
+
signature: text("signature").primaryKey(),
|
|
7
|
+
slot: integer("slot").notNull(),
|
|
8
|
+
blockTime: integer("block_time"),
|
|
9
|
+
version: text("version").notNull(), // 0 | "legacy"
|
|
10
|
+
errJson: text("err_json"),
|
|
11
|
+
fee: integer("fee").notNull(),
|
|
12
|
+
rawBase64: text("raw_base64").notNull(),
|
|
13
|
+
preBalancesJson: text("pre_balances_json").notNull(),
|
|
14
|
+
postBalancesJson: text("post_balances_json").notNull(),
|
|
15
|
+
logsJson: text("logs_json").notNull(),
|
|
16
|
+
preTokenBalancesJson: text("pre_token_balances_json")
|
|
17
|
+
.default("[]")
|
|
18
|
+
.notNull(),
|
|
19
|
+
postTokenBalancesJson: text("post_token_balances_json")
|
|
20
|
+
.default("[]")
|
|
21
|
+
.notNull(),
|
|
22
|
+
},
|
|
23
|
+
(t) => ({
|
|
24
|
+
slotIdx: index("idx_transactions_slot").on(t.slot),
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export type TransactionRow = typeof transactions.$inferSelect;
|
|
29
|
+
export type NewTransactionRow = typeof transactions.$inferInsert;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
integer,
|
|
4
|
+
primaryKey,
|
|
5
|
+
sqliteTable,
|
|
6
|
+
text,
|
|
7
|
+
} from "drizzle-orm/sqlite-core";
|
|
8
|
+
|
|
9
|
+
export const txAccounts = sqliteTable(
|
|
10
|
+
"tx_accounts",
|
|
11
|
+
{
|
|
12
|
+
signature: text("signature").notNull(),
|
|
13
|
+
accountIndex: integer("account_index").notNull(),
|
|
14
|
+
address: text("address").notNull(),
|
|
15
|
+
signer: integer("signer").notNull(), // 0 or 1
|
|
16
|
+
writable: integer("writable").notNull(), // 0 or 1
|
|
17
|
+
programIdIndex: integer("program_id_index"),
|
|
18
|
+
},
|
|
19
|
+
(t) => ({
|
|
20
|
+
pk: primaryKey({
|
|
21
|
+
columns: [t.signature, t.accountIndex],
|
|
22
|
+
name: "pk_tx_accounts",
|
|
23
|
+
}),
|
|
24
|
+
addressIdx: index("idx_tx_accounts_address").on(t.address),
|
|
25
|
+
addressSigIdx: index("idx_tx_accounts_address_signature").on(
|
|
26
|
+
t.address,
|
|
27
|
+
t.signature,
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type TxAccountRow = typeof txAccounts.$inferSelect;
|
|
33
|
+
export type NewTxAccountRow = typeof txAccounts.$inferInsert;
|