solforge 0.2.6 → 0.2.8

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.
@@ -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
+ });
@@ -25,22 +25,51 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
25
25
  : Array.isArray(msg.accountKeys)
26
26
  ? msg.accountKeys
27
27
  : [];
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
- const preBalances = staticKeys.map((pk) => {
38
- try {
39
- return Number(context.svm.getBalance(pk));
40
- } catch {
41
- return 0;
42
- }
43
- });
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 {}
44
73
 
45
74
  // Collect SPL token accounts from instructions for pre/post token balance snapshots
46
75
  const msgAny = msg as unknown as {
@@ -57,14 +86,15 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
57
86
  TOKEN_2022_PROGRAM_ID.toBase58(),
58
87
  ]);
59
88
  const tokenAccountSet = new Set<string>();
89
+ // 1) Collect from compiled ixs (best-effort)
60
90
  for (const ci of compiled) {
61
91
  try {
62
- const pid = staticKeys[ci.programIdIndex]?.toBase58();
92
+ const pid = staticKeys[(ci as any).programIdIndex]?.toBase58();
63
93
  if (!pid || !tokenProgramIds.has(pid)) continue;
64
- const accIdxs: number[] = Array.isArray(ci.accountKeyIndexes)
65
- ? ci.accountKeyIndexes
66
- : Array.isArray(ci.accounts)
67
- ? ci.accounts
94
+ const accIdxs: number[] = Array.isArray((ci as any).accountKeyIndexes)
95
+ ? (ci as any).accountKeyIndexes
96
+ : Array.isArray((ci as any).accounts)
97
+ ? (ci as any).accounts
68
98
  : [];
69
99
  for (const ix of accIdxs) {
70
100
  const addr = staticKeys[ix]?.toBase58();
@@ -72,6 +102,17 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
72
102
  }
73
103
  } catch {}
74
104
  }
105
+ // 2) Also collect from all static keys that are SPL token accounts pre-send
106
+ for (const pk of staticKeys) {
107
+ try {
108
+ const acc = context.svm.getAccount(pk);
109
+ if (!acc) continue;
110
+ const ownerStr = new PublicKey(acc.owner).toBase58();
111
+ if (tokenProgramIds.has(ownerStr) && (acc.data?.length ?? 0) >= ACCOUNT_SIZE) {
112
+ tokenAccountSet.add(pk.toBase58());
113
+ }
114
+ } catch {}
115
+ }
75
116
  // Pre token balances
76
117
  const preTokenBalances: unknown[] = [];
77
118
  const ataToInfo = new Map<
@@ -130,7 +171,7 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
130
171
  } catch {}
131
172
  }
132
173
 
133
- const result = context.svm.sendTransaction(tx);
174
+ const result = context.svm.sendTransaction(tx);
134
175
 
135
176
  try {
136
177
  const rawErr = (result as { err?: unknown }).err;
@@ -146,19 +187,47 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
146
187
  }
147
188
  } catch {}
148
189
 
149
- const signature = tx.signatures[0]
150
- ? context.encodeBase58(tx.signatures[0])
151
- : context.encodeBase58(new Uint8Array(64).fill(0));
152
- context.notifySignature(signature);
153
- // Snapshot post balances and capture logs for rich view
154
- const postBalances = staticKeys.map((pk) => {
155
- try {
156
- return Number(context.svm.getBalance(pk));
157
- } catch {
158
- return 0;
159
- }
160
- });
161
- // Post token balances
190
+ const signature = tx.signatures[0]
191
+ ? context.encodeBase58(tx.signatures[0])
192
+ : context.encodeBase58(new Uint8Array(64).fill(0));
193
+ context.notifySignature(signature);
194
+ // Snapshot post balances and capture logs for rich view
195
+ const postBalances = staticKeys.map((pk) => {
196
+ try {
197
+ return Number(context.svm.getBalance(pk));
198
+ } catch {
199
+ return 0;
200
+ }
201
+ });
202
+ const postAccountStates = staticKeys.map((pk) => {
203
+ try {
204
+ const addr = pk.toBase58();
205
+ const acc = context.svm.getAccount(pk);
206
+ if (!acc) return { address: addr, post: null } as const;
207
+ return {
208
+ address: addr,
209
+ post: {
210
+ lamports: Number(acc.lamports || 0n),
211
+ ownerProgram: new PublicKey(acc.owner).toBase58(),
212
+ executable: !!acc.executable,
213
+ rentEpoch: Number(acc.rentEpoch || 0),
214
+ dataLen: acc.data?.length ?? 0,
215
+ dataBase64: undefined,
216
+ lastSlot: Number(context.slot),
217
+ },
218
+ } as const;
219
+ } catch {
220
+ return { address: pk.toBase58(), post: null } as const;
221
+ }
222
+ });
223
+ try {
224
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
225
+ console.debug(
226
+ `[tx-capture] post snapshots: keys=${staticKeys.length} captured=${postAccountStates.length}`,
227
+ );
228
+ }
229
+ } catch {}
230
+ // Post token balances (scan token accounts among static keys)
162
231
  const postTokenBalances: unknown[] = [];
163
232
  for (const addr of tokenAccountSet) {
164
233
  try {
@@ -209,28 +278,150 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
209
278
  }
210
279
  } catch {}
211
280
  }
212
- let logs: string[] = [];
213
- try {
214
- const sr = result as {
215
- logs?: () => string[];
216
- meta?: () => { logs?: () => string[] } | undefined;
217
- };
218
- if (typeof sr?.logs === "function") logs = sr.logs();
219
- else if (typeof sr?.meta === "function") {
220
- const m = sr.meta();
221
- const lg = m?.logs;
222
- if (typeof lg === "function") logs = lg();
223
- }
224
- } catch {}
225
- context.recordTransaction(signature, tx, {
226
- logs,
227
- fee: 5000,
228
- blockTime: Math.floor(Date.now() / 1000),
229
- preBalances,
230
- postBalances,
231
- preTokenBalances,
232
- postTokenBalances,
233
- });
281
+ let logs: string[] = [];
282
+ let innerInstructions: unknown[] = [];
283
+ let computeUnits: number | null = null;
284
+ let returnData: { programId: string; dataBase64: string } | null = null;
285
+ try {
286
+ const DBG = process.env.DEBUG_TX_CAPTURE === "1";
287
+ const r: any = result as any;
288
+ // Logs can be on TransactionMetadata or in meta() for failures
289
+ try {
290
+ if (typeof r?.logs === "function") logs = r.logs();
291
+ } catch {}
292
+ let metaObj: any | undefined;
293
+ // Success shape: methods on result
294
+ if (
295
+ typeof r?.innerInstructions === "function" ||
296
+ typeof r?.computeUnitsConsumed === "function" ||
297
+ typeof r?.returnData === "function"
298
+ ) {
299
+ metaObj = r;
300
+ }
301
+ // Failed shape: meta() returns TransactionMetadata
302
+ if (!metaObj && typeof r?.meta === "function") {
303
+ try {
304
+ metaObj = r.meta();
305
+ if (!logs.length && typeof metaObj?.logs === "function") {
306
+ logs = metaObj.logs();
307
+ }
308
+ } catch (e) {
309
+ if (DBG)
310
+ console.debug("[tx-capture] meta() threw while extracting:", e);
311
+ }
312
+ }
313
+ // Extract richer metadata from whichever object exposes it
314
+ if (metaObj) {
315
+ try {
316
+ const inner = metaObj.innerInstructions?.();
317
+ if (Array.isArray(inner)) {
318
+ innerInstructions = inner.map((group: any, index: number) => {
319
+ const instructions = Array.isArray(group)
320
+ ? group
321
+ .map((ii: any) => {
322
+ try {
323
+ const inst = ii.instruction?.();
324
+ const accIdxs: number[] = Array.from(
325
+ inst?.accounts?.() || [],
326
+ );
327
+ const dataBytes: Uint8Array =
328
+ inst?.data?.() || new Uint8Array();
329
+ return {
330
+ programIdIndex: Number(
331
+ inst?.programIdIndex?.() ?? 0,
332
+ ),
333
+ accounts: accIdxs,
334
+ data: context.encodeBase58(dataBytes),
335
+ stackHeight: Number(ii.stackHeight?.() ?? 0),
336
+ };
337
+ } catch {
338
+ return null;
339
+ }
340
+ })
341
+ .filter(Boolean)
342
+ : [];
343
+ return { index, instructions };
344
+ });
345
+ }
346
+ } catch (e) {
347
+ if (DBG)
348
+ console.debug(
349
+ "[tx-capture] innerInstructions extraction failed:",
350
+ e,
351
+ );
352
+ }
353
+ try {
354
+ const cu = metaObj.computeUnitsConsumed?.();
355
+ if (typeof cu === "bigint") computeUnits = Number(cu);
356
+ } catch (e) {
357
+ if (DBG)
358
+ console.debug(
359
+ "[tx-capture] computeUnitsConsumed extraction failed:",
360
+ e,
361
+ );
362
+ }
363
+ try {
364
+ const rd = metaObj.returnData?.();
365
+ if (rd) {
366
+ const pid = new PublicKey(rd.programId()).toBase58();
367
+ const dataB64 = Buffer.from(rd.data()).toString("base64");
368
+ returnData = { programId: pid, dataBase64: dataB64 };
369
+ }
370
+ } catch (e) {
371
+ if (DBG)
372
+ console.debug(
373
+ "[tx-capture] returnData extraction failed:",
374
+ e,
375
+ );
376
+ }
377
+ } else if (DBG) {
378
+ console.debug(
379
+ "[tx-capture] no metadata object found on result shape",
380
+ );
381
+ }
382
+ } catch {}
383
+ try {
384
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
385
+ console.debug(
386
+ `[tx-capture] sendTransaction meta: logs=${logs.length} innerGroups=${Array.isArray(innerInstructions) ? innerInstructions.length : 0} computeUnits=${computeUnits} returnData=${returnData ? "yes" : "no"}`,
387
+ );
388
+ }
389
+ } catch {}
390
+ context.recordTransaction(signature, tx, {
391
+ logs,
392
+ fee: 5000,
393
+ blockTime: Math.floor(Date.now() / 1000),
394
+ preBalances,
395
+ postBalances,
396
+ preTokenBalances,
397
+ postTokenBalances,
398
+ innerInstructions,
399
+ computeUnits,
400
+ returnData,
401
+ accountStates: (() => {
402
+ try {
403
+ const byAddr = new Map<string, { pre?: any; post?: any }>();
404
+ for (const s of preAccountStates)
405
+ byAddr.set(s.address, { pre: s.pre || null });
406
+ for (const s of postAccountStates) {
407
+ const e = byAddr.get(s.address) || {};
408
+ e.post = s.post || null;
409
+ byAddr.set(s.address, e);
410
+ }
411
+ return Array.from(byAddr.entries()).map(([address, v]) => ({
412
+ address,
413
+ pre: v.pre || null,
414
+ post: v.post || null,
415
+ }));
416
+ } catch {
417
+ return [] as Array<{
418
+ address: string;
419
+ pre?: unknown;
420
+ post?: unknown;
421
+ }>;
422
+ }
423
+ })(),
424
+ });
234
425
 
235
426
  return context.createSuccessResponse(id, signature);
236
427
  } catch (error: unknown) {
@@ -150,23 +150,36 @@ export class LiteSVMRpcServer {
150
150
  } catch {}
151
151
  },
152
152
  listPrograms: () => Array.from(this.knownPrograms),
153
- recordTransaction: (signature, tx, meta) => {
154
- this.txRecords.set(signature, {
155
- tx,
156
- logs: meta?.logs || [],
157
- err: meta?.err ?? null,
158
- fee: meta?.fee ?? 5000,
159
- slot: Number(this.slot),
160
- blockTime: meta?.blockTime,
161
- preBalances: meta?.preBalances,
162
- postBalances: meta?.postBalances,
163
- preTokenBalances: (
164
- meta as { preTokenBalances?: unknown[] } | undefined
165
- )?.preTokenBalances,
166
- postTokenBalances: (
167
- meta as { postTokenBalances?: unknown[] } | undefined
168
- )?.postTokenBalances,
169
- });
153
+ recordTransaction: (signature, tx, meta) => {
154
+ this.txRecords.set(signature, {
155
+ tx,
156
+ logs: meta?.logs || [],
157
+ err: meta?.err ?? null,
158
+ fee: meta?.fee ?? 5000,
159
+ slot: Number(this.slot),
160
+ blockTime: meta?.blockTime,
161
+ preBalances: meta?.preBalances,
162
+ postBalances: meta?.postBalances,
163
+ preTokenBalances: (
164
+ meta as { preTokenBalances?: unknown[] } | undefined
165
+ )?.preTokenBalances,
166
+ postTokenBalances: (
167
+ meta as { postTokenBalances?: unknown[] } | undefined
168
+ )?.postTokenBalances,
169
+ innerInstructions: meta?.innerInstructions || [],
170
+ computeUnits:
171
+ meta?.computeUnits == null
172
+ ? null
173
+ : Number(meta.computeUnits),
174
+ returnData: meta?.returnData ?? null,
175
+ });
176
+ try {
177
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
178
+ console.debug(
179
+ `[tx-capture] recordTransaction: sig=${signature} slot=${this.slot} logs=${meta?.logs?.length || 0} inner=${Array.isArray(meta?.innerInstructions) ? meta?.innerInstructions?.length : 0} cu=${meta?.computeUnits ?? null} returnData=${meta?.returnData ? "yes" : "no"}`,
180
+ );
181
+ }
182
+ } catch {}
170
183
 
171
184
  // Persist to SQLite for durability and history queries
172
185
  try {
@@ -227,58 +240,82 @@ export class LiteSVMRpcServer {
227
240
  : "legacy"
228
241
  : 0;
229
242
  const rawBase64 = Buffer.from(tx.serialize()).toString("base64");
230
- this.store
231
- .insertTransactionBundle({
232
- signature,
233
- slot: Number(this.slot),
234
- blockTime: meta?.blockTime,
235
- version,
236
- fee: Number(meta?.fee ?? 5000),
237
- err: meta?.err ?? null,
238
- rawBase64,
239
- preBalances: Array.isArray(meta?.preBalances)
240
- ? (meta?.preBalances as number[])
241
- : [],
242
- postBalances: Array.isArray(meta?.postBalances)
243
- ? (meta?.postBalances as number[])
244
- : [],
245
- logs: Array.isArray(meta?.logs) ? (meta?.logs as string[]) : [],
246
- preTokenBalances: (() => {
247
- const arr = (
248
- meta as { preTokenBalances?: unknown[] } | undefined
249
- )?.preTokenBalances;
250
- return Array.isArray(arr) ? arr : [];
251
- })(),
252
- postTokenBalances: (() => {
253
- const arr = (
254
- meta as { postTokenBalances?: unknown[] } | undefined
255
- )?.postTokenBalances;
256
- return Array.isArray(arr) ? arr : [];
257
- })(),
258
- accounts,
259
- })
260
- .catch(() => {});
243
+ this.store
244
+ .insertTransactionBundle({
245
+ signature,
246
+ slot: Number(this.slot),
247
+ blockTime: meta?.blockTime,
248
+ version,
249
+ fee: Number(meta?.fee ?? 5000),
250
+ err: meta?.err ?? null,
251
+ rawBase64,
252
+ preBalances: Array.isArray(meta?.preBalances)
253
+ ? (meta?.preBalances as number[])
254
+ : [],
255
+ postBalances: Array.isArray(meta?.postBalances)
256
+ ? (meta?.postBalances as number[])
257
+ : [],
258
+ logs: Array.isArray(meta?.logs) ? (meta?.logs as string[]) : [],
259
+ preTokenBalances: (() => {
260
+ const arr = (
261
+ meta as { preTokenBalances?: unknown[] } | undefined
262
+ )?.preTokenBalances;
263
+ return Array.isArray(arr) ? arr : [];
264
+ })(),
265
+ postTokenBalances: (() => {
266
+ const arr = (
267
+ meta as { postTokenBalances?: unknown[] } | undefined
268
+ )?.postTokenBalances;
269
+ return Array.isArray(arr) ? arr : [];
270
+ })(),
271
+ innerInstructions: Array.isArray(meta?.innerInstructions)
272
+ ? meta?.innerInstructions
273
+ : [],
274
+ computeUnits:
275
+ meta?.computeUnits == null
276
+ ? null
277
+ : Number(meta.computeUnits),
278
+ returnData: meta?.returnData ?? null,
279
+ accounts,
280
+ accountStates: Array.isArray(meta?.accountStates)
281
+ ? meta?.accountStates
282
+ : [],
283
+ })
284
+ .catch(() => {});
261
285
 
262
286
  // Upsert account snapshots for static keys
263
- const snapshots = keys
264
- .map((addr) => {
265
- try {
266
- const acc = this.svm.getAccount(new PublicKey(addr));
267
- if (!acc) return null;
268
- return {
269
- address: addr,
270
- lamports: Number(acc.lamports || 0n),
271
- ownerProgram: new PublicKey(acc.owner).toBase58(),
272
- executable: !!acc.executable,
273
- rentEpoch: Number(acc.rentEpoch || 0),
274
- dataLen: acc.data?.length ?? 0,
275
- dataBase64: undefined,
276
- lastSlot: Number(this.slot),
277
- };
278
- } catch {
279
- return null;
280
- }
281
- })
287
+ const snapshots = keys
288
+ .map((addr) => {
289
+ try {
290
+ const acc = this.svm.getAccount(new PublicKey(addr));
291
+ if (!acc) return null;
292
+ const ownerStr = new PublicKey(acc.owner).toBase58();
293
+ let dataBase64: string | undefined;
294
+ // Store raw data for SPL Token accounts to reflect balance changes
295
+ try {
296
+ if (
297
+ ownerStr === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ||
298
+ ownerStr === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
299
+ ) {
300
+ if (acc.data && acc.data.length > 0) {
301
+ dataBase64 = Buffer.from(acc.data).toString("base64");
302
+ }
303
+ }
304
+ } catch {}
305
+ return {
306
+ address: addr,
307
+ lamports: Number(acc.lamports || 0n),
308
+ ownerProgram: ownerStr,
309
+ executable: !!acc.executable,
310
+ rentEpoch: Number(acc.rentEpoch || 0),
311
+ dataLen: acc.data?.length ?? 0,
312
+ dataBase64,
313
+ lastSlot: Number(this.slot),
314
+ };
315
+ } catch {
316
+ return null;
317
+ }
318
+ })
282
319
  .filter(Boolean) as import("../src/db/tx-store").AccountSnapshot[];
283
320
  if (snapshots.length > 0)
284
321
  this.store.upsertAccounts(snapshots).catch(() => {});