solforge 0.2.7 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solforge",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "private": false,
@@ -0,0 +1,6 @@
1
+ import { decodeBase58 } from "../server/lib/base58";
2
+
3
+ const s = process.argv[2] || "84eT";
4
+ const bytes = decodeBase58(s);
5
+ console.log(bytes);
6
+ console.log(Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(" "));
@@ -8,6 +8,9 @@ import {
8
8
  TransactionInstruction,
9
9
  VoteInstruction,
10
10
  } from "@solana/web3.js";
11
+ import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
12
+ import { tryParseSplToken } from "./parsers/spl-token";
13
+ import { tryParseAta } from "./parsers/spl-associated-token-account";
11
14
  import { decodeBase58 as _decodeBase58 } from "./base58";
12
15
 
13
16
  export type ParsedInstruction =
@@ -43,18 +46,38 @@ export function parseInstruction(
43
46
  const pid = new PublicKey(programId);
44
47
  const ix = makeIx(programId, accountKeys, accounts, dataBase58);
45
48
 
49
+ // SPL Token
50
+ if (pid.equals(TOKEN_PROGRAM_ID)) {
51
+ const parsed = tryParseSplToken(ix, programId, accountKeys, dataBase58);
52
+ if (parsed) return parsed;
53
+ }
54
+
55
+ // Associated Token Account
56
+ if (pid.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) {
57
+ const parsed = tryParseAta(ix, programId);
58
+ if (parsed) return parsed;
59
+ }
60
+
46
61
  // System Program
47
62
  if (pid.equals(SystemProgram.programId)) {
48
63
  const t = SystemInstruction.decodeInstructionType(ix);
49
64
  switch (t) {
50
65
  case "Create": {
51
66
  const p = SystemInstruction.decodeCreateAccount(ix);
67
+ const from = p.fromPubkey.toBase58();
68
+ const newAcc = p.newAccountPubkey.toBase58();
69
+ const owner = p.programId.toBase58();
52
70
  return ok("system", programId, "createAccount", {
53
- fromPubkey: p.fromPubkey.toBase58(),
54
- newAccountPubkey: p.newAccountPubkey.toBase58(),
71
+ // Explorer-compatible field names
72
+ source: from,
73
+ newAccount: newAcc,
74
+ owner,
55
75
  lamports: Number(p.lamports),
56
76
  space: Number(p.space),
57
- programId: p.programId.toBase58(),
77
+ // Keep legacy aliases too
78
+ fromPubkey: from,
79
+ newAccountPubkey: newAcc,
80
+ programId: owner,
58
81
  });
59
82
  }
60
83
  case "Transfer": {
@@ -239,4 +262,3 @@ export function parseInstruction(
239
262
  };
240
263
  }
241
264
  }
242
-
@@ -0,0 +1,44 @@
1
+ import type { TransactionInstruction } from "@solana/web3.js";
2
+ import { ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
3
+
4
+ // Keep shape compatible with instruction-parser
5
+ export type ParsedInstruction =
6
+ | { program: string; programId: string; parsed: { type: string; info: any } }
7
+ | { programId: string; accounts: string[]; data: string };
8
+
9
+ function ok(programId: string, type: string, info: any): ParsedInstruction {
10
+ return { program: "spl-associated-token-account", programId, parsed: { type, info } };
11
+ }
12
+
13
+ function asBase58(ix: TransactionInstruction, idx: number): string | undefined {
14
+ try {
15
+ return ix.keys[idx]?.pubkey?.toBase58();
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ export function tryParseAta(
22
+ ix: TransactionInstruction,
23
+ programIdStr: string,
24
+ ): ParsedInstruction | null {
25
+ try {
26
+ if (!ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) return null;
27
+ const dataLen = ix.data?.length ?? 0;
28
+ // Both create (empty) and createIdempotent ([1]) map to type "create" in explorers
29
+ const type = "create";
30
+ // Expected keys: [payer, associatedToken, owner, mint, systemProgram, tokenProgram]
31
+ const info = {
32
+ source: asBase58(ix, 0),
33
+ account: asBase58(ix, 1),
34
+ wallet: asBase58(ix, 2),
35
+ mint: asBase58(ix, 3),
36
+ systemProgram: asBase58(ix, 4),
37
+ tokenProgram: asBase58(ix, 5),
38
+ };
39
+ return ok(programIdStr, type, info);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
@@ -0,0 +1,172 @@
1
+ import type { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
+ import {
3
+ TOKEN_PROGRAM_ID,
4
+ decodeInstruction as splDecodeInstruction,
5
+ decodeMintToCheckedInstruction,
6
+ decodeTransferInstruction,
7
+ decodeTransferCheckedInstruction,
8
+ decodeInitializeAccount3Instruction,
9
+ decodeInitializeImmutableOwnerInstruction,
10
+ } from "@solana/spl-token";
11
+ import { u8 } from "@solana/buffer-layout";
12
+
13
+ // Keep shape compatible with instruction-parser
14
+ export type ParsedInstruction =
15
+ | { program: string; programId: string; parsed: { type: string; info: any } }
16
+ | { programId: string; accounts: string[]; data: string };
17
+
18
+ function ok(programId: string, type: string, info: any): ParsedInstruction {
19
+ return { program: "spl-token", programId, parsed: { type, info } };
20
+ }
21
+
22
+ function asBase58(pk: PublicKey | undefined): string | undefined {
23
+ try {
24
+ return pk ? pk.toBase58() : undefined;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ export function tryParseSplToken(
31
+ ix: TransactionInstruction,
32
+ programIdStr: string,
33
+ accountKeys: string[],
34
+ dataBase58: string,
35
+ ): ParsedInstruction | null {
36
+ try {
37
+ if (!ix.programId.equals(TOKEN_PROGRAM_ID)) return null;
38
+
39
+ // Probe with generic decoder (ignore result), then try specific decoders
40
+ try {
41
+ splDecodeInstruction(ix);
42
+ } catch {}
43
+
44
+ // MintToChecked
45
+ try {
46
+ const m = decodeMintToCheckedInstruction(ix);
47
+ const amount = m.data.amount;
48
+ const decimals = m.data.decimals;
49
+ const amtStr = typeof amount === "bigint" ? amount.toString() : String(amount);
50
+ const base = BigInt(10) ** BigInt(decimals);
51
+ const whole = BigInt(amtStr) / base;
52
+ const frac = BigInt(amtStr) % base;
53
+ const fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
54
+ const uiStr = fracStr.length ? `${whole}.${fracStr}` : `${whole}`;
55
+ return ok(programIdStr, "mintToChecked", {
56
+ account: asBase58(m.keys.destination.pubkey),
57
+ mint: asBase58(m.keys.mint.pubkey),
58
+ mintAuthority: asBase58(m.keys.authority.pubkey),
59
+ tokenAmount: {
60
+ amount: amtStr,
61
+ decimals,
62
+ uiAmount: Number(uiStr),
63
+ uiAmountString: uiStr,
64
+ },
65
+ });
66
+ } catch {}
67
+
68
+ // Transfer / TransferChecked
69
+ try {
70
+ const t = decodeTransferInstruction(ix);
71
+ const amt = t.data.amount;
72
+ return ok(programIdStr, "transfer", {
73
+ amount: (typeof amt === "bigint" ? amt.toString() : String(amt)),
74
+ source: asBase58(t.keys.source.pubkey),
75
+ destination: asBase58(t.keys.destination.pubkey),
76
+ authority: asBase58(t.keys.owner.pubkey),
77
+ });
78
+ } catch {}
79
+
80
+ try {
81
+ const t = decodeTransferCheckedInstruction(ix);
82
+ const amt = t.data.amount;
83
+ const decimals = t.data.decimals;
84
+ return ok(programIdStr, "transferChecked", {
85
+ tokenAmount: {
86
+ amount: (typeof amt === "bigint" ? amt.toString() : String(amt)),
87
+ decimals,
88
+ },
89
+ source: asBase58(t.keys.source.pubkey),
90
+ destination: asBase58(t.keys.destination.pubkey),
91
+ authority: asBase58(t.keys.owner.pubkey),
92
+ mint: asBase58(t.keys.mint.pubkey),
93
+ });
94
+ } catch {}
95
+
96
+ // InitializeAccount3
97
+ try {
98
+ const a = decodeInitializeAccount3Instruction(ix);
99
+ return ok(programIdStr, "initializeAccount3", {
100
+ account: asBase58(a.keys.account.pubkey),
101
+ mint: asBase58(a.keys.mint.pubkey),
102
+ owner: asBase58(a.data.owner),
103
+ });
104
+ } catch {}
105
+
106
+ // InitializeImmutableOwner
107
+ try {
108
+ const im = decodeInitializeImmutableOwnerInstruction(ix, TOKEN_PROGRAM_ID);
109
+ return ok(programIdStr, "initializeImmutableOwner", {
110
+ account: asBase58(im.keys.account.pubkey),
111
+ });
112
+ } catch {}
113
+
114
+ // GetAccountDataSize: decode extension types (u16 little-endian sequence)
115
+ try {
116
+ const bytes = Buffer.from(bs58decode(dataBase58));
117
+ const t = u8().decode(bytes);
118
+ // 21 corresponds to TokenInstruction.GetAccountDataSize
119
+ if (t === 21) {
120
+ const mint = asBase58(ix.keys[0]?.pubkey);
121
+ const extCodes: number[] = [];
122
+ for (let i = 1; i + 1 < bytes.length; i += 2) {
123
+ const code = bytes[i] | (bytes[i + 1] << 8);
124
+ extCodes.push(code);
125
+ }
126
+ const extMap: Record<number, string> = {
127
+ 7: "immutableOwner",
128
+ 8: "memoTransfer",
129
+ 9: "nonTransferable",
130
+ 12: "permanentDelegate",
131
+ 14: "transferHook",
132
+ 15: "transferHookAccount",
133
+ 18: "metadataPointer",
134
+ 19: "tokenMetadata",
135
+ 20: "groupPointer",
136
+ 21: "tokenGroup",
137
+ 22: "groupMemberPointer",
138
+ 23: "tokenGroupMember",
139
+ 25: "scaledUiAmountConfig",
140
+ 26: "pausableConfig",
141
+ 27: "pausableAccount",
142
+ };
143
+ const extensionTypes = extCodes.map((c) => extMap[c] || String(c));
144
+ return ok(programIdStr, "getAccountDataSize", { mint, extensionTypes });
145
+ }
146
+ } catch {}
147
+
148
+ // Unknown SPL token instruction
149
+ return null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ // Local base58 decode to avoid importing from sibling file
156
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
157
+ const BASE = BigInt(ALPHABET.length);
158
+ function bs58decode(str: string): Uint8Array {
159
+ let num = 0n;
160
+ for (const char of str) {
161
+ const index = ALPHABET.indexOf(char);
162
+ if (index === -1) throw new Error("Invalid base58 character");
163
+ num = num * BASE + BigInt(index);
164
+ }
165
+ const bytes: number[] = [];
166
+ while (num > 0n) {
167
+ bytes.unshift(Number(num % 256n));
168
+ num = num / 256n;
169
+ }
170
+ for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.unshift(0);
171
+ return new Uint8Array(bytes.length > 0 ? bytes : [0]);
172
+ }
@@ -86,14 +86,15 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
86
86
  TOKEN_2022_PROGRAM_ID.toBase58(),
87
87
  ]);
88
88
  const tokenAccountSet = new Set<string>();
89
+ // 1) Collect from compiled ixs (best-effort)
89
90
  for (const ci of compiled) {
90
91
  try {
91
- const pid = staticKeys[ci.programIdIndex]?.toBase58();
92
+ const pid = staticKeys[(ci as any).programIdIndex]?.toBase58();
92
93
  if (!pid || !tokenProgramIds.has(pid)) continue;
93
- const accIdxs: number[] = Array.isArray(ci.accountKeyIndexes)
94
- ? ci.accountKeyIndexes
95
- : Array.isArray(ci.accounts)
96
- ? 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
97
98
  : [];
98
99
  for (const ix of accIdxs) {
99
100
  const addr = staticKeys[ix]?.toBase58();
@@ -101,6 +102,17 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
101
102
  }
102
103
  } catch {}
103
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
+ }
104
116
  // Pre token balances
105
117
  const preTokenBalances: unknown[] = [];
106
118
  const ataToInfo = new Map<
@@ -215,7 +227,7 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
215
227
  );
216
228
  }
217
229
  } catch {}
218
- // Post token balances
230
+ // Post token balances (scan token accounts among static keys)
219
231
  const postTokenBalances: unknown[] = [];
220
232
  for (const addr of tokenAccountSet) {
221
233
  try {
@@ -284,25 +284,38 @@ export class LiteSVMRpcServer {
284
284
  .catch(() => {});
285
285
 
286
286
  // Upsert account snapshots for static keys
287
- const snapshots = keys
288
- .map((addr) => {
289
- try {
290
- const acc = this.svm.getAccount(new PublicKey(addr));
291
- if (!acc) return null;
292
- return {
293
- address: addr,
294
- lamports: Number(acc.lamports || 0n),
295
- ownerProgram: new PublicKey(acc.owner).toBase58(),
296
- executable: !!acc.executable,
297
- rentEpoch: Number(acc.rentEpoch || 0),
298
- dataLen: acc.data?.length ?? 0,
299
- dataBase64: undefined,
300
- lastSlot: Number(this.slot),
301
- };
302
- } catch {
303
- return null;
304
- }
305
- })
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
+ })
306
319
  .filter(Boolean) as import("../src/db/tx-store").AccountSnapshot[];
307
320
  if (snapshots.length > 0)
308
321
  this.store.upsertAccounts(snapshots).catch(() => {});