solforge 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/scripts/postinstall.cjs +3 -3
  3. package/server/lib/base58.ts +1 -1
  4. package/server/lib/instruction-parser.ts +242 -0
  5. package/server/methods/account/get-account-info.ts +3 -7
  6. package/server/methods/account/get-balance.ts +3 -7
  7. package/server/methods/account/get-multiple-accounts.ts +2 -1
  8. package/server/methods/account/get-parsed-account-info.ts +3 -7
  9. package/server/methods/account/parsers/index.ts +2 -2
  10. package/server/methods/account/parsers/loader-upgradeable.ts +14 -1
  11. package/server/methods/account/parsers/spl-token.ts +29 -10
  12. package/server/methods/account/request-airdrop.ts +122 -86
  13. package/server/methods/admin/mint-to.ts +11 -38
  14. package/server/methods/block/get-block.ts +3 -7
  15. package/server/methods/block/get-blocks-with-limit.ts +3 -7
  16. package/server/methods/block/is-blockhash-valid.ts +3 -7
  17. package/server/methods/get-address-lookup-table.ts +3 -7
  18. package/server/methods/program/get-program-accounts.ts +9 -9
  19. package/server/methods/program/get-token-account-balance.ts +3 -7
  20. package/server/methods/program/get-token-accounts-by-delegate.ts +4 -3
  21. package/server/methods/program/get-token-accounts-by-owner.ts +54 -33
  22. package/server/methods/program/get-token-largest-accounts.ts +3 -2
  23. package/server/methods/program/get-token-supply.ts +3 -2
  24. package/server/methods/solforge/index.ts +9 -6
  25. package/server/methods/transaction/get-parsed-transaction.ts +3 -7
  26. package/server/methods/transaction/get-signature-statuses.ts +14 -7
  27. package/server/methods/transaction/get-signatures-for-address.ts +3 -7
  28. package/server/methods/transaction/get-transaction.ts +434 -287
  29. package/server/methods/transaction/inner-instructions.test.ts +63 -0
  30. package/server/methods/transaction/send-transaction.ts +248 -56
  31. package/server/methods/transaction/simulate-transaction.ts +3 -2
  32. package/server/rpc-server.ts +98 -61
  33. package/server/types.ts +65 -30
  34. package/server/ws-server.ts +11 -7
  35. package/src/api-server-entry.ts +5 -5
  36. package/src/cli/commands/airdrop.ts +2 -2
  37. package/src/cli/commands/config.ts +2 -2
  38. package/src/cli/commands/mint.ts +3 -3
  39. package/src/cli/commands/program-clone.ts +9 -11
  40. package/src/cli/commands/program-load.ts +3 -3
  41. package/src/cli/commands/rpc-start.ts +7 -7
  42. package/src/cli/commands/token-adopt-authority.ts +1 -1
  43. package/src/cli/commands/token-clone.ts +5 -6
  44. package/src/cli/commands/token-create.ts +5 -5
  45. package/src/cli/main.ts +33 -36
  46. package/src/cli/run-solforge.ts +3 -3
  47. package/src/cli/setup-wizard.ts +8 -6
  48. package/src/commands/add-program.ts +1 -1
  49. package/src/commands/init.ts +2 -2
  50. package/src/commands/mint.ts +5 -6
  51. package/src/commands/start.ts +10 -9
  52. package/src/commands/status.ts +1 -1
  53. package/src/commands/stop.ts +1 -1
  54. package/src/config/index.ts +33 -17
  55. package/src/config/manager.ts +3 -3
  56. package/src/db/index.ts +2 -2
  57. package/src/db/schema/index.ts +1 -0
  58. package/src/db/schema/transactions.ts +29 -22
  59. package/src/db/schema/tx-account-states.ts +21 -0
  60. package/src/db/tx-store.ts +113 -76
  61. package/src/gui/public/app.css +13 -13
  62. package/src/gui/server.ts +1 -1
  63. package/src/gui/src/api.ts +1 -1
  64. package/src/gui/src/app.tsx +49 -17
  65. package/src/gui/src/components/airdrop-mint-form.tsx +32 -8
  66. package/src/gui/src/components/clone-program-modal.tsx +25 -6
  67. package/src/gui/src/components/clone-token-modal.tsx +25 -6
  68. package/src/gui/src/components/modal.tsx +6 -1
  69. package/src/gui/src/components/status-panel.tsx +1 -1
  70. package/src/index.ts +19 -6
  71. package/src/migrations-bundled.ts +8 -2
  72. package/src/services/api-server.ts +41 -19
  73. package/src/services/port-manager.ts +7 -10
  74. package/src/services/process-registry.ts +4 -5
  75. package/src/services/program-cloner.ts +4 -4
  76. package/src/services/token-cloner.ts +4 -4
  77. package/src/services/validator.ts +2 -4
  78. package/src/types/config.ts +2 -2
  79. package/src/utils/shell.ts +1 -1
  80. package/src/utils/token-loader.ts +2 -2
@@ -0,0 +1,63 @@
1
+ import { test, expect } from "bun:test";
2
+ import {
3
+ Keypair,
4
+ LAMPORTS_PER_SOL,
5
+ TransactionMessage,
6
+ VersionedTransaction,
7
+ PublicKey,
8
+ } from "@solana/web3.js";
9
+ import {
10
+ getAssociatedTokenAddress,
11
+ createAssociatedTokenAccountInstruction,
12
+ } from "@solana/spl-token";
13
+ import { LiteSVMRpcServer } from "../../rpc-server";
14
+
15
+ type RpcResp<T = any> = { jsonrpc: "2.0"; id: number; result?: T; error?: { code: number; message: string; data?: unknown } };
16
+
17
+ function jsonReq(method: string, params?: unknown) {
18
+ return { jsonrpc: "2.0", id: Math.floor(Math.random() * 1e9), method, params };
19
+ }
20
+
21
+ test("captures inner instructions for ATA create (CPI)", async () => {
22
+ const server = new LiteSVMRpcServer();
23
+ async function call<T = any>(method: string, params?: unknown): Promise<RpcResp<T>> {
24
+ return (await server.handleRequest(jsonReq(method, params))) as RpcResp<T>;
25
+ }
26
+
27
+ // Payer + airdrop
28
+ const payer = Keypair.generate();
29
+ const recip = Keypair.generate();
30
+ await call("requestAirdrop", [payer.publicKey.toBase58(), 1 * LAMPORTS_PER_SOL]);
31
+
32
+ // Create a test mint via admin helper
33
+ const mintResp = await call<{ mint: string }>("solforgeCreateMint", [null, 6, null]);
34
+ expect(mintResp.error).toBeUndefined();
35
+ const mint = new PublicKey(mintResp.result!.mint);
36
+
37
+ // Build 1 ATA instruction that triggers CPIs into system + token
38
+ const bh = await call<{ value: { blockhash: string } }>("getLatestBlockhash", []);
39
+ const ata = await getAssociatedTokenAddress(mint, recip.publicKey, false);
40
+ const ix = createAssociatedTokenAccountInstruction(payer.publicKey, ata, recip.publicKey, mint);
41
+ const msg = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: bh.result!.value.blockhash, instructions: [ix] }).compileToLegacyMessage();
42
+ const tx = new VersionedTransaction(msg);
43
+ tx.sign([payer]);
44
+
45
+ const sigResp = await call<string>("sendTransaction", [Buffer.from(tx.serialize()).toString("base64")]);
46
+ expect(sigResp.error).toBeUndefined();
47
+ const sig = sigResp.result!;
48
+
49
+ const txResp = await call<any>("getTransaction", [sig, { encoding: "json" }]);
50
+ expect(txResp.error).toBeUndefined();
51
+ const tr = txResp.result!;
52
+
53
+ // At least one top-level instruction
54
+ expect(Array.isArray(tr.transaction.message.instructions)).toBe(true);
55
+ expect(tr.transaction.message.instructions.length).toBe(1);
56
+
57
+ // Check inner instructions captured or (worst case) logs exist
58
+ const ii = tr.meta.innerInstructions;
59
+ const logs = tr.meta.logMessages || [];
60
+ expect(Array.isArray(ii)).toBe(true);
61
+ // At minimum, either we have structured inner ixs, or logs were captured
62
+ expect(ii.length > 0 || logs.length > 0).toBe(true);
63
+ });
@@ -16,32 +16,67 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
16
16
  const tx = VersionedTransaction.deserialize(txData);
17
17
 
18
18
  // Snapshot pre balances
19
- const msg: any = tx.message as any;
20
- const rawKeys: any[] = Array.isArray(msg.staticAccountKeys)
19
+ const msg = tx.message as unknown as {
20
+ staticAccountKeys?: unknown[];
21
+ accountKeys?: unknown[];
22
+ };
23
+ const rawKeys: unknown[] = Array.isArray(msg.staticAccountKeys)
21
24
  ? msg.staticAccountKeys
22
25
  : Array.isArray(msg.accountKeys)
23
26
  ? msg.accountKeys
24
27
  : [];
25
- const staticKeys = rawKeys
26
- .map((k: any) => {
27
- try {
28
- return typeof k === "string" ? new PublicKey(k) : (k as PublicKey);
29
- } catch {
30
- return undefined;
31
- }
32
- })
33
- .filter(Boolean) as PublicKey[];
34
- const preBalances = staticKeys.map((pk) => {
35
- try {
36
- return Number(context.svm.getBalance(pk));
37
- } catch {
38
- return 0;
39
- }
40
- });
28
+ const staticKeys = rawKeys
29
+ .map((k) => {
30
+ try {
31
+ return typeof k === "string" ? new PublicKey(k) : (k as PublicKey);
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ })
36
+ .filter(Boolean) as PublicKey[];
37
+ // Pre snapshots and balances
38
+ const preBalances = staticKeys.map((pk) => {
39
+ try {
40
+ return Number(context.svm.getBalance(pk));
41
+ } catch {
42
+ return 0;
43
+ }
44
+ });
45
+ const preAccountStates = staticKeys.map((pk) => {
46
+ try {
47
+ const addr = pk.toBase58();
48
+ const acc = context.svm.getAccount(pk);
49
+ if (!acc) return { address: addr, pre: null } as const;
50
+ return {
51
+ address: addr,
52
+ pre: {
53
+ lamports: Number(acc.lamports || 0n),
54
+ ownerProgram: new PublicKey(acc.owner).toBase58(),
55
+ executable: !!acc.executable,
56
+ rentEpoch: Number(acc.rentEpoch || 0),
57
+ dataLen: acc.data?.length ?? 0,
58
+ dataBase64: undefined,
59
+ lastSlot: Number(context.slot),
60
+ },
61
+ } as const;
62
+ } catch {
63
+ return { address: pk.toBase58(), pre: null } as const;
64
+ }
65
+ });
66
+ try {
67
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
68
+ console.debug(
69
+ `[tx-capture] pre snapshots: keys=${staticKeys.length} captured=${preAccountStates.length}`,
70
+ );
71
+ }
72
+ } catch {}
41
73
 
42
74
  // Collect SPL token accounts from instructions for pre/post token balance snapshots
43
- const msgAny: any = msg;
44
- const compiled = Array.isArray(msgAny.compiledInstructions)
75
+ const msgAny = msg as unknown as {
76
+ compiledInstructions?: unknown[];
77
+ instructions?: unknown[];
78
+ };
79
+ const compiled: unknown[] = Array.isArray(msgAny.compiledInstructions)
45
80
  ? msgAny.compiledInstructions
46
81
  : Array.isArray(msgAny.instructions)
47
82
  ? msgAny.instructions
@@ -67,7 +102,7 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
67
102
  } catch {}
68
103
  }
69
104
  // Pre token balances
70
- const preTokenBalances: any[] = [];
105
+ const preTokenBalances: unknown[] = [];
71
106
  const ataToInfo = new Map<
72
107
  string,
73
108
  {
@@ -124,11 +159,12 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
124
159
  } catch {}
125
160
  }
126
161
 
127
- const result = context.svm.sendTransaction(tx);
162
+ const result = context.svm.sendTransaction(tx);
128
163
 
129
164
  try {
130
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
- const maybeErr = (result as any).err?.();
165
+ const rawErr = (result as { err?: unknown }).err;
166
+ const maybeErr =
167
+ typeof rawErr === "function" ? (rawErr as () => unknown)() : rawErr;
132
168
  if (maybeErr) {
133
169
  return context.createErrorResponse(
134
170
  id,
@@ -139,20 +175,48 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
139
175
  }
140
176
  } catch {}
141
177
 
142
- const signature = tx.signatures[0]
143
- ? context.encodeBase58(tx.signatures[0])
144
- : context.encodeBase58(new Uint8Array(64).fill(0));
145
- context.notifySignature(signature);
146
- // Snapshot post balances and capture logs for rich view
147
- const postBalances = staticKeys.map((pk) => {
148
- try {
149
- return Number(context.svm.getBalance(pk));
150
- } catch {
151
- return 0;
152
- }
153
- });
178
+ const signature = tx.signatures[0]
179
+ ? context.encodeBase58(tx.signatures[0])
180
+ : context.encodeBase58(new Uint8Array(64).fill(0));
181
+ context.notifySignature(signature);
182
+ // Snapshot post balances and capture logs for rich view
183
+ const postBalances = staticKeys.map((pk) => {
184
+ try {
185
+ return Number(context.svm.getBalance(pk));
186
+ } catch {
187
+ return 0;
188
+ }
189
+ });
190
+ const postAccountStates = staticKeys.map((pk) => {
191
+ try {
192
+ const addr = pk.toBase58();
193
+ const acc = context.svm.getAccount(pk);
194
+ if (!acc) return { address: addr, post: null } as const;
195
+ return {
196
+ address: addr,
197
+ post: {
198
+ lamports: Number(acc.lamports || 0n),
199
+ ownerProgram: new PublicKey(acc.owner).toBase58(),
200
+ executable: !!acc.executable,
201
+ rentEpoch: Number(acc.rentEpoch || 0),
202
+ dataLen: acc.data?.length ?? 0,
203
+ dataBase64: undefined,
204
+ lastSlot: Number(context.slot),
205
+ },
206
+ } as const;
207
+ } catch {
208
+ return { address: pk.toBase58(), post: null } as const;
209
+ }
210
+ });
211
+ try {
212
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
213
+ console.debug(
214
+ `[tx-capture] post snapshots: keys=${staticKeys.length} captured=${postAccountStates.length}`,
215
+ );
216
+ }
217
+ } catch {}
154
218
  // Post token balances
155
- const postTokenBalances: any[] = [];
219
+ const postTokenBalances: unknown[] = [];
156
220
  for (const addr of tokenAccountSet) {
157
221
  try {
158
222
  const pk = new PublicKey(addr);
@@ -202,31 +266,159 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
202
266
  }
203
267
  } catch {}
204
268
  }
205
- let logs: string[] = [];
206
- try {
207
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
- const anyRes: any = result;
209
- if (typeof anyRes?.logs === "function") logs = anyRes.logs();
210
- else if (typeof anyRes?.meta === "function")
211
- logs = anyRes.meta()?.logs?.() ?? [];
212
- } catch {}
213
- context.recordTransaction(signature, tx, {
214
- logs,
215
- fee: 5000,
216
- blockTime: Math.floor(Date.now() / 1000),
217
- preBalances,
218
- postBalances,
219
- preTokenBalances,
220
- postTokenBalances,
221
- });
269
+ let logs: string[] = [];
270
+ let innerInstructions: unknown[] = [];
271
+ let computeUnits: number | null = null;
272
+ let returnData: { programId: string; dataBase64: string } | null = null;
273
+ try {
274
+ const DBG = process.env.DEBUG_TX_CAPTURE === "1";
275
+ const r: any = result as any;
276
+ // Logs can be on TransactionMetadata or in meta() for failures
277
+ try {
278
+ if (typeof r?.logs === "function") logs = r.logs();
279
+ } catch {}
280
+ let metaObj: any | undefined;
281
+ // Success shape: methods on result
282
+ if (
283
+ typeof r?.innerInstructions === "function" ||
284
+ typeof r?.computeUnitsConsumed === "function" ||
285
+ typeof r?.returnData === "function"
286
+ ) {
287
+ metaObj = r;
288
+ }
289
+ // Failed shape: meta() returns TransactionMetadata
290
+ if (!metaObj && typeof r?.meta === "function") {
291
+ try {
292
+ metaObj = r.meta();
293
+ if (!logs.length && typeof metaObj?.logs === "function") {
294
+ logs = metaObj.logs();
295
+ }
296
+ } catch (e) {
297
+ if (DBG)
298
+ console.debug("[tx-capture] meta() threw while extracting:", e);
299
+ }
300
+ }
301
+ // Extract richer metadata from whichever object exposes it
302
+ if (metaObj) {
303
+ try {
304
+ const inner = metaObj.innerInstructions?.();
305
+ if (Array.isArray(inner)) {
306
+ innerInstructions = inner.map((group: any, index: number) => {
307
+ const instructions = Array.isArray(group)
308
+ ? group
309
+ .map((ii: any) => {
310
+ try {
311
+ const inst = ii.instruction?.();
312
+ const accIdxs: number[] = Array.from(
313
+ inst?.accounts?.() || [],
314
+ );
315
+ const dataBytes: Uint8Array =
316
+ inst?.data?.() || new Uint8Array();
317
+ return {
318
+ programIdIndex: Number(
319
+ inst?.programIdIndex?.() ?? 0,
320
+ ),
321
+ accounts: accIdxs,
322
+ data: context.encodeBase58(dataBytes),
323
+ stackHeight: Number(ii.stackHeight?.() ?? 0),
324
+ };
325
+ } catch {
326
+ return null;
327
+ }
328
+ })
329
+ .filter(Boolean)
330
+ : [];
331
+ return { index, instructions };
332
+ });
333
+ }
334
+ } catch (e) {
335
+ if (DBG)
336
+ console.debug(
337
+ "[tx-capture] innerInstructions extraction failed:",
338
+ e,
339
+ );
340
+ }
341
+ try {
342
+ const cu = metaObj.computeUnitsConsumed?.();
343
+ if (typeof cu === "bigint") computeUnits = Number(cu);
344
+ } catch (e) {
345
+ if (DBG)
346
+ console.debug(
347
+ "[tx-capture] computeUnitsConsumed extraction failed:",
348
+ e,
349
+ );
350
+ }
351
+ try {
352
+ const rd = metaObj.returnData?.();
353
+ if (rd) {
354
+ const pid = new PublicKey(rd.programId()).toBase58();
355
+ const dataB64 = Buffer.from(rd.data()).toString("base64");
356
+ returnData = { programId: pid, dataBase64: dataB64 };
357
+ }
358
+ } catch (e) {
359
+ if (DBG)
360
+ console.debug(
361
+ "[tx-capture] returnData extraction failed:",
362
+ e,
363
+ );
364
+ }
365
+ } else if (DBG) {
366
+ console.debug(
367
+ "[tx-capture] no metadata object found on result shape",
368
+ );
369
+ }
370
+ } catch {}
371
+ try {
372
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
373
+ console.debug(
374
+ `[tx-capture] sendTransaction meta: logs=${logs.length} innerGroups=${Array.isArray(innerInstructions) ? innerInstructions.length : 0} computeUnits=${computeUnits} returnData=${returnData ? "yes" : "no"}`,
375
+ );
376
+ }
377
+ } catch {}
378
+ context.recordTransaction(signature, tx, {
379
+ logs,
380
+ fee: 5000,
381
+ blockTime: Math.floor(Date.now() / 1000),
382
+ preBalances,
383
+ postBalances,
384
+ preTokenBalances,
385
+ postTokenBalances,
386
+ innerInstructions,
387
+ computeUnits,
388
+ returnData,
389
+ accountStates: (() => {
390
+ try {
391
+ const byAddr = new Map<string, { pre?: any; post?: any }>();
392
+ for (const s of preAccountStates)
393
+ byAddr.set(s.address, { pre: s.pre || null });
394
+ for (const s of postAccountStates) {
395
+ const e = byAddr.get(s.address) || {};
396
+ e.post = s.post || null;
397
+ byAddr.set(s.address, e);
398
+ }
399
+ return Array.from(byAddr.entries()).map(([address, v]) => ({
400
+ address,
401
+ pre: v.pre || null,
402
+ post: v.post || null,
403
+ }));
404
+ } catch {
405
+ return [] as Array<{
406
+ address: string;
407
+ pre?: unknown;
408
+ post?: unknown;
409
+ }>;
410
+ }
411
+ })(),
412
+ });
222
413
 
223
414
  return context.createSuccessResponse(id, signature);
224
- } catch (error: any) {
415
+ } catch (error: unknown) {
416
+ const message = error instanceof Error ? error.message : String(error);
225
417
  return context.createErrorResponse(
226
418
  id,
227
419
  -32003,
228
420
  "Transaction failed",
229
- error.message,
421
+ message,
230
422
  );
231
423
  }
232
424
  };
@@ -45,12 +45,13 @@ export const simulateTransaction: RpcMethodHandler = (id, params, context) => {
45
45
  : null,
46
46
  },
47
47
  });
48
- } catch (error: any) {
48
+ } catch (error: unknown) {
49
+ const message = error instanceof Error ? error.message : String(error);
49
50
  return context.createErrorResponse(
50
51
  id,
51
52
  -32003,
52
53
  "Simulation failed",
53
- error.message,
54
+ message,
54
55
  );
55
56
  }
56
57
  };