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
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
|
+
}
|