solforge 0.2.8 → 0.2.10

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.
@@ -3,42 +3,48 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
3
3
 
4
4
  // Keep shape compatible with instruction-parser
5
5
  export type ParsedInstruction =
6
- | { program: string; programId: string; parsed: { type: string; info: any } }
7
- | { programId: string; accounts: string[]; data: string };
6
+ | {
7
+ program: string;
8
+ programId: string;
9
+ parsed: { type: string; info: unknown };
10
+ }
11
+ | { programId: string; accounts: string[]; data: string };
8
12
 
9
- function ok(programId: string, type: string, info: any): ParsedInstruction {
10
- return { program: "spl-associated-token-account", programId, parsed: { type, info } };
13
+ function ok(programId: string, type: string, info: unknown): ParsedInstruction {
14
+ return {
15
+ program: "spl-associated-token-account",
16
+ programId,
17
+ parsed: { type, info },
18
+ };
11
19
  }
12
20
 
13
21
  function asBase58(ix: TransactionInstruction, idx: number): string | undefined {
14
- try {
15
- return ix.keys[idx]?.pubkey?.toBase58();
16
- } catch {
17
- return undefined;
18
- }
22
+ try {
23
+ return ix.keys[idx]?.pubkey?.toBase58();
24
+ } catch {
25
+ return undefined;
26
+ }
19
27
  }
20
28
 
21
29
  export function tryParseAta(
22
- ix: TransactionInstruction,
23
- programIdStr: string,
30
+ ix: TransactionInstruction,
31
+ programIdStr: string,
24
32
  ): 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
- }
33
+ try {
34
+ if (!ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) return null;
35
+ // Both create (empty) and createIdempotent ([1]) map to type "create" in explorers
36
+ const type = "create";
37
+ // Expected keys: [payer, associatedToken, owner, mint, systemProgram, tokenProgram]
38
+ const info = {
39
+ source: asBase58(ix, 0),
40
+ account: asBase58(ix, 1),
41
+ wallet: asBase58(ix, 2),
42
+ mint: asBase58(ix, 3),
43
+ systemProgram: asBase58(ix, 4),
44
+ tokenProgram: asBase58(ix, 5),
45
+ };
46
+ return ok(programIdStr, type, info);
47
+ } catch {
48
+ return null;
49
+ }
43
50
  }
44
-
@@ -1,172 +1,340 @@
1
1
  import type { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
2
  import {
3
- TOKEN_PROGRAM_ID,
4
- decodeInstruction as splDecodeInstruction,
5
- decodeMintToCheckedInstruction,
6
- decodeTransferInstruction,
7
- decodeTransferCheckedInstruction,
8
- decodeInitializeAccount3Instruction,
9
- decodeInitializeImmutableOwnerInstruction,
3
+ decodeMintToCheckedInstruction,
4
+ decodeTransferInstruction,
5
+ decodeTransferCheckedInstruction,
6
+ decodeInitializeAccount3Instruction,
7
+ decodeInitializeImmutableOwnerInstruction,
8
+ decodeTransferCheckedInstructionUnchecked,
9
+ decodeTransferInstructionUnchecked,
10
+ decodeInitializeAccount3InstructionUnchecked,
11
+ decodeInitializeImmutableOwnerInstructionUnchecked,
10
12
  } from "@solana/spl-token";
11
13
  import { u8 } from "@solana/buffer-layout";
14
+ import { PublicKey as PK } from "@solana/web3.js";
15
+ import {
16
+ TOKEN_PROGRAM_ID as TOKEN_PROGRAM_V1,
17
+ TOKEN_2022_PROGRAM_ID as TOKEN_PROGRAM_2022,
18
+ getAssociatedTokenAddressSync,
19
+ } from "@solana/spl-token";
12
20
 
13
21
  // Keep shape compatible with instruction-parser
14
22
  export type ParsedInstruction =
15
- | { program: string; programId: string; parsed: { type: string; info: any } }
16
- | { programId: string; accounts: string[]; data: string };
23
+ | {
24
+ program: string;
25
+ programId: string;
26
+ parsed: { type: string; info: unknown };
27
+ }
28
+ | { programId: string; accounts: string[]; data: string };
17
29
 
18
- function ok(programId: string, type: string, info: any): ParsedInstruction {
19
- return { program: "spl-token", programId, parsed: { type, info } };
30
+ function ok(programId: string, type: string, info: unknown): ParsedInstruction {
31
+ // Use a single label for both SPL v1 and Token-2022 for compatibility with UIs
32
+ return { program: "spl-token", programId, parsed: { type, info } };
20
33
  }
21
34
 
22
35
  function asBase58(pk: PublicKey | undefined): string | undefined {
23
- try {
24
- return pk ? pk.toBase58() : undefined;
25
- } catch {
26
- return undefined;
27
- }
36
+ try {
37
+ return pk ? pk.toBase58() : undefined;
38
+ } catch {
39
+ return undefined;
40
+ }
28
41
  }
29
42
 
30
43
  export function tryParseSplToken(
31
- ix: TransactionInstruction,
32
- programIdStr: string,
33
- accountKeys: string[],
34
- dataBase58: string,
44
+ ix: TransactionInstruction,
45
+ programIdStr: string,
46
+ _accountKeys: string[],
47
+ dataBase58: string,
48
+ tokenBalanceHints?: Array<{ mint: string; decimals: number }>,
35
49
  ): ParsedInstruction | null {
36
- try {
37
- if (!ix.programId.equals(TOKEN_PROGRAM_ID)) return null;
50
+ try {
51
+ // Accept both SPL Token and Token-2022 program ids
52
+ // We pass the actual program id to decoders for strict match
53
+ const programPk = new PK(programIdStr);
54
+ // Don't early-return on program id; allow both programs
55
+
56
+ // MintToChecked
57
+ try {
58
+ const m = decodeMintToCheckedInstruction(ix, programPk);
59
+ const amount = m.data.amount;
60
+ const decimals = m.data.decimals;
61
+ const amtStr =
62
+ typeof amount === "bigint" ? amount.toString() : String(amount);
63
+ const base = BigInt(10) ** BigInt(decimals);
64
+ const whole = BigInt(amtStr) / base;
65
+ const frac = BigInt(amtStr) % base;
66
+ const fracStr = frac
67
+ .toString()
68
+ .padStart(decimals, "0")
69
+ .replace(/0+$/, "");
70
+ const uiStr = fracStr.length ? `${whole}.${fracStr}` : `${whole}`;
71
+ return ok(programIdStr, "mintToChecked", {
72
+ account: asBase58(m.keys.destination.pubkey),
73
+ mint: asBase58(m.keys.mint.pubkey),
74
+ mintAuthority: asBase58(m.keys.authority.pubkey),
75
+ tokenAmount: {
76
+ amount: amtStr,
77
+ decimals,
78
+ uiAmount: Number(uiStr),
79
+ uiAmountString: uiStr,
80
+ },
81
+ });
82
+ } catch {}
38
83
 
39
- // Probe with generic decoder (ignore result), then try specific decoders
40
- try {
41
- splDecodeInstruction(ix);
42
- } catch {}
84
+ // Transfer / TransferChecked (strict)
85
+ try {
86
+ const t = decodeTransferInstruction(ix, programPk);
87
+ const amt = t.data.amount;
88
+ return ok(programIdStr, "transfer", {
89
+ amount: typeof amt === "bigint" ? amt.toString() : String(amt),
90
+ source: asBase58(t.keys.source.pubkey),
91
+ destination: asBase58(t.keys.destination.pubkey),
92
+ authority: asBase58(t.keys.owner.pubkey),
93
+ });
94
+ } catch {}
43
95
 
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 {}
96
+ try {
97
+ const t = decodeTransferCheckedInstruction(ix, programPk);
98
+ const amt = t.data.amount;
99
+ const decimals = t.data.decimals;
100
+ return ok(programIdStr, "transferChecked", {
101
+ tokenAmount: {
102
+ amount: typeof amt === "bigint" ? amt.toString() : String(amt),
103
+ decimals,
104
+ },
105
+ source: asBase58(t.keys.source.pubkey),
106
+ destination: asBase58(t.keys.destination.pubkey),
107
+ authority: asBase58(t.keys.owner.pubkey),
108
+ mint: asBase58(t.keys.mint.pubkey),
109
+ });
110
+ } catch {}
67
111
 
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 {}
112
+ // InitializeAccount3 (strict)
113
+ try {
114
+ const a = decodeInitializeAccount3Instruction(ix, programPk);
115
+ return ok(programIdStr, "initializeAccount3", {
116
+ account: asBase58(a.keys.account.pubkey),
117
+ mint: asBase58(a.keys.mint.pubkey),
118
+ owner: asBase58(a.data.owner),
119
+ });
120
+ } catch {}
79
121
 
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 {}
122
+ // InitializeImmutableOwner
123
+ try {
124
+ const im = decodeInitializeImmutableOwnerInstruction(ix, programPk);
125
+ return ok(programIdStr, "initializeImmutableOwner", {
126
+ account: asBase58(im.keys.account.pubkey),
127
+ });
128
+ } catch {}
95
129
 
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 {}
130
+ // GetAccountDataSize: decode extension types (u16 little-endian sequence)
131
+ try {
132
+ const bytes = Buffer.from(bs58decode(dataBase58));
133
+ const t = u8().decode(bytes);
134
+ // 21 corresponds to TokenInstruction.GetAccountDataSize
135
+ if (t === 21) {
136
+ const mint = asBase58(ix.keys[0]?.pubkey);
137
+ const extCodes: number[] = [];
138
+ for (let i = 1; i + 1 < bytes.length; i += 2) {
139
+ const code = bytes[i] | (bytes[i + 1] << 8);
140
+ extCodes.push(code);
141
+ }
142
+ const extMap: Record<number, string> = {
143
+ 7: "immutableOwner",
144
+ 8: "memoTransfer",
145
+ 9: "nonTransferable",
146
+ 12: "permanentDelegate",
147
+ 14: "transferHook",
148
+ 15: "transferHookAccount",
149
+ 18: "metadataPointer",
150
+ 19: "tokenMetadata",
151
+ 20: "groupPointer",
152
+ 21: "tokenGroup",
153
+ 22: "groupMemberPointer",
154
+ 23: "tokenGroupMember",
155
+ 25: "scaledUiAmountConfig",
156
+ 26: "pausableConfig",
157
+ 27: "pausableAccount",
158
+ };
159
+ const extensionTypes = extCodes.map((c) => extMap[c] || String(c));
160
+ return ok(programIdStr, "getAccountDataSize", { mint, extensionTypes });
161
+ }
162
+ } catch {}
105
163
 
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 {}
164
+ // Unchecked fallbacks: decode data fields even if keys/validation missing
165
+ try {
166
+ const raw = bs58decode(dataBase58);
167
+ const op = raw[0];
168
+ // Transfer
169
+ if (op === 3) {
170
+ const t = decodeTransferInstructionUnchecked(ix);
171
+ const amt = t.data.amount;
172
+ return ok(programIdStr, "transfer", {
173
+ amount: typeof amt === "bigint" ? amt.toString() : String(amt),
174
+ source: asBase58(t.keys.source?.pubkey),
175
+ destination: asBase58(t.keys.destination?.pubkey),
176
+ authority: asBase58(t.keys.owner?.pubkey),
177
+ });
178
+ }
179
+ // TransferChecked
180
+ if (op === 12) {
181
+ const t = decodeTransferCheckedInstructionUnchecked(ix);
182
+ const amt = t.data.amount;
183
+ const decimals = t.data.decimals;
184
+ const hintMint = (() => {
185
+ try {
186
+ const dec = Number(decimals);
187
+ const candidates = (tokenBalanceHints || []).filter(
188
+ (h) => Number(h.decimals) === dec,
189
+ );
190
+ if (candidates.length === 1) return candidates[0].mint;
191
+ // Prefer non-zero decimals over 0 (filters out native 4uQe mint in many cases)
192
+ const nonZero = candidates.filter((c) => c.decimals > 0);
193
+ if (nonZero.length === 1) return nonZero[0].mint;
194
+ // Fall back to first candidate if multiple
195
+ return candidates[0]?.mint;
196
+ } catch {
197
+ return undefined;
198
+ }
199
+ })();
200
+ return ok(programIdStr, "transferChecked", {
201
+ tokenAmount: {
202
+ amount: typeof amt === "bigint" ? amt.toString() : String(amt),
203
+ decimals,
204
+ },
205
+ source: asBase58(t.keys.source?.pubkey),
206
+ destination: asBase58(t.keys.destination?.pubkey),
207
+ authority: asBase58(t.keys.owner?.pubkey),
208
+ mint: asBase58(t.keys.mint?.pubkey) || hintMint,
209
+ });
210
+ }
211
+ // InitializeAccount3
212
+ if (op === 18) {
213
+ const a = decodeInitializeAccount3InstructionUnchecked(ix);
214
+ const hintMint = (() => {
215
+ try {
216
+ // Prefer single non-zero-decimals mint in this tx
217
+ const nonZero = (tokenBalanceHints || []).filter(
218
+ (h) => h.decimals > 0,
219
+ );
220
+ if (nonZero.length === 1) return nonZero[0].mint;
221
+ // Fall back to first available mint
222
+ return (tokenBalanceHints || [])[0]?.mint;
223
+ } catch {
224
+ return undefined;
225
+ }
226
+ })();
227
+ const ownerStr = asBase58(a.data.owner);
228
+ const mintStr = asBase58(a.keys.mint?.pubkey) || hintMint;
229
+ let accountStr = asBase58(a.keys.account?.pubkey);
230
+ try {
231
+ if (!accountStr && ownerStr && mintStr) {
232
+ const ownerPk = new PK(ownerStr);
233
+ const mintPk = new PK(mintStr);
234
+ const programId =
235
+ programIdStr === TOKEN_PROGRAM_2022.toBase58()
236
+ ? TOKEN_PROGRAM_2022
237
+ : TOKEN_PROGRAM_V1;
238
+ const ata = getAssociatedTokenAddressSync(
239
+ mintPk,
240
+ ownerPk,
241
+ true,
242
+ programId,
243
+ );
244
+ accountStr = ata.toBase58();
245
+ }
246
+ } catch {}
247
+ return ok(programIdStr, "initializeAccount3", {
248
+ account: accountStr,
249
+ mint: mintStr,
250
+ owner: ownerStr,
251
+ });
252
+ }
253
+ // InitializeImmutableOwner
254
+ if (op === 22) {
255
+ const im = decodeInitializeImmutableOwnerInstructionUnchecked(ix);
256
+ return ok(programIdStr, "initializeImmutableOwner", {
257
+ account: asBase58(im.keys.account?.pubkey),
258
+ });
259
+ }
260
+ } catch {}
113
261
 
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 {}
262
+ // Fallback: classify by TokenInstruction opcode (first byte) when nothing else matched
263
+ try {
264
+ const raw = bs58decode(dataBase58);
265
+ if (raw.length > 0) {
266
+ const op = raw[0];
267
+ const map: Record<number, string> = {
268
+ 0: "initializeMint",
269
+ 1: "initializeAccount",
270
+ 2: "initializeMultisig",
271
+ 3: "transfer",
272
+ 4: "approve",
273
+ 5: "revoke",
274
+ 6: "setAuthority",
275
+ 7: "mintTo",
276
+ 8: "burn",
277
+ 9: "closeAccount",
278
+ 10: "freezeAccount",
279
+ 11: "thawAccount",
280
+ 12: "transferChecked",
281
+ 13: "approveChecked",
282
+ 14: "mintToChecked",
283
+ 15: "burnChecked",
284
+ 16: "initializeAccount2",
285
+ 17: "syncNative",
286
+ 18: "initializeAccount3",
287
+ 19: "initializeMultisig2",
288
+ 20: "initializeMint2",
289
+ 21: "getAccountDataSize",
290
+ 22: "initializeImmutableOwner",
291
+ 23: "amountToUiAmount",
292
+ 24: "uiAmountToAmount",
293
+ 25: "initializeMintCloseAuthority",
294
+ 26: "transferFeeExtension",
295
+ 27: "confidentialTransferExtension",
296
+ 28: "defaultAccountStateExtension",
297
+ 29: "reallocate",
298
+ 30: "memoTransferExtension",
299
+ 31: "createNativeMint",
300
+ 32: "initializeNonTransferableMint",
301
+ 33: "interestBearingMintExtension",
302
+ 34: "cpiGuardExtension",
303
+ 35: "initializePermanentDelegate",
304
+ 36: "transferHookExtension",
305
+ 39: "metadataPointerExtension",
306
+ 40: "groupPointerExtension",
307
+ 41: "groupMemberPointerExtension",
308
+ 43: "scaledUiAmountExtension",
309
+ 44: "pausableExtension",
310
+ };
311
+ const type = map[op];
312
+ if (type) return ok(programIdStr, type, {});
313
+ }
314
+ } catch {}
147
315
 
148
- // Unknown SPL token instruction
149
- return null;
150
- } catch {
151
- return null;
152
- }
316
+ // Unknown SPL token instruction (unrecognized opcode)
317
+ return null;
318
+ } catch {
319
+ return null;
320
+ }
153
321
  }
154
322
 
155
323
  // Local base58 decode to avoid importing from sibling file
156
324
  const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
157
325
  const BASE = BigInt(ALPHABET.length);
158
326
  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]);
327
+ let num = 0n;
328
+ for (const char of str) {
329
+ const index = ALPHABET.indexOf(char);
330
+ if (index === -1) throw new Error("Invalid base58 character");
331
+ num = num * BASE + BigInt(index);
332
+ }
333
+ const bytes: number[] = [];
334
+ while (num > 0n) {
335
+ bytes.unshift(Number(num % 256n));
336
+ num = num / 256n;
337
+ }
338
+ for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.unshift(0);
339
+ return new Uint8Array(bytes.length > 0 ? bytes : [0]);
172
340
  }