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.
Files changed (194) hide show
  1. package/.agi/agi.sqlite +0 -0
  2. package/.claude/settings.local.json +9 -0
  3. package/.github/workflows/release-binaries.yml +133 -0
  4. package/.tmp/.787ebcdbf7b8fde8-00000000.hm +0 -0
  5. package/.tmp/.bffe6efebdf8aedc-00000000.hm +0 -0
  6. package/AGENTS.md +271 -0
  7. package/CLAUDE.md +106 -0
  8. package/PROJECT_STRUCTURE.md +124 -0
  9. package/README.md +367 -393
  10. package/SOLANA_KIT_GUIDE.md +251 -0
  11. package/SOLFORGE.md +119 -0
  12. package/biome.json +34 -0
  13. package/bun.lock +743 -0
  14. package/docs/bun-single-file-executable.md +585 -0
  15. package/docs/cli-plan.md +154 -0
  16. package/docs/data-indexing-plan.md +214 -0
  17. package/docs/gui-roadmap.md +202 -0
  18. package/drizzle/0000_friendly_millenium_guard.sql +53 -0
  19. package/drizzle/0001_stale_sentinels.sql +2 -0
  20. package/drizzle/meta/0000_snapshot.json +329 -0
  21. package/drizzle/meta/0001_snapshot.json +345 -0
  22. package/drizzle/meta/_journal.json +20 -0
  23. package/drizzle.config.ts +12 -0
  24. package/index.ts +21 -0
  25. package/mint.sh +47 -0
  26. package/package.json +45 -69
  27. package/postcss.config.js +6 -0
  28. package/rpc-server.ts.backup +519 -0
  29. package/server/index.ts +5 -0
  30. package/server/lib/base58.ts +33 -0
  31. package/server/lib/faucet.ts +110 -0
  32. package/server/lib/spl-token.ts +57 -0
  33. package/server/methods/TEMPLATE.md +117 -0
  34. package/server/methods/account/get-account-info.ts +90 -0
  35. package/server/methods/account/get-balance.ts +27 -0
  36. package/server/methods/account/get-multiple-accounts.ts +83 -0
  37. package/server/methods/account/get-parsed-account-info.ts +21 -0
  38. package/server/methods/account/index.ts +12 -0
  39. package/server/methods/account/parsers/index.ts +52 -0
  40. package/server/methods/account/parsers/loader-upgradeable.ts +66 -0
  41. package/server/methods/account/parsers/spl-token.ts +237 -0
  42. package/server/methods/account/parsers/system.ts +4 -0
  43. package/server/methods/account/request-airdrop.ts +219 -0
  44. package/server/methods/admin/adopt-mint-authority.ts +94 -0
  45. package/server/methods/admin/clone-program-accounts.ts +55 -0
  46. package/server/methods/admin/clone-program.ts +152 -0
  47. package/server/methods/admin/clone-token-accounts.ts +117 -0
  48. package/server/methods/admin/clone-token-mint.ts +82 -0
  49. package/server/methods/admin/create-mint.ts +114 -0
  50. package/server/methods/admin/create-token-account.ts +137 -0
  51. package/server/methods/admin/helpers.ts +70 -0
  52. package/server/methods/admin/index.ts +10 -0
  53. package/server/methods/admin/list-mints.ts +21 -0
  54. package/server/methods/admin/load-program.ts +52 -0
  55. package/server/methods/admin/mint-to.ts +278 -0
  56. package/server/methods/block/get-block-height.ts +5 -0
  57. package/server/methods/block/get-block.ts +35 -0
  58. package/server/methods/block/get-blocks-with-limit.ts +23 -0
  59. package/server/methods/block/get-latest-blockhash.ts +12 -0
  60. package/server/methods/block/get-slot.ts +5 -0
  61. package/server/methods/block/index.ts +6 -0
  62. package/server/methods/block/is-blockhash-valid.ts +23 -0
  63. package/server/methods/epoch/get-cluster-nodes.ts +17 -0
  64. package/server/methods/epoch/get-epoch-info.ts +16 -0
  65. package/server/methods/epoch/get-epoch-schedule.ts +15 -0
  66. package/server/methods/epoch/get-highest-snapshot-slot.ts +9 -0
  67. package/server/methods/epoch/get-leader-schedule.ts +8 -0
  68. package/server/methods/epoch/get-max-retransmit-slot.ts +9 -0
  69. package/server/methods/epoch/get-max-shred-insert-slot.ts +9 -0
  70. package/server/methods/epoch/get-slot-leader.ts +6 -0
  71. package/server/methods/epoch/get-slot-leaders.ts +9 -0
  72. package/server/methods/epoch/get-stake-activation.ts +9 -0
  73. package/server/methods/epoch/get-stake-minimum-delegation.ts +9 -0
  74. package/server/methods/epoch/get-vote-accounts.ts +19 -0
  75. package/server/methods/epoch/index.ts +13 -0
  76. package/server/methods/epoch/minimum-ledger-slot.ts +5 -0
  77. package/server/methods/fee/get-fee-calculator-for-blockhash.ts +12 -0
  78. package/server/methods/fee/get-fee-for-message.ts +8 -0
  79. package/server/methods/fee/get-fee-rate-governor.ts +16 -0
  80. package/server/methods/fee/get-fees.ts +14 -0
  81. package/server/methods/fee/get-recent-prioritization-fees.ts +22 -0
  82. package/server/methods/fee/index.ts +5 -0
  83. package/server/methods/get-address-lookup-table.ts +31 -0
  84. package/server/methods/index.ts +265 -0
  85. package/server/methods/performance/get-recent-performance-samples.ts +25 -0
  86. package/server/methods/performance/get-transaction-count.ts +5 -0
  87. package/server/methods/performance/index.ts +2 -0
  88. package/server/methods/program/get-block-commitment.ts +9 -0
  89. package/server/methods/program/get-block-production.ts +14 -0
  90. package/server/methods/program/get-block-time.ts +21 -0
  91. package/server/methods/program/get-blocks.ts +11 -0
  92. package/server/methods/program/get-first-available-block.ts +9 -0
  93. package/server/methods/program/get-genesis-hash.ts +6 -0
  94. package/server/methods/program/get-identity.ts +6 -0
  95. package/server/methods/program/get-inflation-governor.ts +15 -0
  96. package/server/methods/program/get-inflation-rate.ts +10 -0
  97. package/server/methods/program/get-inflation-reward.ts +12 -0
  98. package/server/methods/program/get-largest-accounts.ts +8 -0
  99. package/server/methods/program/get-parsed-program-accounts.ts +12 -0
  100. package/server/methods/program/get-parsed-token-accounts-by-delegate.ts +12 -0
  101. package/server/methods/program/get-parsed-token-accounts-by-owner.ts +12 -0
  102. package/server/methods/program/get-program-accounts.ts +221 -0
  103. package/server/methods/program/get-supply.ts +13 -0
  104. package/server/methods/program/get-token-account-balance.ts +64 -0
  105. package/server/methods/program/get-token-accounts-by-delegate.ts +81 -0
  106. package/server/methods/program/get-token-accounts-by-owner.ts +390 -0
  107. package/server/methods/program/get-token-largest-accounts.ts +80 -0
  108. package/server/methods/program/get-token-supply.ts +38 -0
  109. package/server/methods/program/index.ts +21 -0
  110. package/server/methods/solforge/index.ts +155 -0
  111. package/server/methods/system/get-health.ts +5 -0
  112. package/server/methods/system/get-minimum-balance-for-rent-exemption.ts +13 -0
  113. package/server/methods/system/get-version.ts +9 -0
  114. package/server/methods/system/index.ts +3 -0
  115. package/server/methods/transaction/get-confirmed-transaction.ts +11 -0
  116. package/server/methods/transaction/get-parsed-transaction.ts +21 -0
  117. package/server/methods/transaction/get-signature-statuses.ts +72 -0
  118. package/server/methods/transaction/get-signatures-for-address.ts +45 -0
  119. package/server/methods/transaction/get-transaction.ts +428 -0
  120. package/server/methods/transaction/index.ts +7 -0
  121. package/server/methods/transaction/send-transaction.ts +232 -0
  122. package/server/methods/transaction/simulate-transaction.ts +56 -0
  123. package/server/rpc-server.ts +474 -0
  124. package/server/types.ts +74 -0
  125. package/server/ws-server.ts +171 -0
  126. package/sf.config.json +38 -0
  127. package/src/cli/bootstrap.ts +67 -0
  128. package/src/cli/commands/airdrop.ts +37 -0
  129. package/src/cli/commands/config.ts +39 -0
  130. package/src/cli/commands/mint.ts +187 -0
  131. package/src/cli/commands/program-clone.ts +124 -0
  132. package/src/cli/commands/program-load.ts +64 -0
  133. package/src/cli/commands/rpc-start.ts +46 -0
  134. package/src/cli/commands/token-adopt-authority.ts +37 -0
  135. package/src/cli/commands/token-clone.ts +113 -0
  136. package/src/cli/commands/token-create.ts +81 -0
  137. package/src/cli/main.ts +130 -0
  138. package/src/cli/run-solforge.ts +98 -0
  139. package/src/cli/setup-utils.ts +54 -0
  140. package/src/cli/setup-wizard.ts +256 -0
  141. package/src/cli/utils/args.ts +15 -0
  142. package/src/config/index.ts +130 -0
  143. package/src/db/index.ts +83 -0
  144. package/src/db/schema/accounts.ts +23 -0
  145. package/src/db/schema/address-signatures.ts +31 -0
  146. package/src/db/schema/index.ts +5 -0
  147. package/src/db/schema/meta-kv.ts +9 -0
  148. package/src/db/schema/transactions.ts +29 -0
  149. package/src/db/schema/tx-accounts.ts +33 -0
  150. package/src/db/tx-store.ts +229 -0
  151. package/src/gui/public/app.css +1 -0
  152. package/src/gui/public/index.html +19 -0
  153. package/src/gui/server.ts +297 -0
  154. package/src/gui/src/api.ts +127 -0
  155. package/src/gui/src/app.tsx +390 -0
  156. package/src/gui/src/components/airdrop-mint-form.tsx +216 -0
  157. package/src/gui/src/components/clone-program-modal.tsx +183 -0
  158. package/src/gui/src/components/clone-token-modal.tsx +211 -0
  159. package/src/gui/src/components/modal.tsx +127 -0
  160. package/src/gui/src/components/programs-panel.tsx +112 -0
  161. package/src/gui/src/components/status-panel.tsx +122 -0
  162. package/src/gui/src/components/tokens-panel.tsx +116 -0
  163. package/src/gui/src/hooks/use-interval.ts +17 -0
  164. package/src/gui/src/index.css +529 -0
  165. package/src/gui/src/main.tsx +17 -0
  166. package/src/migrations-bundled.ts +17 -0
  167. package/src/rpc/start.ts +44 -0
  168. package/tailwind.config.js +27 -0
  169. package/test-client.ts +120 -0
  170. package/tmp/inspect-html.ts +4 -0
  171. package/tmp/response-test.ts +5 -0
  172. package/tmp/test-html.ts +5 -0
  173. package/tmp/test-server.ts +13 -0
  174. package/tsconfig.json +24 -23
  175. package/LICENSE +0 -21
  176. package/scripts/postinstall.cjs +0 -103
  177. package/src/api-server-entry.ts +0 -109
  178. package/src/commands/add-program.ts +0 -337
  179. package/src/commands/init.ts +0 -122
  180. package/src/commands/list.ts +0 -136
  181. package/src/commands/mint.ts +0 -336
  182. package/src/commands/start.ts +0 -878
  183. package/src/commands/status.ts +0 -99
  184. package/src/commands/stop.ts +0 -406
  185. package/src/config/manager.ts +0 -157
  186. package/src/index.ts +0 -188
  187. package/src/services/api-server.ts +0 -532
  188. package/src/services/port-manager.ts +0 -177
  189. package/src/services/process-registry.ts +0 -154
  190. package/src/services/program-cloner.ts +0 -317
  191. package/src/services/token-cloner.ts +0 -809
  192. package/src/services/validator.ts +0 -295
  193. package/src/types/config.ts +0 -110
  194. package/src/utils/shell.ts +0 -110
package/sf.config.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "server": {
3
+ "rpcPort": 8899,
4
+ "wsPort": 8900,
5
+ "db": {
6
+ "mode": "ephemeral",
7
+ "path": ".solforge/db.db"
8
+ }
9
+ },
10
+ "svm": {
11
+ "initialLamports": "1000000000000000",
12
+ "faucetSOL": 1000
13
+ },
14
+ "clone": {
15
+ "endpoint": "https://api.mainnet-beta.solana.com",
16
+ "programs": [
17
+ "JARSq9S9RgyynuAwcdWh2yEG6MbhfntWq7zjXjAo87uQ"
18
+ ],
19
+ "tokens": [
20
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
21
+ "XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB",
22
+ "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn"
23
+ ],
24
+ "programAccounts": []
25
+ },
26
+ "gui": {
27
+ "enabled": true,
28
+ "port": 42069
29
+ },
30
+ "bootstrap": {
31
+ "airdrops": [
32
+ {
33
+ "address": "JARSnsWXAxUAp6Ny3Td9H1i4wNpesqkcfL1wf82t5tKi",
34
+ "amountSol": 100
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,67 @@
1
+ import * as p from "@clack/prompts";
2
+ import { defaultConfig, type SolforgeConfig } from "../config";
3
+
4
+ export async function bootstrapEnvironment(
5
+ config: SolforgeConfig,
6
+ host: string,
7
+ rpcPort: number,
8
+ ) {
9
+ const url = `http://${host}:${rpcPort}`;
10
+ const endpoint = config.clone.endpoint || defaultConfig.clone.endpoint;
11
+
12
+ for (const mint of config.clone.tokens || []) {
13
+ await withSpinner(`Cloning token ${mint}`, async () => {
14
+ await callRpc(url, "solforgeAdminCloneTokenMint", [mint, { endpoint }]);
15
+ await callRpc(url, "solforgeAdoptMintAuthority", [mint]);
16
+ });
17
+ }
18
+
19
+ for (const programId of config.clone.programs || []) {
20
+ await withSpinner(`Cloning program ${programId}`, async () => {
21
+ await callRpc(url, "solforgeAdminCloneProgram", [
22
+ programId,
23
+ { endpoint },
24
+ ]);
25
+ });
26
+ }
27
+
28
+ for (const { address, amountSol } of config.bootstrap?.airdrops || []) {
29
+ await withSpinner(
30
+ `Airdropping ${amountSol} SOL to ${address}`,
31
+ async () => {
32
+ const lamports = Math.round(amountSol * 1_000_000_000);
33
+ await callRpc(url, "requestAirdrop", [address, lamports]);
34
+ },
35
+ );
36
+ }
37
+ }
38
+
39
+ async function withSpinner(task: string, action: () => Promise<void>) {
40
+ const spin = p.spinner();
41
+ spin.start(`${task}...`);
42
+ try {
43
+ await action();
44
+ spin.stop(`${task} done`);
45
+ } catch (error) {
46
+ spin.stop(`${task} failed`);
47
+ p.log.error(String(error));
48
+ }
49
+ }
50
+
51
+ async function callRpc(url: string, method: string, params: unknown[]) {
52
+ const res = await fetch(url, {
53
+ method: "POST",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }),
56
+ });
57
+ if (!res.ok) throw new Error(`${method} HTTP ${res.status}`);
58
+ const json = await res.json();
59
+ if (json?.error) {
60
+ const message = json.error?.message || `${method} failed`;
61
+ const detail = json.error?.data
62
+ ? `: ${JSON.stringify(json.error.data)}`
63
+ : "";
64
+ throw new Error(message + detail);
65
+ }
66
+ return json.result;
67
+ }
@@ -0,0 +1,37 @@
1
+ import * as p from "@clack/prompts";
2
+ import { readConfig } from "../../config";
3
+ import { parseFlags } from "../utils/args";
4
+
5
+ export async function airdropCommand(args: string[]) {
6
+ const { flags } = parseFlags(args);
7
+ const to = String(flags["to"] || "");
8
+ const sol = Number(flags["sol"] || 0);
9
+ const cfg = await readConfig();
10
+ const url = `http://localhost:${cfg.server.rpcPort}`;
11
+ if (!to || !sol) {
12
+ p.log.error("Usage: solforge airdrop --to <pubkey> --sol <amount>");
13
+ return;
14
+ }
15
+ const lamports = BigInt(Math.floor(sol * 1_000_000_000));
16
+ const body = {
17
+ jsonrpc: "2.0",
18
+ id: 1,
19
+ method: "requestAirdrop",
20
+ params: [to, Number(lamports)],
21
+ };
22
+ const s = p.spinner();
23
+ s.start("Requesting airdrop...");
24
+ try {
25
+ const res = await fetch(url, {
26
+ method: "POST",
27
+ headers: { "content-type": "application/json" },
28
+ body: JSON.stringify(body),
29
+ });
30
+ const json = await res.json();
31
+ s.stop("Airdrop requested");
32
+ console.log(JSON.stringify(json, null, 2));
33
+ } catch (e) {
34
+ s.stop("Airdrop failed");
35
+ p.log.error(String(e));
36
+ }
37
+ }
@@ -0,0 +1,39 @@
1
+ import * as p from "@clack/prompts";
2
+ import {
3
+ getConfigValue,
4
+ readConfig,
5
+ setConfigValue,
6
+ writeDefaultConfig,
7
+ } from "../../config";
8
+ import { parseFlags } from "../utils/args";
9
+
10
+ export async function configCommand(sub: string | undefined, args: string[]) {
11
+ switch (sub) {
12
+ case "init": {
13
+ const { flags } = parseFlags(args);
14
+ const force = !!flags["force"];
15
+ await writeDefaultConfig({ force });
16
+ p.log.success("Wrote sf.config.json");
17
+ return;
18
+ }
19
+ case "get": {
20
+ const key = args[0];
21
+ const cfg = await readConfig();
22
+ console.log(getConfigValue(cfg, key));
23
+ return;
24
+ }
25
+ case "set": {
26
+ const [key, value] = args;
27
+ const cfg = await readConfig();
28
+ const updated = setConfigValue(cfg, key, value);
29
+ await Bun.write(
30
+ "sf.config.json",
31
+ JSON.stringify(updated, null, 2) + "\n",
32
+ );
33
+ p.log.success(`Updated ${key}`);
34
+ return;
35
+ }
36
+ default:
37
+ p.log.error("Usage: solforge config <init|get|set>");
38
+ }
39
+ }
@@ -0,0 +1,187 @@
1
+ import * as p from "@clack/prompts";
2
+ import { readConfig } from "../../config";
3
+ import { parseFlags } from "../utils/args";
4
+ // No network fetch; decimals are read from LiteSVM via RPC getTokenSupply
5
+
6
+ // Create/overwrite a token account (ATA) with a specified amount (base units)
7
+ // Usage: solforge mint --mint <mint> --to <owner> --amount <amount>
8
+ export async function mintCommand(args: string[]) {
9
+ const { flags } = parseFlags(args);
10
+ let mint = flags["mint"] as string | undefined;
11
+ let receiver = flags["to"] as string | undefined; // required: receiver address (ATA owner)
12
+ let amountBase = flags["amount"] as string | undefined; // optional direct base-units
13
+ let uiAmount = flags["ui-amount"] as string | undefined; // preferred UI units
14
+
15
+ const cfg = await readConfig();
16
+ const url = `http://localhost:${cfg.server.rpcPort}`;
17
+
18
+ // Get known mints from server for selection
19
+ let knownMints: string[] = [];
20
+ try {
21
+ const resList = await fetch(url, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/json" },
24
+ body: JSON.stringify({
25
+ jsonrpc: "2.0",
26
+ id: 1,
27
+ method: "solforgeListMints",
28
+ params: [],
29
+ }),
30
+ });
31
+ const j = await resList.json();
32
+ if (!j.error && Array.isArray(j.result)) knownMints = j.result;
33
+ } catch {}
34
+
35
+ if (!mint) {
36
+ if (knownMints.length > 0) {
37
+ const choice = (await p.select({
38
+ message: "Select mint",
39
+ options: knownMints.map((m) => ({ value: m, label: m })),
40
+ })) as string | symbol | null;
41
+ if (!choice || typeof choice !== "string") return;
42
+ mint = choice;
43
+ } else {
44
+ p.log.error("No known mints. Clone or create a token first.");
45
+ return;
46
+ }
47
+ }
48
+
49
+ // Receiver (ATA owner) is required
50
+ if (!receiver) {
51
+ receiver = (await p.text({
52
+ message: "Receiver public key (ATA owner)",
53
+ placeholder: "<receiver public key>",
54
+ validate: (v) =>
55
+ v && v.length >= 32 ? undefined : "Enter a valid public key",
56
+ })) as string;
57
+ if (!receiver) return;
58
+ }
59
+
60
+ // Amount selection (prefer UI units)
61
+ if (!amountBase && !uiAmount) {
62
+ uiAmount = (await p.text({
63
+ message: "Amount (UI units)",
64
+ placeholder: "1000",
65
+ validate: (v) =>
66
+ v && !Number.isNaN(Number(v)) ? undefined : "Enter a number",
67
+ })) as string;
68
+ if (!uiAmount) return;
69
+ }
70
+
71
+ // If UI amount provided, get decimals from LiteSVM via getTokenSupply
72
+ if (!amountBase && uiAmount) {
73
+ let decimals = 0;
74
+ try {
75
+ const res = await fetch(url, {
76
+ method: "POST",
77
+ headers: { "content-type": "application/json" },
78
+ body: JSON.stringify({
79
+ jsonrpc: "2.0",
80
+ id: 2,
81
+ method: "getTokenSupply",
82
+ params: [mint],
83
+ }),
84
+ });
85
+ const j = await res.json();
86
+ const d = j?.result?.value?.decimals ?? j?.result?.decimals;
87
+ if (typeof d === "number") decimals = d;
88
+ } catch {}
89
+ amountBase = toBaseUnits(uiAmount, decimals);
90
+ }
91
+
92
+ const s = p.spinner();
93
+ s.start("Minting via real transaction...");
94
+ try {
95
+ const res = await fetch(url, {
96
+ method: "POST",
97
+ headers: { "content-type": "application/json" },
98
+ body: JSON.stringify({
99
+ jsonrpc: "2.0",
100
+ id: 1,
101
+ method: "solforgeMintTo",
102
+ params: [mint, receiver, amountBase],
103
+ }),
104
+ });
105
+ const json = await res.json();
106
+ if (json.error) {
107
+ const msg = String(json.error.message || "mint failed");
108
+ // Offer admin fallback when faucet is not mint authority
109
+ if (/no faucet authority|authority/i.test(msg)) {
110
+ const choice = (await p.select({
111
+ message: "Mint authority is not faucet. Choose action:",
112
+ options: [
113
+ {
114
+ value: "adopt",
115
+ label: "Adopt faucet as authority (local) and mint (real tx)",
116
+ },
117
+ { value: "admin", label: "Admin set-balance (no real tx)" },
118
+ { value: "cancel", label: "Cancel" },
119
+ ],
120
+ })) as "adopt" | "admin" | "cancel" | symbol | null;
121
+ if (!choice || choice === "cancel") throw new Error(msg);
122
+ if (choice === "adopt") {
123
+ s.message("Adopting faucet as authority...");
124
+ const resAdopt = await fetch(url, {
125
+ method: "POST",
126
+ headers: { "content-type": "application/json" },
127
+ body: JSON.stringify({
128
+ jsonrpc: "2.0",
129
+ id: 2,
130
+ method: "solforgeAdoptMintAuthority",
131
+ params: [mint],
132
+ }),
133
+ });
134
+ const jA = await resAdopt.json();
135
+ if (jA.error)
136
+ throw new Error(jA.error.message || "adopt authority failed");
137
+ s.message("Minting via real transaction...");
138
+ const resRetry = await fetch(url, {
139
+ method: "POST",
140
+ headers: { "content-type": "application/json" },
141
+ body: JSON.stringify({
142
+ jsonrpc: "2.0",
143
+ id: 3,
144
+ method: "solforgeMintTo",
145
+ params: [mint, receiver, amountBase],
146
+ }),
147
+ });
148
+ const jR = await resRetry.json();
149
+ if (jR.error) throw new Error(jR.error.message || "mint failed");
150
+ s.stop("Minted");
151
+ console.log(JSON.stringify(jR.result, null, 2));
152
+ return;
153
+ }
154
+ if (choice === "admin") {
155
+ const res2 = await fetch(url, {
156
+ method: "POST",
157
+ headers: { "content-type": "application/json" },
158
+ body: JSON.stringify({
159
+ jsonrpc: "2.0",
160
+ id: 4,
161
+ method: "solforgeCreateTokenAccount",
162
+ params: [mint, receiver, amountBase],
163
+ }),
164
+ });
165
+ const json2 = await res2.json();
166
+ if (json2.error)
167
+ throw new Error(json2.error.message || "admin mint failed");
168
+ s.stop("Minted (admin)");
169
+ console.log(JSON.stringify(json2.result, null, 2));
170
+ return;
171
+ }
172
+ }
173
+ throw new Error(msg);
174
+ }
175
+ s.stop("Minted");
176
+ console.log(JSON.stringify(json.result, null, 2));
177
+ } catch (e) {
178
+ s.stop("Mint failed");
179
+ p.log.error(String(e));
180
+ }
181
+ }
182
+
183
+ function toBaseUnits(ui: string, decimals: number): string {
184
+ const [i, f = ""] = String(ui).split(".");
185
+ const frac = (f + "0".repeat(decimals)).slice(0, decimals);
186
+ return BigInt(i + (decimals ? frac : "")).toString();
187
+ }
@@ -0,0 +1,124 @@
1
+ import * as p from "@clack/prompts";
2
+ import { readConfig, writeConfig } from "../../config";
3
+ import { parseFlags } from "../utils/args";
4
+
5
+ export async function programCloneCommand(args: string[]) {
6
+ const { flags, rest } = parseFlags(args);
7
+ const programId = (
8
+ (rest[0] as string) ||
9
+ (flags["program"] as string) ||
10
+ ""
11
+ ).trim();
12
+ const configPath = flags["config"] as string | undefined;
13
+ const cfg = await readConfig(configPath);
14
+ const endpoint = (flags["endpoint"] as string) || cfg.clone.endpoint;
15
+ const withAccounts = !!flags["with-accounts"];
16
+ const accountsLimit = flags["accounts-limit"]
17
+ ? Number(flags["accounts-limit"])
18
+ : undefined;
19
+ if (!programId) {
20
+ p.log.error(
21
+ "Usage: solforge program clone <programId> [--endpoint URL] [--with-accounts] [--accounts-limit N]",
22
+ );
23
+ return;
24
+ }
25
+ const url = `http://localhost:${cfg.server.rpcPort}`;
26
+ const s = p.spinner();
27
+ s.start("Cloning program...");
28
+ try {
29
+ const res = await fetch(url, {
30
+ method: "POST",
31
+ headers: { "content-type": "application/json" },
32
+ body: JSON.stringify({
33
+ jsonrpc: "2.0",
34
+ id: 1,
35
+ method: "solforgeAdminCloneProgram",
36
+ params: [programId, { endpoint, withAccounts, accountsLimit }],
37
+ }),
38
+ });
39
+ const json = await res.json();
40
+ if (json.error) {
41
+ const details = json.error.data
42
+ ? `\nDetails: ${JSON.stringify(json.error.data)}`
43
+ : "";
44
+ throw new Error((json.error.message || "program clone failed") + details);
45
+ }
46
+ s.stop("Program cloned");
47
+ console.log(JSON.stringify(json.result, null, 2));
48
+ await recordProgramClone(configPath, programId);
49
+ } catch (e) {
50
+ s.stop("Clone failed");
51
+ p.log.error(String(e));
52
+ }
53
+ }
54
+
55
+ export async function programAccountsCloneCommand(args: string[]) {
56
+ const { flags, rest } = parseFlags(args);
57
+ const programId = (
58
+ (rest[0] as string) ||
59
+ (flags["program"] as string) ||
60
+ ""
61
+ ).trim();
62
+ const configPath = flags["config"] as string | undefined;
63
+ const cfg = await readConfig(configPath);
64
+ const endpoint = (flags["endpoint"] as string) || cfg.clone.endpoint;
65
+ const limit = flags["limit"] ? Number(flags["limit"]) : undefined;
66
+ const filters = flags["filters"]
67
+ ? safeJson(flags["filters"] as string)
68
+ : undefined;
69
+ if (!programId) {
70
+ p.log.error(
71
+ "Usage: solforge program accounts clone <programId> [--endpoint URL] [--limit N] [--filters JSON]",
72
+ );
73
+ return;
74
+ }
75
+ const url = `http://localhost:${cfg.server.rpcPort}`;
76
+ const s = p.spinner();
77
+ s.start("Cloning program accounts...");
78
+ try {
79
+ const res = await fetch(url, {
80
+ method: "POST",
81
+ headers: { "content-type": "application/json" },
82
+ body: JSON.stringify({
83
+ jsonrpc: "2.0",
84
+ id: 1,
85
+ method: "solforgeAdminCloneProgramAccounts",
86
+ params: [programId, { endpoint, limit, filters }],
87
+ }),
88
+ });
89
+ const json = await res.json();
90
+ if (json.error)
91
+ throw new Error(json.error.message || "program accounts clone failed");
92
+ s.stop("Program accounts cloned");
93
+ console.log(JSON.stringify(json.result, null, 2));
94
+ } catch (e) {
95
+ s.stop("Clone failed");
96
+ p.log.error(String(e));
97
+ }
98
+ }
99
+
100
+ function safeJson(s: string): any {
101
+ try {
102
+ return JSON.parse(s);
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ async function recordProgramClone(
109
+ configPath: string | undefined,
110
+ programId: string,
111
+ ) {
112
+ try {
113
+ const cfg = await readConfig(configPath);
114
+ const next = new Set(cfg.clone.programs ?? []);
115
+ if (!next.has(programId)) {
116
+ next.add(programId);
117
+ cfg.clone.programs = Array.from(next);
118
+ await writeConfig(cfg, configPath ?? "sf.config.json");
119
+ p.log.info(`Added ${programId} to clone programs in config`);
120
+ }
121
+ } catch (error) {
122
+ console.warn(`[config] Failed to update clone programs: ${String(error)}`);
123
+ }
124
+ }
@@ -0,0 +1,64 @@
1
+ import * as p from "@clack/prompts";
2
+ import { Connection, PublicKey } from "@solana/web3.js";
3
+ import { readConfig } from "../../config";
4
+ import { parseFlags } from "../utils/args";
5
+
6
+ export async function programLoadCommand(args: string[]) {
7
+ const { flags, rest } = parseFlags(args);
8
+ const programId = (rest[0] as string) || (flags["program"] as string);
9
+ const fromFile = flags["file"] as string | undefined;
10
+ const endpoint = flags["endpoint"] as string | undefined;
11
+ if (!programId) {
12
+ p.log.error(
13
+ "Usage: solforge program load <programId> [--file PATH | --endpoint URL]",
14
+ );
15
+ return;
16
+ }
17
+ let base64: string | undefined;
18
+ try {
19
+ if (fromFile) {
20
+ const bytes = await Bun.file(fromFile).arrayBuffer();
21
+ base64 = Buffer.from(bytes).toString("base64");
22
+ } else if (endpoint) {
23
+ // Fetch ProgramData from endpoint
24
+ const conn = new Connection(endpoint, "confirmed");
25
+ const pid = new PublicKey(programId);
26
+ const info = await conn.getAccountInfo(pid, "confirmed");
27
+ if (!info) throw new Error("Program account not found on endpoint");
28
+ // Program account should be upgradeable; fetch ProgramData and extract bytes after header
29
+ // Heuristic: delegate parsing to server if unsure. Here, try raw first.
30
+ base64 = Buffer.from(info.data as Buffer).toString("base64");
31
+ } else {
32
+ p.log.error("Either --file or --endpoint must be provided");
33
+ return;
34
+ }
35
+ } catch (e) {
36
+ p.log.error(`Failed to read ELF: ${String(e)}`);
37
+ return;
38
+ }
39
+
40
+ const cfg = await readConfig();
41
+ const url = `http://localhost:${cfg.server.rpcPort}`;
42
+ const s = p.spinner();
43
+ s.start("Loading program into LiteSVM...");
44
+ try {
45
+ const res = await fetch(url, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json" },
48
+ body: JSON.stringify({
49
+ jsonrpc: "2.0",
50
+ id: 1,
51
+ method: "solforgeLoadProgram",
52
+ params: [programId, base64],
53
+ }),
54
+ });
55
+ const json = await res.json();
56
+ if (json.error)
57
+ throw new Error(json.error.message || "program load failed");
58
+ s.stop("Program loaded");
59
+ console.log(JSON.stringify(json.result, null, 2));
60
+ } catch (e) {
61
+ s.stop("Load failed");
62
+ p.log.error(String(e));
63
+ }
64
+ }
@@ -0,0 +1,46 @@
1
+ import * as p from "@clack/prompts";
2
+ import { readConfig } from "../../config";
3
+ import { startRpcServers } from "../../rpc/start";
4
+ import { parseFlags } from "../utils/args";
5
+
6
+ export async function rpcStartCommand(args: string[]) {
7
+ const { flags } = parseFlags(args);
8
+ const cfg = await readConfig(flags["config"] as string | undefined);
9
+ const rpcPort = Number(flags["port"] ?? cfg.server.rpcPort ?? 8899);
10
+ const wsPort = Number(flags["ws-port"] ?? cfg.server.wsPort ?? rpcPort + 1);
11
+ const host = (flags["host"] as string) || "127.0.0.1";
12
+ const dbMode =
13
+ (flags["db-mode"] as string) || cfg.server.db.mode || "ephemeral";
14
+ const dbPath =
15
+ (flags["db-path"] as string) || cfg.server.db.path || ".solforge/db.db";
16
+ const guiPort = Number(flags["gui-port"] ?? cfg.gui.port ?? 42069);
17
+ const guiEnabled =
18
+ flags["no-gui"] === true
19
+ ? false
20
+ : flags["gui"] === true
21
+ ? true
22
+ : cfg.gui.enabled !== false;
23
+
24
+ const s = p.spinner();
25
+ const guiMsg = guiEnabled ? `, GUI on ${guiPort}` : "";
26
+ s.start(`Starting RPC on ${host}:${rpcPort}, WS on ${wsPort}${guiMsg}...`);
27
+ try {
28
+ const started = startRpcServers({
29
+ rpcPort,
30
+ wsPort,
31
+ dbMode: dbMode as any,
32
+ dbPath,
33
+ host,
34
+ guiEnabled,
35
+ guiPort,
36
+ });
37
+ s.stop("RPC started");
38
+ console.log(`HTTP: http://${host}:${started.rpcPort}`);
39
+ console.log(`WS: ws://${host}:${started.wsPort}`);
40
+ if (started.guiPort) console.log(`GUI: http://${host}:${started.guiPort}`);
41
+ } catch (e) {
42
+ s.stop("Failed to start RPC");
43
+ p.log.error(String(e));
44
+ process.exitCode = 1;
45
+ }
46
+ }
@@ -0,0 +1,37 @@
1
+ import * as p from "@clack/prompts";
2
+ import { readConfig } from "../../config";
3
+ import { parseFlags } from "../utils/args";
4
+
5
+ // Set the faucet as mint authority for an existing mint in LiteSVM (local-only)
6
+ export async function tokenAdoptAuthorityCommand(args: string[]) {
7
+ const { flags, rest } = parseFlags(args);
8
+ const mint = (rest[0] as string) || (flags["mint"] as string);
9
+ if (!mint) {
10
+ p.log.error("Usage: solforge token adopt-authority <mint>");
11
+ return;
12
+ }
13
+ const cfg = await readConfig();
14
+ const url = `http://localhost:${cfg.server.rpcPort}`;
15
+ const s = p.spinner();
16
+ s.start("Adopting faucet as mint authority...");
17
+ try {
18
+ const res = await fetch(url, {
19
+ method: "POST",
20
+ headers: { "content-type": "application/json" },
21
+ body: JSON.stringify({
22
+ jsonrpc: "2.0",
23
+ id: 1,
24
+ method: "solforgeAdoptMintAuthority",
25
+ params: [mint],
26
+ }),
27
+ });
28
+ const json = await res.json();
29
+ if (json.error)
30
+ throw new Error(json.error.message || "adopt authority failed");
31
+ s.stop("Authority updated");
32
+ console.log(JSON.stringify(json.result, null, 2));
33
+ } catch (e) {
34
+ s.stop("Failed");
35
+ p.log.error(String(e));
36
+ }
37
+ }