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
package/package.json
CHANGED
|
@@ -1,71 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solforge",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Solana localnet orchestration tool for cloning mainnet programs and tokens",
|
|
3
|
+
"version": "0.2.1",
|
|
5
4
|
"module": "index.ts",
|
|
6
5
|
"type": "module",
|
|
7
6
|
"private": false,
|
|
8
7
|
"bin": {
|
|
9
|
-
"solforge": "
|
|
8
|
+
"solforge": "dist/solforge"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"server",
|
|
14
|
+
"docs",
|
|
16
15
|
"README.md",
|
|
17
16
|
"LICENSE"
|
|
18
17
|
],
|
|
19
18
|
"scripts": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"build:
|
|
23
|
-
"build:
|
|
24
|
-
"
|
|
25
|
-
"build:
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@inquirer/prompts": "^7.5.3",
|
|
35
|
-
"@solana/spl-token": "^0.4.1",
|
|
36
|
-
"@solana/web3.js": "^1.87.6",
|
|
37
|
-
"chalk": "^5.3.0",
|
|
38
|
-
"commander": "^11.1.0",
|
|
39
|
-
"cors": "^2.8.5",
|
|
40
|
-
"express": "^4.18.2",
|
|
41
|
-
"inquirer": "^9.2.12",
|
|
42
|
-
"ora": "^8.0.1",
|
|
43
|
-
"ws": "^8.16.0",
|
|
44
|
-
"zod": "^3.22.4"
|
|
19
|
+
"lint": "biome check",
|
|
20
|
+
"cli": "bun src/cli/main.ts",
|
|
21
|
+
"build:gui": "bun build src/gui/src/main.tsx --outdir src/gui/public/build --target=browser --minify",
|
|
22
|
+
"build:css": "bunx tailwindcss -i src/gui/src/index.css -o src/gui/public/app.css --minify",
|
|
23
|
+
"build": "bun run build:bin",
|
|
24
|
+
"build:bin": "bun run build:css && bun run build:gui && bun build --compile src/cli/main.ts --outfile dist/solforge",
|
|
25
|
+
"build:bin:darwin-arm64": "bun run build:css && bun run build:gui && bun build --compile --target=bun-darwin-arm64 src/cli/main.ts --outfile dist/solforge-darwin-arm64",
|
|
26
|
+
"build:bin:darwin-x64": "bun run build:css && bun run build:gui && bun build --compile --target=bun-darwin-x64 src/cli/main.ts --outfile dist/solforge-darwin-x64",
|
|
27
|
+
"build:bin:linux-x64": "bun run build:css && bun run build:gui && bun build --compile --target=bun-linux-x64 src/cli/main.ts --outfile dist/solforge-linux-x64",
|
|
28
|
+
"build:bin:linux-arm64": "bun run build:css && bun run build:gui && bun build --compile --target=bun-linux-arm64 src/cli/main.ts --outfile dist/solforge-linux-arm64",
|
|
29
|
+
"build:bin:windows-x64": "bun run build:css && bun run build:gui && bun build --compile --target=bun-windows-x64 src/cli/main.ts --outfile dist/solforge-windows-x64.exe",
|
|
30
|
+
"build:bin:all": "bun run build:bin:darwin-arm64 && bun run build:bin:darwin-x64 && bun run build:bin:linux-x64 && bun run build:bin:linux-arm64 && bun run build:bin:windows-x64"
|
|
45
31
|
},
|
|
46
32
|
"devDependencies": {
|
|
33
|
+
"@biomejs/biome": "2.2.4",
|
|
47
34
|
"@types/bun": "latest",
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
35
|
+
"@types/react": "^19.1.13",
|
|
36
|
+
"@types/react-dom": "^19.1.9",
|
|
37
|
+
"autoprefixer": "^10.4.21",
|
|
38
|
+
"drizzle-kit": "^0.31.4",
|
|
39
|
+
"postcss": "^8.5.6",
|
|
40
|
+
"tailwindcss": "3"
|
|
54
41
|
},
|
|
55
42
|
"peerDependencies": {
|
|
56
43
|
"typescript": "^5"
|
|
57
44
|
},
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@clack/prompts": "^0.11.0",
|
|
47
|
+
"@solana-program/compute-budget": "^0.9.0",
|
|
48
|
+
"@solana-program/memo": "^0.8.0",
|
|
49
|
+
"@solana-program/system": "^0.8.0",
|
|
50
|
+
"@solana-program/token": "^0.6.0",
|
|
51
|
+
"@solana/kit": "^3.0.3",
|
|
52
|
+
"@solana/spl-token": "^0.4.14",
|
|
53
|
+
"drizzle-orm": "^0.44.5",
|
|
54
|
+
"litesvm": "^0.3.3",
|
|
55
|
+
"react": "^19.1.1",
|
|
56
|
+
"react-dom": "^19.1.1"
|
|
57
|
+
}
|
|
71
58
|
}
|
package/server/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2
|
+
const BASE = BigInt(ALPHABET.length);
|
|
3
|
+
|
|
4
|
+
export function encodeBase58(bytes: Uint8Array): string {
|
|
5
|
+
let num = 0n;
|
|
6
|
+
for (let i = 0; i < bytes.length; i++)
|
|
7
|
+
num = num * 256n + BigInt(bytes[i] || 0);
|
|
8
|
+
let encoded = "";
|
|
9
|
+
while (num > 0n) {
|
|
10
|
+
const remainder = num % BASE;
|
|
11
|
+
num = num / BASE;
|
|
12
|
+
encoded = ALPHABET[Number(remainder)] + encoded;
|
|
13
|
+
}
|
|
14
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++)
|
|
15
|
+
encoded = "1" + encoded;
|
|
16
|
+
return encoded || "1";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decodeBase58(str: string): Uint8Array {
|
|
20
|
+
let num = 0n;
|
|
21
|
+
for (const char of str) {
|
|
22
|
+
const index = ALPHABET.indexOf(char);
|
|
23
|
+
if (index === -1) throw new Error("Invalid base58 character");
|
|
24
|
+
num = num * BASE + BigInt(index);
|
|
25
|
+
}
|
|
26
|
+
const bytes: number[] = [];
|
|
27
|
+
while (num > 0n) {
|
|
28
|
+
bytes.unshift(Number(num % 256n));
|
|
29
|
+
num = num / 256n;
|
|
30
|
+
}
|
|
31
|
+
for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.unshift(0);
|
|
32
|
+
return new Uint8Array(bytes.length > 0 ? bytes : [0]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
Keypair,
|
|
5
|
+
type PublicKey,
|
|
6
|
+
SystemProgram,
|
|
7
|
+
Transaction,
|
|
8
|
+
} from "@solana/web3.js";
|
|
9
|
+
import type { LiteSVM } from "litesvm";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PATH =
|
|
12
|
+
process.env.SOLFORGE_FAUCET_PATH || ".solforge/faucet.json";
|
|
13
|
+
// Default to 1,000,000 SOL so we never run out in local dev.
|
|
14
|
+
// Override with SOLFORGE_FAUCET_LAMPORTS if desired.
|
|
15
|
+
const DEFAULT_INITIAL_LAMPORTS = BigInt(
|
|
16
|
+
process.env.SOLFORGE_FAUCET_LAMPORTS || "1000000000000000",
|
|
17
|
+
); // 1e15 lamports = 1,000,000 SOL
|
|
18
|
+
|
|
19
|
+
type FaucetFile = { secretKey: string; publicKey: string; createdAt: string };
|
|
20
|
+
|
|
21
|
+
function ensureDir(path: string) {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadOrCreateFaucet(path: string = DEFAULT_PATH): Keypair {
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(path)) {
|
|
30
|
+
const raw = readFileSync(path, "utf8");
|
|
31
|
+
const data = JSON.parse(raw) as FaucetFile;
|
|
32
|
+
const secret = Buffer.from(data.secretKey, "base64");
|
|
33
|
+
return Keypair.fromSecretKey(new Uint8Array(secret));
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
|
|
37
|
+
const kp = Keypair.generate();
|
|
38
|
+
try {
|
|
39
|
+
ensureDir(path);
|
|
40
|
+
const payload: FaucetFile = {
|
|
41
|
+
secretKey: Buffer.from(kp.secretKey).toString("base64"),
|
|
42
|
+
publicKey: kp.publicKey.toBase58(),
|
|
43
|
+
createdAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(path, JSON.stringify(payload, null, 2), "utf8");
|
|
46
|
+
} catch {}
|
|
47
|
+
return kp;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function fundFaucetIfNeeded(
|
|
51
|
+
svm: LiteSVM,
|
|
52
|
+
faucet: Keypair,
|
|
53
|
+
targetLamports: bigint = DEFAULT_INITIAL_LAMPORTS,
|
|
54
|
+
): bigint {
|
|
55
|
+
let bal = 0n;
|
|
56
|
+
try {
|
|
57
|
+
bal = svm.getBalance(faucet.publicKey as PublicKey) || 0n;
|
|
58
|
+
} catch {
|
|
59
|
+
bal = 0n;
|
|
60
|
+
}
|
|
61
|
+
if (bal >= targetLamports) return bal;
|
|
62
|
+
|
|
63
|
+
// Observed per-account airdrop cap ~10k SOL. Work around by creating feeder accounts,
|
|
64
|
+
// airdropping to each, then transferring to the faucet until the target is reached.
|
|
65
|
+
const LAMPORTS_PER_SOL = 1_000_000_000n;
|
|
66
|
+
const PER_ACCOUNT_CAP = 10_000n * LAMPORTS_PER_SOL; // 10k SOL per feeder
|
|
67
|
+
const FEE = 5_000n; // rough fee for legacy transfer
|
|
68
|
+
|
|
69
|
+
let remaining = targetLamports - bal;
|
|
70
|
+
let safety = 0;
|
|
71
|
+
while (remaining > 0n && safety < 1000) {
|
|
72
|
+
safety++;
|
|
73
|
+
const feeder = Keypair.generate();
|
|
74
|
+
const mint = remaining > PER_ACCOUNT_CAP ? PER_ACCOUNT_CAP : remaining;
|
|
75
|
+
// Airdrop enough to cover transfer + fee
|
|
76
|
+
try {
|
|
77
|
+
svm.airdrop(feeder.publicKey as PublicKey, mint + FEE);
|
|
78
|
+
} catch {
|
|
79
|
+
// If airdrop fails, try smaller amount; if still fails, stop
|
|
80
|
+
try {
|
|
81
|
+
svm.airdrop(feeder.publicKey as PublicKey, 1_000_000_000n);
|
|
82
|
+
} catch {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Transfer from feeder -> faucet
|
|
88
|
+
try {
|
|
89
|
+
const tx = new Transaction();
|
|
90
|
+
try {
|
|
91
|
+
tx.recentBlockhash = svm.latestBlockhash();
|
|
92
|
+
} catch {}
|
|
93
|
+
tx.add(
|
|
94
|
+
SystemProgram.transfer({
|
|
95
|
+
fromPubkey: feeder.publicKey,
|
|
96
|
+
toPubkey: faucet.publicKey as PublicKey,
|
|
97
|
+
lamports: Number(mint),
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
tx.sign(feeder);
|
|
101
|
+
svm.sendTransaction(tx);
|
|
102
|
+
remaining -= mint;
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
bal = svm.getBalance(faucet.publicKey as PublicKey) || 0n;
|
|
108
|
+
} catch {}
|
|
109
|
+
return bal;
|
|
110
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
|
|
3
|
+
export const TOKEN_PROGRAM_ID = new PublicKey(
|
|
4
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
// Minimal SPL Token v1 layouts
|
|
8
|
+
export type DecodedMint = {
|
|
9
|
+
supply: bigint;
|
|
10
|
+
decimals: number;
|
|
11
|
+
isInitialized: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DecodedTokenAccount = {
|
|
15
|
+
mint: string;
|
|
16
|
+
owner: string;
|
|
17
|
+
amount: bigint;
|
|
18
|
+
state: number;
|
|
19
|
+
delegatedAmount: bigint;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function decodeMint(data: Uint8Array): DecodedMint | null {
|
|
23
|
+
if (!data || data.length < 82) return null;
|
|
24
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
25
|
+
// supply at offset 36? Common layout: 4 (opt) + 32 (auth) + 8 (supply) + 1 (dec) + 1 (init)
|
|
26
|
+
const supply = view.getBigUint64(36, true);
|
|
27
|
+
const decimals = view.getUint8(44);
|
|
28
|
+
const isInitialized = view.getUint8(45) !== 0;
|
|
29
|
+
return { supply, decimals, isInitialized };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function decodeTokenAccount(
|
|
33
|
+
data: Uint8Array,
|
|
34
|
+
): DecodedTokenAccount | null {
|
|
35
|
+
if (!data || data.length < 165) return null;
|
|
36
|
+
const mint = new PublicKey(data.slice(0, 32)).toBase58();
|
|
37
|
+
const owner = new PublicKey(data.slice(32, 64)).toBase58();
|
|
38
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
39
|
+
const amount = view.getBigUint64(64, true);
|
|
40
|
+
const state = data[108];
|
|
41
|
+
const delegatedAmount = view.getBigUint64(124, true);
|
|
42
|
+
return { mint, owner, amount, state, delegatedAmount };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatUiAmount(amount: bigint, decimals: number) {
|
|
46
|
+
const base = BigInt(10) ** BigInt(decimals);
|
|
47
|
+
const whole = amount / base;
|
|
48
|
+
const frac = amount % base;
|
|
49
|
+
const fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
50
|
+
const ui = fracStr.length > 0 ? `${whole}.${fracStr}` : `${whole}`;
|
|
51
|
+
return {
|
|
52
|
+
amount: amount.toString(),
|
|
53
|
+
decimals,
|
|
54
|
+
uiAmount: Number(ui),
|
|
55
|
+
uiAmountString: ui,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# RPC Method Template
|
|
2
|
+
|
|
3
|
+
Use this template when adding new RPC methods to SolForge.
|
|
4
|
+
|
|
5
|
+
## File Template
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { PublicKey } from "@solana/web3.js"; // Only if needed
|
|
9
|
+
import type { RpcMethodHandler } from "../types"; // Adjust path based on location
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Implements the methodName RPC method
|
|
13
|
+
* @see https://docs.solana.com/api/http#methodname
|
|
14
|
+
*/
|
|
15
|
+
export const methodName: RpcMethodHandler = (id, params, context) => {
|
|
16
|
+
// 1. Parse parameters
|
|
17
|
+
const [param1, param2, config] = params;
|
|
18
|
+
|
|
19
|
+
// 2. Validate parameters (if needed)
|
|
20
|
+
if (!param1) {
|
|
21
|
+
return context.createErrorResponse(
|
|
22
|
+
id,
|
|
23
|
+
-32602,
|
|
24
|
+
"Missing required parameter: param1"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// 3. Execute the RPC logic
|
|
30
|
+
const result = context.svm.someOperation();
|
|
31
|
+
|
|
32
|
+
// 4. Format and return the response
|
|
33
|
+
return context.createSuccessResponse(id, {
|
|
34
|
+
context: {
|
|
35
|
+
slot: Number(context.slot),
|
|
36
|
+
apiVersion: "1.18.0" // If needed
|
|
37
|
+
},
|
|
38
|
+
value: result
|
|
39
|
+
});
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
// 5. Handle errors appropriately
|
|
42
|
+
return context.createErrorResponse(
|
|
43
|
+
id,
|
|
44
|
+
-32603, // Use appropriate error code
|
|
45
|
+
"Operation failed",
|
|
46
|
+
error.message
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Common Patterns
|
|
53
|
+
|
|
54
|
+
### Methods that modify state
|
|
55
|
+
```typescript
|
|
56
|
+
// After successful operation, increment slot/blockHeight
|
|
57
|
+
if (success) {
|
|
58
|
+
// This is handled in the main server now
|
|
59
|
+
// Just return success response
|
|
60
|
+
return context.createSuccessResponse(id, signature);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Methods with optional config
|
|
65
|
+
```typescript
|
|
66
|
+
const [address, config] = params;
|
|
67
|
+
const encoding = config?.encoding || "base64";
|
|
68
|
+
const commitment = config?.commitment || "confirmed";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Batch operations
|
|
72
|
+
```typescript
|
|
73
|
+
const results = addresses.map(addr => {
|
|
74
|
+
try {
|
|
75
|
+
// Process each item
|
|
76
|
+
return processItem(addr);
|
|
77
|
+
} catch {
|
|
78
|
+
// Return null for failed items
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Error Codes Reference
|
|
85
|
+
|
|
86
|
+
- `-32700`: Parse error (JSON parsing failed)
|
|
87
|
+
- `-32600`: Invalid request (not a valid JSON-RPC request)
|
|
88
|
+
- `-32601`: Method not found
|
|
89
|
+
- `-32602`: Invalid params
|
|
90
|
+
- `-32603`: Internal error
|
|
91
|
+
- `-32000`: Generic server error
|
|
92
|
+
- `-32001`: Resource not found
|
|
93
|
+
- `-32002`: Resource unavailable
|
|
94
|
+
- `-32003`: Transaction rejected
|
|
95
|
+
- `-32004`: Method not supported
|
|
96
|
+
- `-32005`: Limit exceeded
|
|
97
|
+
|
|
98
|
+
## Checklist
|
|
99
|
+
|
|
100
|
+
Before committing a new RPC method:
|
|
101
|
+
|
|
102
|
+
- [ ] Method follows naming convention (camelCase)
|
|
103
|
+
- [ ] File uses kebab-case naming
|
|
104
|
+
- [ ] Proper TypeScript types used
|
|
105
|
+
- [ ] Error handling implemented
|
|
106
|
+
- [ ] Success case tested
|
|
107
|
+
- [ ] Error cases tested
|
|
108
|
+
- [ ] Added to methods/index.ts
|
|
109
|
+
- [ ] Documentation updated in README.md
|
|
110
|
+
- [ ] Follows SolForge patterns
|
|
111
|
+
|
|
112
|
+
## Examples in Codebase
|
|
113
|
+
|
|
114
|
+
- Simple query: See `get-balance.ts`
|
|
115
|
+
- Complex response: See `get-account-info.ts`
|
|
116
|
+
- Transaction handling: See `send-transaction.ts`
|
|
117
|
+
- Batch operation: See `get-multiple-accounts.ts`
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import type { RpcMethodHandler } from "../../types";
|
|
3
|
+
import { parseAccountJson } from "./parsers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Implements the getAccountInfo RPC method
|
|
7
|
+
* @see https://docs.solana.com/api/http#getaccountinfo
|
|
8
|
+
*/
|
|
9
|
+
export const getAccountInfo: RpcMethodHandler = async (id, params, context) => {
|
|
10
|
+
const [pubkeyStr, config] = params;
|
|
11
|
+
const encoding = config?.encoding || "base64";
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
15
|
+
const account = context.svm.getAccount(pubkey);
|
|
16
|
+
|
|
17
|
+
if (!account) {
|
|
18
|
+
return context.createSuccessResponse(id, {
|
|
19
|
+
context: { slot: Number(context.slot) },
|
|
20
|
+
value: null,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const owner = new PublicKey(account.owner).toBase58();
|
|
25
|
+
// Opportunistic index update
|
|
26
|
+
try {
|
|
27
|
+
await context.store?.upsertAccounts([
|
|
28
|
+
{
|
|
29
|
+
address: pubkey.toBase58(),
|
|
30
|
+
lamports: Number(account.lamports || 0n),
|
|
31
|
+
ownerProgram: owner,
|
|
32
|
+
executable: !!account.executable,
|
|
33
|
+
rentEpoch: Number(account.rentEpoch || 0),
|
|
34
|
+
dataLen: account.data?.length ?? 0,
|
|
35
|
+
dataBase64: undefined,
|
|
36
|
+
lastSlot: Number(context.slot),
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
41
|
+
if (encoding === "jsonParsed") {
|
|
42
|
+
const parsed = parseAccountJson(
|
|
43
|
+
pubkey,
|
|
44
|
+
{
|
|
45
|
+
owner: new PublicKey(account.owner),
|
|
46
|
+
data: account.data ? new Uint8Array(account.data) : new Uint8Array(),
|
|
47
|
+
lamports: account.lamports,
|
|
48
|
+
executable: account.executable,
|
|
49
|
+
rentEpoch: account.rentEpoch,
|
|
50
|
+
},
|
|
51
|
+
context,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return context.createSuccessResponse(id, {
|
|
55
|
+
context: { slot: Number(context.slot) },
|
|
56
|
+
value: {
|
|
57
|
+
lamports: Number(account.lamports),
|
|
58
|
+
owner,
|
|
59
|
+
executable: account.executable,
|
|
60
|
+
rentEpoch: Number(account.rentEpoch || 0),
|
|
61
|
+
data: parsed || {
|
|
62
|
+
program: "unknown",
|
|
63
|
+
parsed: null,
|
|
64
|
+
space: account.data?.length ?? 0,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const accountInfo = {
|
|
71
|
+
lamports: Number(account.lamports),
|
|
72
|
+
owner,
|
|
73
|
+
data: [Buffer.from(account.data).toString("base64"), "base64"] as const,
|
|
74
|
+
executable: account.executable,
|
|
75
|
+
rentEpoch: Number(account.rentEpoch || 0),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return context.createSuccessResponse(id, {
|
|
79
|
+
context: { slot: Number(context.slot) },
|
|
80
|
+
value: accountInfo,
|
|
81
|
+
});
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
return context.createErrorResponse(
|
|
84
|
+
id,
|
|
85
|
+
-32602,
|
|
86
|
+
"Invalid params",
|
|
87
|
+
error.message,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import type { RpcMethodHandler } from "../../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Implements the getBalance RPC method
|
|
6
|
+
* @see https://docs.solana.com/api/http#getbalance
|
|
7
|
+
*/
|
|
8
|
+
export const getBalance: RpcMethodHandler = (id, params, context) => {
|
|
9
|
+
const [pubkeyStr] = params;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
13
|
+
const balance = context.svm.getBalance(pubkey);
|
|
14
|
+
|
|
15
|
+
return context.createSuccessResponse(id, {
|
|
16
|
+
context: { slot: Number(context.slot) },
|
|
17
|
+
value: Number(balance || 0n),
|
|
18
|
+
});
|
|
19
|
+
} catch (error: any) {
|
|
20
|
+
return context.createErrorResponse(
|
|
21
|
+
id,
|
|
22
|
+
-32602,
|
|
23
|
+
"Invalid params",
|
|
24
|
+
error.message,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import type { RpcMethodHandler } from "../../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Implements the getMultipleAccounts RPC method
|
|
6
|
+
* @see https://docs.solana.com/api/http#getmultipleaccounts
|
|
7
|
+
*/
|
|
8
|
+
export const getMultipleAccounts: RpcMethodHandler = async (
|
|
9
|
+
id,
|
|
10
|
+
params,
|
|
11
|
+
context,
|
|
12
|
+
) => {
|
|
13
|
+
const [pubkeys, config] = params;
|
|
14
|
+
const encoding = config?.encoding || "base64";
|
|
15
|
+
|
|
16
|
+
const accounts = pubkeys.map((pubkeyStr: string) => {
|
|
17
|
+
try {
|
|
18
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
19
|
+
const account = context.svm.getAccount(pubkey);
|
|
20
|
+
|
|
21
|
+
if (!account) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const owner = new PublicKey(account.owner).toBase58();
|
|
26
|
+
if (encoding === "jsonParsed") {
|
|
27
|
+
const program =
|
|
28
|
+
owner === "11111111111111111111111111111111" ? "system" : "unknown";
|
|
29
|
+
const space = account.data?.length ?? 0;
|
|
30
|
+
return {
|
|
31
|
+
lamports: Number(account.lamports),
|
|
32
|
+
owner,
|
|
33
|
+
executable: account.executable,
|
|
34
|
+
rentEpoch: Number(account.rentEpoch || 0),
|
|
35
|
+
data: {
|
|
36
|
+
program,
|
|
37
|
+
parsed: program === "system" ? { type: "account", info: {} } : null,
|
|
38
|
+
space,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
lamports: Number(account.lamports),
|
|
45
|
+
owner,
|
|
46
|
+
data: [Buffer.from(account.data).toString("base64"), "base64"] as const,
|
|
47
|
+
executable: account.executable,
|
|
48
|
+
rentEpoch: Number(account.rentEpoch || 0),
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Opportunistic index update
|
|
56
|
+
try {
|
|
57
|
+
const snaps: any[] = [];
|
|
58
|
+
for (const pubkeyStr of pubkeys) {
|
|
59
|
+
try {
|
|
60
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
61
|
+
const acc = context.svm.getAccount(pubkey);
|
|
62
|
+
if (!acc) continue;
|
|
63
|
+
const owner = new PublicKey(acc.owner).toBase58();
|
|
64
|
+
snaps.push({
|
|
65
|
+
address: pubkey.toBase58(),
|
|
66
|
+
lamports: Number(acc.lamports || 0n),
|
|
67
|
+
ownerProgram: owner,
|
|
68
|
+
executable: !!acc.executable,
|
|
69
|
+
rentEpoch: Number(acc.rentEpoch || 0),
|
|
70
|
+
dataLen: acc.data?.length ?? 0,
|
|
71
|
+
dataBase64: undefined,
|
|
72
|
+
lastSlot: Number(context.slot),
|
|
73
|
+
});
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
if (snaps.length) await context.store?.upsertAccounts(snaps);
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
return context.createSuccessResponse(id, {
|
|
80
|
+
context: { slot: Number(context.slot) },
|
|
81
|
+
value: accounts,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RpcMethodHandler } from "../../types";
|
|
2
|
+
import { getAccountInfo } from "./get-account-info";
|
|
3
|
+
|
|
4
|
+
export const getParsedAccountInfo: RpcMethodHandler = async (
|
|
5
|
+
id,
|
|
6
|
+
params,
|
|
7
|
+
context,
|
|
8
|
+
) => {
|
|
9
|
+
const [pubkey, config] = params || [];
|
|
10
|
+
const cfg = { ...(config || {}), encoding: "jsonParsed" };
|
|
11
|
+
try {
|
|
12
|
+
return await getAccountInfo(id, [pubkey, cfg], context);
|
|
13
|
+
} catch (error: any) {
|
|
14
|
+
return context.createErrorResponse(
|
|
15
|
+
id,
|
|
16
|
+
-32603,
|
|
17
|
+
"Internal error",
|
|
18
|
+
error.message,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account-related RPC methods
|
|
3
|
+
*
|
|
4
|
+
* This module contains all account-related RPC methods.
|
|
5
|
+
* Each method is in its own file to maintain modularity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { getAccountInfo } from "./get-account-info";
|
|
9
|
+
export { getBalance } from "./get-balance";
|
|
10
|
+
export { getMultipleAccounts } from "./get-multiple-accounts";
|
|
11
|
+
export { getParsedAccountInfo } from "./get-parsed-account-info";
|
|
12
|
+
export { requestAirdrop } from "./request-airdrop";
|