solforge 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +367 -393
- package/docs/API.md +379 -0
- package/docs/CONFIGURATION.md +407 -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/package.json +38 -51
- 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/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/build/main.css +1 -0
- package/src/gui/public/build/main.js +303 -0
- package/src/gui/public/build/main.js.txt +231 -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/scripts/postinstall.cjs +0 -103
- package/tsconfig.json +0 -28
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { readConfig, writeConfig } from "../../config";
|
|
3
|
+
import { parseFlags } from "../utils/args";
|
|
4
|
+
|
|
5
|
+
// Simplified token "clone": create an ATA locally with the requested amount.
|
|
6
|
+
// Usage:
|
|
7
|
+
// solforge token clone <mint> --to <owner> --amount <baseUnits>
|
|
8
|
+
// solforge token clone <mint> --to <owner> --ui-amount <num> --decimals <d>
|
|
9
|
+
export async function tokenCloneCommand(args: string[]) {
|
|
10
|
+
const { flags, rest } = parseFlags(args);
|
|
11
|
+
const mint = ((rest[0] as string) || (flags["mint"] as string) || "").trim();
|
|
12
|
+
if (!mint) {
|
|
13
|
+
p.log.error(
|
|
14
|
+
"Usage: solforge token clone <mint> [--amount <baseUnits> | --ui-amount <num>] [--endpoint URL]",
|
|
15
|
+
);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const owner = flags["to"] as string | undefined; // optional; defaults to faucet on server
|
|
20
|
+
const configPath = flags["config"] as string | undefined;
|
|
21
|
+
const cfg = await readConfig(configPath);
|
|
22
|
+
const endpoint = (flags["endpoint"] as string) || cfg.clone.endpoint;
|
|
23
|
+
const url = `http://localhost:${cfg.server.rpcPort}`;
|
|
24
|
+
const s = p.spinner();
|
|
25
|
+
s.start("Cloning mint into LiteSVM...");
|
|
26
|
+
try {
|
|
27
|
+
// First, mirror the mint account so downstream reads work
|
|
28
|
+
const resMint = await fetch(url, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "content-type": "application/json" },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
id: 1,
|
|
34
|
+
method: "solforgeAdminCloneTokenMint",
|
|
35
|
+
params: [mint, { endpoint }],
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
const jsonMint = await resMint.json();
|
|
39
|
+
if (jsonMint.error) {
|
|
40
|
+
const data = jsonMint.error.data
|
|
41
|
+
? `\nDetails: ${JSON.stringify(jsonMint.error.data)}`
|
|
42
|
+
: "";
|
|
43
|
+
throw new Error((jsonMint.error.message || "clone mint failed") + data);
|
|
44
|
+
}
|
|
45
|
+
// Record mint in config immediately after a successful clone
|
|
46
|
+
await recordTokenClone(configPath, mint);
|
|
47
|
+
// Try to adopt faucet as mint authority for local usage (do not fail the command if this step fails)
|
|
48
|
+
try {
|
|
49
|
+
s.message("Adopting faucet as authority...");
|
|
50
|
+
const resAdopt = await fetch(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
jsonrpc: "2.0",
|
|
55
|
+
id: 3,
|
|
56
|
+
method: "solforgeAdoptMintAuthority",
|
|
57
|
+
params: [mint],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const jsonAdopt = await resAdopt.json();
|
|
61
|
+
if (jsonAdopt.error) {
|
|
62
|
+
p.log.warn(jsonAdopt.error.message || "adopt authority failed");
|
|
63
|
+
s.stop("Mint cloned (authority unchanged)");
|
|
64
|
+
console.log(
|
|
65
|
+
JSON.stringify({ mint: jsonMint.result, adopt: null }, null, 2),
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
s.stop("Mint cloned and authority adopted");
|
|
70
|
+
console.log(
|
|
71
|
+
JSON.stringify(
|
|
72
|
+
{ mint: jsonMint.result, adopt: jsonAdopt.result },
|
|
73
|
+
null,
|
|
74
|
+
2,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
} catch (adoptErr: any) {
|
|
79
|
+
p.log.warn(
|
|
80
|
+
`Adopt authority failed: ${adoptErr?.message || String(adoptErr)}`,
|
|
81
|
+
);
|
|
82
|
+
s.stop("Mint cloned (authority unchanged)");
|
|
83
|
+
console.log(
|
|
84
|
+
JSON.stringify({ mint: jsonMint.result, adopt: null }, null, 2),
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
s.stop("Clone failed");
|
|
91
|
+
p.log.error(String(e));
|
|
92
|
+
p.log.info(
|
|
93
|
+
"Token clone mirrors an on-chain mint and requires network access to --endpoint. For an offline token, use 'solforge token create'.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// intentionally no UI conversion here; clone mirrors the on-chain mint only
|
|
99
|
+
|
|
100
|
+
async function recordTokenClone(configPath: string | undefined, mint: string) {
|
|
101
|
+
try {
|
|
102
|
+
const cfg = await readConfig(configPath);
|
|
103
|
+
const next = new Set(cfg.clone.tokens ?? []);
|
|
104
|
+
if (!next.has(mint)) {
|
|
105
|
+
next.add(mint);
|
|
106
|
+
cfg.clone.tokens = Array.from(next);
|
|
107
|
+
await writeConfig(cfg, configPath ?? "sf.config.json");
|
|
108
|
+
p.log.info(`Added ${mint} to clone tokens in config`);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(`[config] Failed to update clone tokens: ${String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { readConfig } from "../../config";
|
|
3
|
+
import { parseFlags } from "../utils/args";
|
|
4
|
+
|
|
5
|
+
// Create a new local SPL mint with given decimals and (optional) owner authority.
|
|
6
|
+
// Also optionally create the owner's ATA with a starting balance (base units or UI amount).
|
|
7
|
+
// Usage:
|
|
8
|
+
// solforge token create --decimals <d> --owner <pubkey> [--mint <pubkey>] [--amount <baseUnits> | --ui-amount <num>]
|
|
9
|
+
export async function tokenCreateCommand(args: string[]) {
|
|
10
|
+
const { flags } = parseFlags(args);
|
|
11
|
+
const decimals = flags["decimals"] ? Number(flags["decimals"]) : NaN;
|
|
12
|
+
const owner = flags["owner"] as string | undefined;
|
|
13
|
+
const mint = flags["mint"] as string | undefined;
|
|
14
|
+
const amountBase = flags["amount"] as string | undefined;
|
|
15
|
+
const uiAmount = flags["ui-amount"] as string | undefined;
|
|
16
|
+
|
|
17
|
+
if (!isFinite(decimals)) {
|
|
18
|
+
p.log.error("--decimals is required (0-18)");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!owner) {
|
|
22
|
+
p.log.error("--owner <pubkey> is required");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cfg = await readConfig();
|
|
27
|
+
const url = `http://localhost:${cfg.server.rpcPort}`;
|
|
28
|
+
const s = p.spinner();
|
|
29
|
+
s.start("Creating mint...");
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
jsonrpc: "2.0",
|
|
36
|
+
id: 1,
|
|
37
|
+
method: "solforgeCreateMint",
|
|
38
|
+
params: [mint ?? null, Number(decimals), owner],
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
const json = await res.json();
|
|
42
|
+
if (json.error) throw new Error(json.error.message || "create mint failed");
|
|
43
|
+
const createdMint = json.result.mint as string;
|
|
44
|
+
|
|
45
|
+
if (amountBase || uiAmount) {
|
|
46
|
+
s.message("Creating owner ATA with balance...");
|
|
47
|
+
const base =
|
|
48
|
+
amountBase ??
|
|
49
|
+
(uiAmount ? toBaseUnits(uiAmount, Number(decimals)) : undefined);
|
|
50
|
+
const res2 = await fetch(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
jsonrpc: "2.0",
|
|
55
|
+
id: 2,
|
|
56
|
+
method: "solforgeCreateTokenAccount",
|
|
57
|
+
params: [createdMint, owner, String(base)],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const json2 = await res2.json();
|
|
61
|
+
if (json2.error)
|
|
62
|
+
throw new Error(json2.error.message || "create token account failed");
|
|
63
|
+
s.stop("Token created");
|
|
64
|
+
console.log(
|
|
65
|
+
JSON.stringify({ mint: json.result, account: json2.result }, null, 2),
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
s.stop("Mint created");
|
|
70
|
+
console.log(JSON.stringify(json.result, null, 2));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
s.stop("Create failed");
|
|
73
|
+
p.log.error(String(e));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toBaseUnits(ui: string, decimals: number): string {
|
|
78
|
+
const [i, f = ""] = String(ui).split(".");
|
|
79
|
+
const frac = (f + "0".repeat(decimals)).slice(0, decimals);
|
|
80
|
+
return BigInt(i + (decimals ? frac : "")).toString();
|
|
81
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Minimal, fast CLI router with @clack/prompts for UX
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
// Robust arg parsing for both bun script and compiled binary
|
|
5
|
+
const known = new Set([
|
|
6
|
+
"help",
|
|
7
|
+
"-h",
|
|
8
|
+
"--help",
|
|
9
|
+
"rpc",
|
|
10
|
+
"start",
|
|
11
|
+
"config",
|
|
12
|
+
"airdrop",
|
|
13
|
+
"mint",
|
|
14
|
+
"token",
|
|
15
|
+
"program",
|
|
16
|
+
]);
|
|
17
|
+
const raw = Bun.argv.slice(1);
|
|
18
|
+
const firstIdx = raw.findIndex((a) => known.has(String(a)));
|
|
19
|
+
const argv = firstIdx >= 0 ? raw.slice(firstIdx) : [];
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const [cmd, sub, ...rest] = argv;
|
|
23
|
+
|
|
24
|
+
if (!cmd) {
|
|
25
|
+
const { runSolforge } = await import("./run-solforge");
|
|
26
|
+
await runSolforge();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (cmd === "help" || cmd === "-h" || cmd === "--help") {
|
|
31
|
+
printHelp();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Alias: solforge start -> solforge rpc start
|
|
36
|
+
if (cmd === "start") {
|
|
37
|
+
const { rpcStartCommand } = await import("./commands/rpc-start");
|
|
38
|
+
await rpcStartCommand(rest);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (cmd) {
|
|
43
|
+
case "rpc": {
|
|
44
|
+
if (sub === "start") {
|
|
45
|
+
const { rpcStartCommand } = await import("./commands/rpc-start");
|
|
46
|
+
return rpcStartCommand(rest);
|
|
47
|
+
}
|
|
48
|
+
return unknownCommand([cmd, sub]);
|
|
49
|
+
}
|
|
50
|
+
case "config": {
|
|
51
|
+
const { configCommand } = await import("./commands/config");
|
|
52
|
+
return configCommand(sub, rest);
|
|
53
|
+
}
|
|
54
|
+
case "airdrop": {
|
|
55
|
+
const { airdropCommand } = await import("./commands/airdrop");
|
|
56
|
+
return airdropCommand(rest);
|
|
57
|
+
}
|
|
58
|
+
case "mint": {
|
|
59
|
+
const { mintCommand } = await import("./commands/mint");
|
|
60
|
+
return mintCommand(rest);
|
|
61
|
+
}
|
|
62
|
+
case "token": {
|
|
63
|
+
if (sub === "clone") {
|
|
64
|
+
const { tokenCloneCommand } = await import("./commands/token-clone");
|
|
65
|
+
return tokenCloneCommand(rest);
|
|
66
|
+
}
|
|
67
|
+
if (sub === "create") {
|
|
68
|
+
const { tokenCreateCommand } = await import("./commands/token-create");
|
|
69
|
+
return tokenCreateCommand(rest);
|
|
70
|
+
}
|
|
71
|
+
if (sub === "adopt-authority") {
|
|
72
|
+
const { tokenAdoptAuthorityCommand } = await import(
|
|
73
|
+
"./commands/token-adopt-authority"
|
|
74
|
+
);
|
|
75
|
+
return tokenAdoptAuthorityCommand(rest);
|
|
76
|
+
}
|
|
77
|
+
return unknownCommand([cmd, sub]);
|
|
78
|
+
}
|
|
79
|
+
case "program": {
|
|
80
|
+
if (sub === "clone") {
|
|
81
|
+
const { programCloneCommand } = await import(
|
|
82
|
+
"./commands/program-clone"
|
|
83
|
+
);
|
|
84
|
+
return programCloneCommand(rest);
|
|
85
|
+
}
|
|
86
|
+
if (sub === "load") {
|
|
87
|
+
const { programLoadCommand } = await import("./commands/program-load");
|
|
88
|
+
return programLoadCommand(rest);
|
|
89
|
+
}
|
|
90
|
+
if (sub === "accounts") {
|
|
91
|
+
const [_, __, ...tail] = argv.slice(2); // re-read to check deep subcommand
|
|
92
|
+
if (tail[0] === "clone") {
|
|
93
|
+
const { programAccountsCloneCommand } = await import(
|
|
94
|
+
"./commands/program-clone"
|
|
95
|
+
);
|
|
96
|
+
return programAccountsCloneCommand(tail.slice(1));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return unknownCommand([cmd, sub]);
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return unknownCommand([cmd, sub]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function printHelp() {
|
|
107
|
+
console.log(`
|
|
108
|
+
solforge <command>
|
|
109
|
+
|
|
110
|
+
Commands:
|
|
111
|
+
(no command) Run setup then start RPC & WS servers
|
|
112
|
+
rpc start Start RPC & WS servers
|
|
113
|
+
start Alias for 'rpc start'
|
|
114
|
+
config init Create sf.config.json in CWD
|
|
115
|
+
config get <key> Read a config value (dot path)
|
|
116
|
+
config set <k> <v> Set a config value
|
|
117
|
+
airdrop --to <pubkey> --sol <amount> Airdrop SOL via RPC faucet
|
|
118
|
+
mint Interactive: pick mint, receiver, amount
|
|
119
|
+
token clone <mint> Clone SPL token mint + accounts
|
|
120
|
+
program clone <programId> Clone program code (and optionally accounts)
|
|
121
|
+
program accounts clone <programId> Clone accounts owned by program
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function unknownCommand(parts: (string | undefined)[]) {
|
|
126
|
+
p.log.error(`Unknown command: ${parts.filter(Boolean).join(" ")}`);
|
|
127
|
+
printHelp();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import {
|
|
3
|
+
defaultConfig,
|
|
4
|
+
readConfig,
|
|
5
|
+
type SolforgeConfig,
|
|
6
|
+
writeConfig,
|
|
7
|
+
} from "../config";
|
|
8
|
+
import { startRpcServers } from "../rpc/start";
|
|
9
|
+
import { bootstrapEnvironment } from "./bootstrap";
|
|
10
|
+
import { cancelSetup } from "./setup-utils";
|
|
11
|
+
import { runSetupWizard } from "./setup-wizard";
|
|
12
|
+
|
|
13
|
+
const CONFIG_PATH = "sf.config.json";
|
|
14
|
+
|
|
15
|
+
export async function runSolforge() {
|
|
16
|
+
const config = await ensureConfig();
|
|
17
|
+
await startWithConfig(config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ensureConfig(): Promise<SolforgeConfig> {
|
|
21
|
+
const exists = await Bun.file(CONFIG_PATH).exists();
|
|
22
|
+
if (!exists) {
|
|
23
|
+
p.intro("Solforge setup");
|
|
24
|
+
const config = await runSetupWizard();
|
|
25
|
+
await saveConfig(config);
|
|
26
|
+
p.outro("Configuration saved");
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const current = await readConfig(CONFIG_PATH);
|
|
31
|
+
const reuse = await p.confirm({
|
|
32
|
+
message: `Use existing config at ${CONFIG_PATH}?`,
|
|
33
|
+
initialValue: true,
|
|
34
|
+
});
|
|
35
|
+
if (p.isCancel(reuse)) cancelSetup();
|
|
36
|
+
|
|
37
|
+
if (reuse) return current;
|
|
38
|
+
|
|
39
|
+
const updated = await runSetupWizard(current);
|
|
40
|
+
await saveConfig(updated);
|
|
41
|
+
return updated;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function startWithConfig(config: SolforgeConfig) {
|
|
45
|
+
const host = String(process.env.RPC_HOST || "127.0.0.1");
|
|
46
|
+
const rpcPort = Number(config.server.rpcPort || defaultConfig.server.rpcPort);
|
|
47
|
+
const wsPort = Number(config.server.wsPort || rpcPort + 1);
|
|
48
|
+
const guiEnabled = config.gui?.enabled !== false;
|
|
49
|
+
const guiPort = Number(config.gui?.port ?? defaultConfig.gui.port);
|
|
50
|
+
|
|
51
|
+
const spinner = p.spinner();
|
|
52
|
+
const guiPart = guiEnabled ? `, GUI ${guiPort}` : "";
|
|
53
|
+
spinner.start(
|
|
54
|
+
`Starting RPC on ${host}:${rpcPort} (WS ${wsPort}${guiPart})...`,
|
|
55
|
+
);
|
|
56
|
+
try {
|
|
57
|
+
const started = startRpcServers({
|
|
58
|
+
rpcPort,
|
|
59
|
+
wsPort,
|
|
60
|
+
dbMode: config.server.db.mode,
|
|
61
|
+
dbPath: config.server.db.path,
|
|
62
|
+
host,
|
|
63
|
+
guiEnabled,
|
|
64
|
+
guiPort,
|
|
65
|
+
});
|
|
66
|
+
spinner.stop("RPC started");
|
|
67
|
+
|
|
68
|
+
await waitForRpc(`http://${host}:${started.rpcPort}`);
|
|
69
|
+
await bootstrapEnvironment(config, host, started.rpcPort);
|
|
70
|
+
|
|
71
|
+
p.log.success(
|
|
72
|
+
`Solforge ready ➜ HTTP http://${host}:${started.rpcPort} | WS ws://${host}:${started.wsPort}${
|
|
73
|
+
started.guiPort ? ` | GUI http://${host}:${started.guiPort}` : ""
|
|
74
|
+
}`,
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
spinner.stop("Failed to start RPC");
|
|
78
|
+
p.log.error(String(error));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForRpc(url: string, timeoutMs = 10_000) {
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
while (Date.now() - start < timeoutMs) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`${url}/health`);
|
|
88
|
+
if (res.ok) return;
|
|
89
|
+
} catch {}
|
|
90
|
+
await Bun.sleep(200);
|
|
91
|
+
}
|
|
92
|
+
throw new Error("RPC server did not become ready in time");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function saveConfig(config: SolforgeConfig) {
|
|
96
|
+
await writeConfig(config, CONFIG_PATH);
|
|
97
|
+
p.log.success(`Updated ${CONFIG_PATH}`);
|
|
98
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
const CANCEL_MESSAGE = "Setup cancelled";
|
|
4
|
+
|
|
5
|
+
export function cancelSetup(): never {
|
|
6
|
+
p.cancel(CANCEL_MESSAGE);
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ensure<T>(value: T | symbol): T {
|
|
11
|
+
if (p.isCancel(value)) cancelSetup();
|
|
12
|
+
return value as T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validatePort(value: string | number | undefined) {
|
|
16
|
+
const num = Number(value);
|
|
17
|
+
if (!Number.isInteger(num)) return "Port must be an integer";
|
|
18
|
+
if (num < 1 || num > 65535) return "Port must be between 1 and 65535";
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validatePositiveNumber(value: string) {
|
|
23
|
+
const num = Number(value);
|
|
24
|
+
if (!Number.isFinite(num)) return "Enter a number";
|
|
25
|
+
if (num <= 0) return "Enter a positive number";
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validatePubkey(value: string | undefined) {
|
|
30
|
+
if (!value) return "Value is required";
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
if (trimmed.length < 32 || trimmed.length > 44)
|
|
33
|
+
return "Expected base58 address";
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function collectCustomEntries(label: string) {
|
|
38
|
+
const results: string[] = [];
|
|
39
|
+
while (true) {
|
|
40
|
+
const value = await p.text({
|
|
41
|
+
message: `Enter ${label} (leave blank to finish)`,
|
|
42
|
+
});
|
|
43
|
+
if (p.isCancel(value)) cancelSetup();
|
|
44
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
45
|
+
if (!trimmed) break;
|
|
46
|
+
const error = validatePubkey(trimmed);
|
|
47
|
+
if (error) {
|
|
48
|
+
p.log.error(error);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
results.push(trimmed);
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|