solforge 0.2.6 → 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.
@@ -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 {
@@ -130,7 +159,7 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
130
159
  } catch {}
131
160
  }
132
161
 
133
- const result = context.svm.sendTransaction(tx);
162
+ const result = context.svm.sendTransaction(tx);
134
163
 
135
164
  try {
136
165
  const rawErr = (result as { err?: unknown }).err;
@@ -146,18 +175,46 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
146
175
  }
147
176
  } catch {}
148
177
 
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
- });
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 {}
161
218
  // Post token balances
162
219
  const postTokenBalances: unknown[] = [];
163
220
  for (const addr of tokenAccountSet) {
@@ -209,28 +266,150 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
209
266
  }
210
267
  } catch {}
211
268
  }
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
- });
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
+ });
234
413
 
235
414
  return context.createSuccessResponse(id, signature);
236
415
  } 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,37 +240,48 @@ 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
287
  const snapshots = keys
package/server/types.ts CHANGED
@@ -44,30 +44,62 @@ export interface RpcMethodContext {
44
44
  listMints?: () => string[];
45
45
  registerProgram?: (program: PublicKey | string) => void;
46
46
  listPrograms?: () => string[];
47
- recordTransaction: (
48
- signature: string,
49
- tx: VersionedTransaction,
50
- meta?: {
51
- logs?: string[];
52
- err?: unknown;
53
- fee?: number;
54
- blockTime?: number;
55
- preBalances?: number[];
56
- postBalances?: number[];
57
- },
58
- ) => void;
59
- getRecordedTransaction: (signature: string) =>
60
- | {
61
- tx: VersionedTransaction;
62
- logs: string[];
63
- err: unknown;
64
- fee: number;
65
- slot: number;
66
- blockTime?: number;
67
- preBalances?: number[];
68
- postBalances?: number[];
69
- }
70
- | undefined;
47
+ recordTransaction: (
48
+ signature: string,
49
+ tx: VersionedTransaction,
50
+ meta?: {
51
+ logs?: string[];
52
+ err?: unknown;
53
+ fee?: number;
54
+ blockTime?: number;
55
+ preBalances?: number[];
56
+ postBalances?: number[];
57
+ preTokenBalances?: unknown[];
58
+ postTokenBalances?: unknown[];
59
+ innerInstructions?: unknown[];
60
+ computeUnits?: number | bigint | null;
61
+ returnData?: { programId: string; dataBase64: string } | null;
62
+ // Optional rich per-account snapshots captured around execution
63
+ accountStates?: Array<{
64
+ address: string;
65
+ pre?: {
66
+ lamports?: number;
67
+ ownerProgram?: string;
68
+ executable?: boolean;
69
+ rentEpoch?: number;
70
+ dataLen?: number;
71
+ dataBase64?: string | null;
72
+ lastSlot?: number;
73
+ } | null;
74
+ post?: {
75
+ lamports?: number;
76
+ ownerProgram?: string;
77
+ executable?: boolean;
78
+ rentEpoch?: number;
79
+ dataLen?: number;
80
+ dataBase64?: string | null;
81
+ lastSlot?: number;
82
+ } | null;
83
+ }>;
84
+ },
85
+ ) => void;
86
+ getRecordedTransaction: (signature: string) =>
87
+ | {
88
+ tx: VersionedTransaction;
89
+ logs: string[];
90
+ err: unknown;
91
+ fee: number;
92
+ slot: number;
93
+ blockTime?: number;
94
+ preBalances?: number[];
95
+ postBalances?: number[];
96
+ preTokenBalances?: unknown[];
97
+ postTokenBalances?: unknown[];
98
+ innerInstructions?: unknown[];
99
+ computeUnits?: number | null;
100
+ returnData?: { programId: string; dataBase64: string } | null;
101
+ }
102
+ | undefined;
71
103
  }
72
104
 
73
105
  export type RpcMethodHandler = (
@@ -3,3 +3,4 @@ export * from "./address-signatures";
3
3
  export * from "./meta-kv";
4
4
  export * from "./transactions";
5
5
  export * from "./tx-accounts";
6
+ export * from "./tx-account-states";
@@ -1,28 +1,35 @@
1
1
  import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
2
 
3
3
  export const transactions = sqliteTable(
4
- "transactions",
5
- {
6
- signature: text("signature").primaryKey(),
7
- slot: integer("slot").notNull(),
8
- blockTime: integer("block_time"),
9
- version: text("version").notNull(), // 0 | "legacy"
10
- errJson: text("err_json"),
11
- fee: integer("fee").notNull(),
12
- rawBase64: text("raw_base64").notNull(),
13
- preBalancesJson: text("pre_balances_json").notNull(),
14
- postBalancesJson: text("post_balances_json").notNull(),
15
- logsJson: text("logs_json").notNull(),
16
- preTokenBalancesJson: text("pre_token_balances_json")
17
- .default("[]")
18
- .notNull(),
19
- postTokenBalancesJson: text("post_token_balances_json")
20
- .default("[]")
21
- .notNull(),
22
- },
23
- (t) => ({
24
- slotIdx: index("idx_transactions_slot").on(t.slot),
25
- }),
4
+ "transactions",
5
+ {
6
+ signature: text("signature").primaryKey(),
7
+ slot: integer("slot").notNull(),
8
+ blockTime: integer("block_time"),
9
+ version: text("version").notNull(), // 0 | "legacy"
10
+ errJson: text("err_json"),
11
+ fee: integer("fee").notNull(),
12
+ rawBase64: text("raw_base64").notNull(),
13
+ preBalancesJson: text("pre_balances_json").notNull(),
14
+ postBalancesJson: text("post_balances_json").notNull(),
15
+ logsJson: text("logs_json").notNull(),
16
+ preTokenBalancesJson: text("pre_token_balances_json")
17
+ .default("[]")
18
+ .notNull(),
19
+ postTokenBalancesJson: text("post_token_balances_json")
20
+ .default("[]")
21
+ .notNull(),
22
+ // Additional rich metadata captured after execution
23
+ innerInstructionsJson: text("inner_instructions_json")
24
+ .default("[]")
25
+ .notNull(),
26
+ computeUnits: integer("compute_units"),
27
+ returnDataProgramId: text("return_data_program_id"),
28
+ returnDataBase64: text("return_data_base64"),
29
+ },
30
+ (t) => ({
31
+ slotIdx: index("idx_transactions_slot").on(t.slot),
32
+ }),
26
33
  );
27
34
 
28
35
  export type TransactionRow = typeof transactions.$inferSelect;