solforge 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agi/agi.sqlite +0 -0
- package/.claude/settings.local.json +9 -0
- package/.github/workflows/release-binaries.yml +133 -0
- package/.tmp/.787ebcdbf7b8fde8-00000000.hm +0 -0
- package/.tmp/.bffe6efebdf8aedc-00000000.hm +0 -0
- package/AGENTS.md +271 -0
- package/CLAUDE.md +106 -0
- package/PROJECT_STRUCTURE.md +124 -0
- package/README.md +367 -393
- package/SOLANA_KIT_GUIDE.md +251 -0
- package/SOLFORGE.md +119 -0
- package/biome.json +34 -0
- package/bun.lock +743 -0
- package/docs/bun-single-file-executable.md +585 -0
- package/docs/cli-plan.md +154 -0
- package/docs/data-indexing-plan.md +214 -0
- package/docs/gui-roadmap.md +202 -0
- package/drizzle/0000_friendly_millenium_guard.sql +53 -0
- package/drizzle/0001_stale_sentinels.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +329 -0
- package/drizzle/meta/0001_snapshot.json +345 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +12 -0
- package/index.ts +21 -0
- package/mint.sh +47 -0
- package/package.json +45 -69
- package/postcss.config.js +6 -0
- package/rpc-server.ts.backup +519 -0
- package/server/index.ts +5 -0
- package/server/lib/base58.ts +33 -0
- package/server/lib/faucet.ts +110 -0
- package/server/lib/spl-token.ts +57 -0
- package/server/methods/TEMPLATE.md +117 -0
- package/server/methods/account/get-account-info.ts +90 -0
- package/server/methods/account/get-balance.ts +27 -0
- package/server/methods/account/get-multiple-accounts.ts +83 -0
- package/server/methods/account/get-parsed-account-info.ts +21 -0
- package/server/methods/account/index.ts +12 -0
- package/server/methods/account/parsers/index.ts +52 -0
- package/server/methods/account/parsers/loader-upgradeable.ts +66 -0
- package/server/methods/account/parsers/spl-token.ts +237 -0
- package/server/methods/account/parsers/system.ts +4 -0
- package/server/methods/account/request-airdrop.ts +219 -0
- package/server/methods/admin/adopt-mint-authority.ts +94 -0
- package/server/methods/admin/clone-program-accounts.ts +55 -0
- package/server/methods/admin/clone-program.ts +152 -0
- package/server/methods/admin/clone-token-accounts.ts +117 -0
- package/server/methods/admin/clone-token-mint.ts +82 -0
- package/server/methods/admin/create-mint.ts +114 -0
- package/server/methods/admin/create-token-account.ts +137 -0
- package/server/methods/admin/helpers.ts +70 -0
- package/server/methods/admin/index.ts +10 -0
- package/server/methods/admin/list-mints.ts +21 -0
- package/server/methods/admin/load-program.ts +52 -0
- package/server/methods/admin/mint-to.ts +278 -0
- package/server/methods/block/get-block-height.ts +5 -0
- package/server/methods/block/get-block.ts +35 -0
- package/server/methods/block/get-blocks-with-limit.ts +23 -0
- package/server/methods/block/get-latest-blockhash.ts +12 -0
- package/server/methods/block/get-slot.ts +5 -0
- package/server/methods/block/index.ts +6 -0
- package/server/methods/block/is-blockhash-valid.ts +23 -0
- package/server/methods/epoch/get-cluster-nodes.ts +17 -0
- package/server/methods/epoch/get-epoch-info.ts +16 -0
- package/server/methods/epoch/get-epoch-schedule.ts +15 -0
- package/server/methods/epoch/get-highest-snapshot-slot.ts +9 -0
- package/server/methods/epoch/get-leader-schedule.ts +8 -0
- package/server/methods/epoch/get-max-retransmit-slot.ts +9 -0
- package/server/methods/epoch/get-max-shred-insert-slot.ts +9 -0
- package/server/methods/epoch/get-slot-leader.ts +6 -0
- package/server/methods/epoch/get-slot-leaders.ts +9 -0
- package/server/methods/epoch/get-stake-activation.ts +9 -0
- package/server/methods/epoch/get-stake-minimum-delegation.ts +9 -0
- package/server/methods/epoch/get-vote-accounts.ts +19 -0
- package/server/methods/epoch/index.ts +13 -0
- package/server/methods/epoch/minimum-ledger-slot.ts +5 -0
- package/server/methods/fee/get-fee-calculator-for-blockhash.ts +12 -0
- package/server/methods/fee/get-fee-for-message.ts +8 -0
- package/server/methods/fee/get-fee-rate-governor.ts +16 -0
- package/server/methods/fee/get-fees.ts +14 -0
- package/server/methods/fee/get-recent-prioritization-fees.ts +22 -0
- package/server/methods/fee/index.ts +5 -0
- package/server/methods/get-address-lookup-table.ts +31 -0
- package/server/methods/index.ts +265 -0
- package/server/methods/performance/get-recent-performance-samples.ts +25 -0
- package/server/methods/performance/get-transaction-count.ts +5 -0
- package/server/methods/performance/index.ts +2 -0
- package/server/methods/program/get-block-commitment.ts +9 -0
- package/server/methods/program/get-block-production.ts +14 -0
- package/server/methods/program/get-block-time.ts +21 -0
- package/server/methods/program/get-blocks.ts +11 -0
- package/server/methods/program/get-first-available-block.ts +9 -0
- package/server/methods/program/get-genesis-hash.ts +6 -0
- package/server/methods/program/get-identity.ts +6 -0
- package/server/methods/program/get-inflation-governor.ts +15 -0
- package/server/methods/program/get-inflation-rate.ts +10 -0
- package/server/methods/program/get-inflation-reward.ts +12 -0
- package/server/methods/program/get-largest-accounts.ts +8 -0
- package/server/methods/program/get-parsed-program-accounts.ts +12 -0
- package/server/methods/program/get-parsed-token-accounts-by-delegate.ts +12 -0
- package/server/methods/program/get-parsed-token-accounts-by-owner.ts +12 -0
- package/server/methods/program/get-program-accounts.ts +221 -0
- package/server/methods/program/get-supply.ts +13 -0
- package/server/methods/program/get-token-account-balance.ts +64 -0
- package/server/methods/program/get-token-accounts-by-delegate.ts +81 -0
- package/server/methods/program/get-token-accounts-by-owner.ts +390 -0
- package/server/methods/program/get-token-largest-accounts.ts +80 -0
- package/server/methods/program/get-token-supply.ts +38 -0
- package/server/methods/program/index.ts +21 -0
- package/server/methods/solforge/index.ts +155 -0
- package/server/methods/system/get-health.ts +5 -0
- package/server/methods/system/get-minimum-balance-for-rent-exemption.ts +13 -0
- package/server/methods/system/get-version.ts +9 -0
- package/server/methods/system/index.ts +3 -0
- package/server/methods/transaction/get-confirmed-transaction.ts +11 -0
- package/server/methods/transaction/get-parsed-transaction.ts +21 -0
- package/server/methods/transaction/get-signature-statuses.ts +72 -0
- package/server/methods/transaction/get-signatures-for-address.ts +45 -0
- package/server/methods/transaction/get-transaction.ts +428 -0
- package/server/methods/transaction/index.ts +7 -0
- package/server/methods/transaction/send-transaction.ts +232 -0
- package/server/methods/transaction/simulate-transaction.ts +56 -0
- package/server/rpc-server.ts +474 -0
- package/server/types.ts +74 -0
- package/server/ws-server.ts +171 -0
- package/sf.config.json +38 -0
- package/src/cli/bootstrap.ts +67 -0
- package/src/cli/commands/airdrop.ts +37 -0
- package/src/cli/commands/config.ts +39 -0
- package/src/cli/commands/mint.ts +187 -0
- package/src/cli/commands/program-clone.ts +124 -0
- package/src/cli/commands/program-load.ts +64 -0
- package/src/cli/commands/rpc-start.ts +46 -0
- package/src/cli/commands/token-adopt-authority.ts +37 -0
- package/src/cli/commands/token-clone.ts +113 -0
- package/src/cli/commands/token-create.ts +81 -0
- package/src/cli/main.ts +130 -0
- package/src/cli/run-solforge.ts +98 -0
- package/src/cli/setup-utils.ts +54 -0
- package/src/cli/setup-wizard.ts +256 -0
- package/src/cli/utils/args.ts +15 -0
- package/src/config/index.ts +130 -0
- package/src/db/index.ts +83 -0
- package/src/db/schema/accounts.ts +23 -0
- package/src/db/schema/address-signatures.ts +31 -0
- package/src/db/schema/index.ts +5 -0
- package/src/db/schema/meta-kv.ts +9 -0
- package/src/db/schema/transactions.ts +29 -0
- package/src/db/schema/tx-accounts.ts +33 -0
- package/src/db/tx-store.ts +229 -0
- package/src/gui/public/app.css +1 -0
- package/src/gui/public/index.html +19 -0
- package/src/gui/server.ts +297 -0
- package/src/gui/src/api.ts +127 -0
- package/src/gui/src/app.tsx +390 -0
- package/src/gui/src/components/airdrop-mint-form.tsx +216 -0
- package/src/gui/src/components/clone-program-modal.tsx +183 -0
- package/src/gui/src/components/clone-token-modal.tsx +211 -0
- package/src/gui/src/components/modal.tsx +127 -0
- package/src/gui/src/components/programs-panel.tsx +112 -0
- package/src/gui/src/components/status-panel.tsx +122 -0
- package/src/gui/src/components/tokens-panel.tsx +116 -0
- package/src/gui/src/hooks/use-interval.ts +17 -0
- package/src/gui/src/index.css +529 -0
- package/src/gui/src/main.tsx +17 -0
- package/src/migrations-bundled.ts +17 -0
- package/src/rpc/start.ts +44 -0
- package/tailwind.config.js +27 -0
- package/test-client.ts +120 -0
- package/tmp/inspect-html.ts +4 -0
- package/tmp/response-test.ts +5 -0
- package/tmp/test-html.ts +5 -0
- package/tmp/test-server.ts +13 -0
- package/tsconfig.json +24 -23
- package/LICENSE +0 -21
- package/scripts/postinstall.cjs +0 -103
- package/src/api-server-entry.ts +0 -109
- package/src/commands/add-program.ts +0 -337
- package/src/commands/init.ts +0 -122
- package/src/commands/list.ts +0 -136
- package/src/commands/mint.ts +0 -336
- package/src/commands/start.ts +0 -878
- package/src/commands/status.ts +0 -99
- package/src/commands/stop.ts +0 -406
- package/src/config/manager.ts +0 -157
- package/src/index.ts +0 -188
- package/src/services/api-server.ts +0 -532
- package/src/services/port-manager.ts +0 -177
- package/src/services/process-registry.ts +0 -154
- package/src/services/program-cloner.ts +0 -317
- package/src/services/token-cloner.ts +0 -809
- package/src/services/validator.ts +0 -295
- package/src/types/config.ts +0 -110
- package/src/utils/shell.ts +0 -110
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { LiteSVM } from "litesvm";
|
|
2
|
+
import {
|
|
3
|
+
PublicKey,
|
|
4
|
+
VersionedTransaction
|
|
5
|
+
} from "@solana/web3.js";
|
|
6
|
+
|
|
7
|
+
interface JsonRpcRequest {
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
id: string | number;
|
|
10
|
+
method: string;
|
|
11
|
+
params?: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface JsonRpcResponse {
|
|
15
|
+
jsonrpc: "2.0";
|
|
16
|
+
id: string | number;
|
|
17
|
+
result?: any;
|
|
18
|
+
error?: {
|
|
19
|
+
code: number;
|
|
20
|
+
message: string;
|
|
21
|
+
data?: any;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class LiteSVMRpcServer {
|
|
26
|
+
private svm: LiteSVM;
|
|
27
|
+
private slot: bigint = 1n;
|
|
28
|
+
private blockHeight: bigint = 1n;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.svm = new LiteSVM()
|
|
32
|
+
.withSysvars()
|
|
33
|
+
.withBuiltins()
|
|
34
|
+
.withDefaultPrograms()
|
|
35
|
+
.withLamports(1000000000000n)
|
|
36
|
+
.withBlockhashCheck(false)
|
|
37
|
+
.withTransactionHistory(0n)
|
|
38
|
+
.withSigverify(false);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private encodeBase58(bytes: Uint8Array): string {
|
|
42
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
43
|
+
const base = BigInt(ALPHABET.length);
|
|
44
|
+
|
|
45
|
+
let num = 0n;
|
|
46
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
47
|
+
num = num * 256n + BigInt(bytes[i] || 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let encoded = "";
|
|
51
|
+
while (num > 0n) {
|
|
52
|
+
const remainder = num % base;
|
|
53
|
+
num = num / base;
|
|
54
|
+
encoded = ALPHABET[Number(remainder)] + encoded;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
|
|
58
|
+
encoded = "1" + encoded;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return encoded || "1";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private decodeBase58(str: string): Uint8Array {
|
|
65
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
66
|
+
const base = BigInt(ALPHABET.length);
|
|
67
|
+
|
|
68
|
+
let num = 0n;
|
|
69
|
+
for (const char of str) {
|
|
70
|
+
const index = ALPHABET.indexOf(char);
|
|
71
|
+
if (index === -1) throw new Error("Invalid base58 character");
|
|
72
|
+
num = num * base + BigInt(index);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const bytes = [];
|
|
76
|
+
while (num > 0n) {
|
|
77
|
+
bytes.unshift(Number(num % 256n));
|
|
78
|
+
num = num / 256n;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < str.length && str[i] === "1"; i++) {
|
|
82
|
+
bytes.unshift(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return new Uint8Array(bytes.length > 0 ? bytes : [0]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private createSuccessResponse(id: string | number, result: any): JsonRpcResponse {
|
|
89
|
+
return {
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
id,
|
|
92
|
+
result
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private createErrorResponse(
|
|
97
|
+
id: string | number,
|
|
98
|
+
code: number,
|
|
99
|
+
message: string,
|
|
100
|
+
data?: any
|
|
101
|
+
): JsonRpcResponse {
|
|
102
|
+
return {
|
|
103
|
+
jsonrpc: "2.0",
|
|
104
|
+
id,
|
|
105
|
+
error: { code, message, data }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
110
|
+
const { method, params, id } = request;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
switch (method) {
|
|
114
|
+
case "getAccountInfo":
|
|
115
|
+
return this.handleGetAccountInfo(id, params);
|
|
116
|
+
|
|
117
|
+
case "getBalance":
|
|
118
|
+
return this.handleGetBalance(id, params);
|
|
119
|
+
|
|
120
|
+
case "getLatestBlockhash":
|
|
121
|
+
return this.handleGetLatestBlockhash(id, params);
|
|
122
|
+
|
|
123
|
+
case "sendTransaction":
|
|
124
|
+
return this.handleSendTransaction(id, params);
|
|
125
|
+
|
|
126
|
+
case "simulateTransaction":
|
|
127
|
+
return this.handleSimulateTransaction(id, params);
|
|
128
|
+
|
|
129
|
+
case "requestAirdrop":
|
|
130
|
+
return this.handleRequestAirdrop(id, params);
|
|
131
|
+
|
|
132
|
+
case "getSlot":
|
|
133
|
+
return this.handleGetSlot(id, params);
|
|
134
|
+
|
|
135
|
+
case "getBlockHeight":
|
|
136
|
+
return this.handleGetBlockHeight(id, params);
|
|
137
|
+
|
|
138
|
+
case "getTransaction":
|
|
139
|
+
return this.handleGetTransaction(id, params);
|
|
140
|
+
|
|
141
|
+
case "getSignatureStatuses":
|
|
142
|
+
return this.handleGetSignatureStatuses(id, params);
|
|
143
|
+
|
|
144
|
+
case "getMinimumBalanceForRentExemption":
|
|
145
|
+
return this.handleGetMinimumBalanceForRentExemption(id, params);
|
|
146
|
+
|
|
147
|
+
case "getMultipleAccounts":
|
|
148
|
+
return this.handleGetMultipleAccounts(id, params);
|
|
149
|
+
|
|
150
|
+
case "getHealth":
|
|
151
|
+
return this.createSuccessResponse(id, "ok");
|
|
152
|
+
|
|
153
|
+
case "getVersion":
|
|
154
|
+
return this.createSuccessResponse(id, {
|
|
155
|
+
"solana-core": "1.18.0",
|
|
156
|
+
"feature-set": 1
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
return this.createErrorResponse(
|
|
161
|
+
id,
|
|
162
|
+
-32601,
|
|
163
|
+
`Method not found: ${method}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
return this.createErrorResponse(
|
|
168
|
+
id,
|
|
169
|
+
-32603,
|
|
170
|
+
"Internal error",
|
|
171
|
+
error.message
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private handleGetAccountInfo(id: string | number, params: any): JsonRpcResponse {
|
|
177
|
+
const [pubkeyStr, config] = params;
|
|
178
|
+
const encoding = config?.encoding || "base64";
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
182
|
+
const account = this.svm.getAccount(pubkey);
|
|
183
|
+
|
|
184
|
+
if (!account) {
|
|
185
|
+
return this.createSuccessResponse(id, {
|
|
186
|
+
context: { slot: Number(this.slot) },
|
|
187
|
+
value: null
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const accountInfo = {
|
|
192
|
+
lamports: Number(account.lamports),
|
|
193
|
+
owner: new PublicKey(account.owner).toBase58(),
|
|
194
|
+
data: encoding === "base64"
|
|
195
|
+
? [Buffer.from(account.data).toString("base64"), encoding]
|
|
196
|
+
: Array.from(account.data),
|
|
197
|
+
executable: account.executable,
|
|
198
|
+
rentEpoch: Number(account.rentEpoch || 0)
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return this.createSuccessResponse(id, {
|
|
202
|
+
context: { slot: Number(this.slot) },
|
|
203
|
+
value: accountInfo
|
|
204
|
+
});
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
return this.createErrorResponse(id, -32602, "Invalid params", error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private handleGetBalance(id: string | number, params: any): JsonRpcResponse {
|
|
211
|
+
const [pubkeyStr] = params;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
215
|
+
const balance = this.svm.getBalance(pubkey);
|
|
216
|
+
|
|
217
|
+
return this.createSuccessResponse(id, {
|
|
218
|
+
context: { slot: Number(this.slot) },
|
|
219
|
+
value: Number(balance || 0n)
|
|
220
|
+
});
|
|
221
|
+
} catch (error: any) {
|
|
222
|
+
return this.createErrorResponse(id, -32602, "Invalid params", error.message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private handleGetLatestBlockhash(id: string | number, params: any): JsonRpcResponse {
|
|
227
|
+
const blockhash = this.svm.latestBlockhash();
|
|
228
|
+
|
|
229
|
+
return this.createSuccessResponse(id, {
|
|
230
|
+
context: { slot: Number(this.slot) },
|
|
231
|
+
value: {
|
|
232
|
+
blockhash,
|
|
233
|
+
lastValidBlockHeight: Number(this.blockHeight + 150n)
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private handleSendTransaction(id: string | number, params: any): JsonRpcResponse {
|
|
239
|
+
const [encodedTx, config] = params;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const txData = Buffer.from(encodedTx, "base64");
|
|
243
|
+
const tx = VersionedTransaction.deserialize(txData);
|
|
244
|
+
|
|
245
|
+
const result = this.svm.sendTransaction(tx);
|
|
246
|
+
|
|
247
|
+
if ("err" in result) {
|
|
248
|
+
return this.createErrorResponse(
|
|
249
|
+
id,
|
|
250
|
+
-32002,
|
|
251
|
+
"Transaction simulation failed",
|
|
252
|
+
result.err
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const signature = tx.signatures[0] ? this.encodeBase58(tx.signatures[0]) : this.encodeBase58(new Uint8Array(64).fill(0));
|
|
257
|
+
|
|
258
|
+
this.slot += 1n;
|
|
259
|
+
this.blockHeight += 1n;
|
|
260
|
+
|
|
261
|
+
return this.createSuccessResponse(id, signature);
|
|
262
|
+
} catch (error: any) {
|
|
263
|
+
return this.createErrorResponse(
|
|
264
|
+
id,
|
|
265
|
+
-32003,
|
|
266
|
+
"Transaction failed",
|
|
267
|
+
error.message
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private handleSimulateTransaction(id: string | number, params: any): JsonRpcResponse {
|
|
273
|
+
const [encodedTx, config] = params;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const txData = Buffer.from(encodedTx, "base64");
|
|
277
|
+
const tx = VersionedTransaction.deserialize(txData);
|
|
278
|
+
|
|
279
|
+
const result = this.svm.simulateTransaction(tx);
|
|
280
|
+
|
|
281
|
+
if ("err" in result) {
|
|
282
|
+
const errorMeta = result.meta();
|
|
283
|
+
return this.createSuccessResponse(id, {
|
|
284
|
+
context: { slot: Number(this.slot) },
|
|
285
|
+
value: {
|
|
286
|
+
err: result.err(),
|
|
287
|
+
logs: errorMeta.logs(),
|
|
288
|
+
accounts: null,
|
|
289
|
+
unitsConsumed: Number(errorMeta.computeUnitsConsumed()),
|
|
290
|
+
returnData: null
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const meta = result.meta();
|
|
296
|
+
const returnData = meta.returnData();
|
|
297
|
+
|
|
298
|
+
return this.createSuccessResponse(id, {
|
|
299
|
+
context: { slot: Number(this.slot) },
|
|
300
|
+
value: {
|
|
301
|
+
err: null,
|
|
302
|
+
logs: meta.logs(),
|
|
303
|
+
accounts: null,
|
|
304
|
+
unitsConsumed: Number(meta.computeUnitsConsumed()),
|
|
305
|
+
returnData: returnData ? {
|
|
306
|
+
programId: this.encodeBase58(returnData.programId()),
|
|
307
|
+
data: [Buffer.from(returnData.data()).toString("base64"), "base64"]
|
|
308
|
+
} : null
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
} catch (error: any) {
|
|
312
|
+
return this.createErrorResponse(
|
|
313
|
+
id,
|
|
314
|
+
-32003,
|
|
315
|
+
"Simulation failed",
|
|
316
|
+
error.message
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private handleRequestAirdrop(id: string | number, params: any): JsonRpcResponse {
|
|
322
|
+
const [pubkeyStr, lamports] = params;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
326
|
+
const result = this.svm.airdrop(pubkey, BigInt(lamports));
|
|
327
|
+
|
|
328
|
+
if (!result || "err" in result) {
|
|
329
|
+
return this.createErrorResponse(
|
|
330
|
+
id,
|
|
331
|
+
-32003,
|
|
332
|
+
"Airdrop failed",
|
|
333
|
+
result?.err || "Unknown error"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const signature = this.encodeBase58(new Uint8Array(64).fill(1));
|
|
338
|
+
|
|
339
|
+
this.slot += 1n;
|
|
340
|
+
this.blockHeight += 1n;
|
|
341
|
+
|
|
342
|
+
return this.createSuccessResponse(id, signature);
|
|
343
|
+
} catch (error: any) {
|
|
344
|
+
return this.createErrorResponse(id, -32602, "Invalid params", error.message);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private handleGetSlot(id: string | number, params: any): JsonRpcResponse {
|
|
349
|
+
return this.createSuccessResponse(id, Number(this.slot));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private handleGetBlockHeight(id: string | number, params: any): JsonRpcResponse {
|
|
353
|
+
return this.createSuccessResponse(id, Number(this.blockHeight));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private handleGetTransaction(id: string | number, params: any): JsonRpcResponse {
|
|
357
|
+
const [signature] = params;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const sigBytes = this.decodeBase58(signature);
|
|
361
|
+
const tx = this.svm.getTransaction(sigBytes);
|
|
362
|
+
|
|
363
|
+
if (!tx) {
|
|
364
|
+
return this.createSuccessResponse(id, null);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const isError = "err" in tx;
|
|
368
|
+
const logs = isError ? tx.meta().logs() : tx.logs();
|
|
369
|
+
|
|
370
|
+
return this.createSuccessResponse(id, {
|
|
371
|
+
slot: Number(this.slot),
|
|
372
|
+
transaction: {
|
|
373
|
+
signatures: [signature]
|
|
374
|
+
},
|
|
375
|
+
meta: {
|
|
376
|
+
err: isError ? tx.err() : null,
|
|
377
|
+
fee: 5000,
|
|
378
|
+
preBalances: [],
|
|
379
|
+
postBalances: [],
|
|
380
|
+
innerInstructions: [],
|
|
381
|
+
logMessages: logs,
|
|
382
|
+
preTokenBalances: [],
|
|
383
|
+
postTokenBalances: [],
|
|
384
|
+
rewards: []
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
} catch (error: any) {
|
|
388
|
+
return this.createErrorResponse(id, -32602, "Invalid params", error.message);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private handleGetSignatureStatuses(id: string | number, params: any): JsonRpcResponse {
|
|
393
|
+
const [signatures, config] = params;
|
|
394
|
+
|
|
395
|
+
const statuses = signatures.map((sig: string) => {
|
|
396
|
+
try {
|
|
397
|
+
const sigBytes = this.decodeBase58(sig);
|
|
398
|
+
const tx = this.svm.getTransaction(sigBytes);
|
|
399
|
+
|
|
400
|
+
if (!tx) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
slot: Number(this.slot),
|
|
406
|
+
confirmations: 0,
|
|
407
|
+
err: "err" in tx ? tx.err : null,
|
|
408
|
+
confirmationStatus: "finalized"
|
|
409
|
+
};
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return this.createSuccessResponse(id, {
|
|
416
|
+
context: { slot: Number(this.slot) },
|
|
417
|
+
value: statuses
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private handleGetMinimumBalanceForRentExemption(
|
|
422
|
+
id: string | number,
|
|
423
|
+
params: any
|
|
424
|
+
): JsonRpcResponse {
|
|
425
|
+
const [dataLength] = params;
|
|
426
|
+
const minBalance = this.svm.minimumBalanceForRentExemption(BigInt(dataLength));
|
|
427
|
+
|
|
428
|
+
return this.createSuccessResponse(id, Number(minBalance));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private handleGetMultipleAccounts(id: string | number, params: any): JsonRpcResponse {
|
|
432
|
+
const [pubkeys, config] = params;
|
|
433
|
+
const encoding = config?.encoding || "base64";
|
|
434
|
+
|
|
435
|
+
const accounts = pubkeys.map((pubkeyStr: string) => {
|
|
436
|
+
try {
|
|
437
|
+
const pubkey = new PublicKey(pubkeyStr);
|
|
438
|
+
const account = this.svm.getAccount(pubkey);
|
|
439
|
+
|
|
440
|
+
if (!account) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
lamports: Number(account.lamports),
|
|
446
|
+
owner: new PublicKey(account.owner).toBase58(),
|
|
447
|
+
data: encoding === "base64"
|
|
448
|
+
? [Buffer.from(account.data).toString("base64"), encoding]
|
|
449
|
+
: Array.from(account.data),
|
|
450
|
+
executable: account.executable,
|
|
451
|
+
rentEpoch: Number(account.rentEpoch || 0)
|
|
452
|
+
};
|
|
453
|
+
} catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return this.createSuccessResponse(id, {
|
|
459
|
+
context: { slot: Number(this.slot) },
|
|
460
|
+
value: accounts
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function createLiteSVMRpcServer(port: number = 8899) {
|
|
466
|
+
const server = new LiteSVMRpcServer();
|
|
467
|
+
|
|
468
|
+
const bunServer = Bun.serve({
|
|
469
|
+
port,
|
|
470
|
+
async fetch(req) {
|
|
471
|
+
if (req.method === "POST") {
|
|
472
|
+
try {
|
|
473
|
+
const body = await req.json();
|
|
474
|
+
|
|
475
|
+
if (Array.isArray(body)) {
|
|
476
|
+
const responses = await Promise.all(
|
|
477
|
+
body.map(request => server.handleRequest(request))
|
|
478
|
+
);
|
|
479
|
+
return Response.json(responses);
|
|
480
|
+
} else {
|
|
481
|
+
const response = await server.handleRequest(body as JsonRpcRequest);
|
|
482
|
+
return Response.json(response);
|
|
483
|
+
}
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return Response.json({
|
|
486
|
+
jsonrpc: "2.0",
|
|
487
|
+
id: null,
|
|
488
|
+
error: {
|
|
489
|
+
code: -32700,
|
|
490
|
+
message: "Parse error"
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (req.method === "OPTIONS") {
|
|
497
|
+
return new Response(null, {
|
|
498
|
+
headers: {
|
|
499
|
+
"Access-Control-Allow-Origin": "*",
|
|
500
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
501
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return new Response("Method not allowed", { status: 405 });
|
|
507
|
+
},
|
|
508
|
+
error(error) {
|
|
509
|
+
console.error("Server error:", error);
|
|
510
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
console.log(`🚀 LiteSVM RPC Server running on http://localhost:${port}`);
|
|
515
|
+
console.log(` Compatible with Solana RPC API`);
|
|
516
|
+
console.log(` Use with: solana config set -u http://localhost:${port}`);
|
|
517
|
+
|
|
518
|
+
return bunServer;
|
|
519
|
+
}
|
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
|
+
}
|