solforge 0.2.8 → 0.2.9

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