solforge 0.2.7 → 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.
@@ -0,0 +1,315 @@
1
+ import type { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
+ import {
3
+ decodeMintToCheckedInstruction,
4
+ decodeTransferInstruction,
5
+ decodeTransferCheckedInstruction,
6
+ decodeInitializeAccount3Instruction,
7
+ decodeInitializeImmutableOwnerInstruction,
8
+ decodeTransferCheckedInstructionUnchecked,
9
+ decodeTransferInstructionUnchecked,
10
+ decodeInitializeAccount3InstructionUnchecked,
11
+ decodeInitializeImmutableOwnerInstructionUnchecked,
12
+ } from "@solana/spl-token";
13
+ import { u8 } from "@solana/buffer-layout";
14
+ import { PublicKey as PK } from "@solana/web3.js";
15
+
16
+ // Keep shape compatible with instruction-parser
17
+ export type ParsedInstruction =
18
+ | {
19
+ program: string;
20
+ programId: string;
21
+ parsed: { type: string; info: unknown };
22
+ }
23
+ | { programId: string; accounts: string[]; data: string };
24
+
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 } };
28
+ }
29
+
30
+ function asBase58(pk: PublicKey | undefined): string | undefined {
31
+ try {
32
+ return pk ? pk.toBase58() : undefined;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ export function tryParseSplToken(
39
+ ix: TransactionInstruction,
40
+ programIdStr: string,
41
+ _accountKeys: string[],
42
+ dataBase58: string,
43
+ tokenBalanceHints?: Array<{ mint: string; decimals: number }>,
44
+ ): ParsedInstruction | 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
50
+
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 {}
78
+
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 {}
90
+
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 {}
106
+
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 {}
116
+
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 {}
124
+
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 {}
158
+
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 {}
236
+
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
+ }
296
+ }
297
+
298
+ // Local base58 decode to avoid importing from sibling file
299
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
300
+ const BASE = BigInt(ALPHABET.length);
301
+ function bs58decode(str: string): Uint8Array {
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]);
315
+ }
@@ -69,48 +69,48 @@ export const requestAirdrop: RpcMethodHandler = async (id, params, context) => {
69
69
  : Array.isArray(msg.accountKeys)
70
70
  ? (msg.accountKeys as unknown[])
71
71
  : [];
72
- const staticKeys = rawKeys.map((k) => {
73
- try {
74
- return typeof k === "string" ? new PublicKey(k) : (k as PublicKey);
75
- } catch {
76
- return faucet.publicKey;
77
- }
78
- });
79
- const preBalances = staticKeys.map((pk) => {
80
- try {
81
- return Number(context.svm.getBalance(pk));
82
- } catch {
83
- return 0;
84
- }
85
- });
86
- const preAccountStates = staticKeys.map((pk) => {
87
- try {
88
- const addr = pk.toBase58();
89
- const acc = context.svm.getAccount(pk);
90
- if (!acc) return { address: addr, pre: null } as const;
91
- return {
92
- address: addr,
93
- pre: {
94
- lamports: Number(acc.lamports || 0n),
95
- ownerProgram: new PublicKey(acc.owner).toBase58(),
96
- executable: !!acc.executable,
97
- rentEpoch: Number(acc.rentEpoch || 0),
98
- dataLen: acc.data?.length ?? 0,
99
- dataBase64: undefined,
100
- lastSlot: Number(context.slot),
101
- },
102
- } as const;
103
- } catch {
104
- return { address: pk.toBase58(), pre: null } as const;
105
- }
106
- });
107
- try {
108
- if (process.env.DEBUG_TX_CAPTURE === "1") {
109
- console.debug(
110
- `[tx-capture] pre snapshots: keys=${staticKeys.length} captured=${preAccountStates.length}`,
111
- );
112
- }
113
- } catch {}
72
+ const staticKeys = rawKeys.map((k) => {
73
+ try {
74
+ return typeof k === "string" ? new PublicKey(k) : (k as PublicKey);
75
+ } catch {
76
+ return faucet.publicKey;
77
+ }
78
+ });
79
+ const preBalances = staticKeys.map((pk) => {
80
+ try {
81
+ return Number(context.svm.getBalance(pk));
82
+ } catch {
83
+ return 0;
84
+ }
85
+ });
86
+ const preAccountStates = staticKeys.map((pk) => {
87
+ try {
88
+ const addr = pk.toBase58();
89
+ const acc = context.svm.getAccount(pk);
90
+ if (!acc) return { address: addr, pre: null } as const;
91
+ return {
92
+ address: addr,
93
+ pre: {
94
+ lamports: Number(acc.lamports || 0n),
95
+ ownerProgram: new PublicKey(acc.owner).toBase58(),
96
+ executable: !!acc.executable,
97
+ rentEpoch: Number(acc.rentEpoch || 0),
98
+ dataLen: acc.data?.length ?? 0,
99
+ dataBase64: undefined,
100
+ lastSlot: Number(context.slot),
101
+ },
102
+ } as const;
103
+ } catch {
104
+ return { address: pk.toBase58(), pre: null } as const;
105
+ }
106
+ });
107
+ try {
108
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
109
+ console.debug(
110
+ `[tx-capture] pre snapshots: keys=${staticKeys.length} captured=${preAccountStates.length}`,
111
+ );
112
+ }
113
+ } catch {}
114
114
  const toIndex = staticKeys.findIndex((pk) => pk.equals(toPubkey));
115
115
  const beforeTo =
116
116
  toIndex >= 0
@@ -123,58 +123,73 @@ export const requestAirdrop: RpcMethodHandler = async (id, params, context) => {
123
123
  }
124
124
  })();
125
125
 
126
- // Send via standard sendTransaction RPC to unify capture + persistence
127
- const rawB64 = Buffer.from(tx.serialize()).toString("base64");
128
- const resp = await (sendTxRpc as RpcMethodHandler)(id, [rawB64], context);
129
- if ((resp as any)?.error) return resp;
130
- // Surface errors to aid debugging
131
- const sendResult = undefined as unknown as { err?: unknown };
132
- // Any send errors would have been returned by send-transaction already
126
+ // Send via standard sendTransaction RPC to unify capture + persistence
127
+ const rawB64 = Buffer.from(tx.serialize()).toString("base64");
128
+ const resp = await (sendTxRpc as RpcMethodHandler)(id, [rawB64], context);
129
+ if (
130
+ resp &&
131
+ typeof resp === "object" &&
132
+ "error" in (resp as Record<string, unknown>) &&
133
+ (resp as Record<string, unknown>).error != null
134
+ ) {
135
+ return resp;
136
+ }
137
+ // Any send errors would have been returned by send-transaction already
133
138
 
134
- let signature = String((resp as any)?.result || "");
135
- if (!signature) {
136
- signature = tx.signatures[0]
137
- ? context.encodeBase58(tx.signatures[0])
138
- : context.encodeBase58(new Uint8Array(64).fill(0));
139
- }
140
- try { context.notifySignature(signature); } catch {}
141
- // Compute post balances and capture logs if available for explorer detail view
142
- let postBalances = staticKeys.map((pk) => {
143
- try {
144
- return Number(context.svm.getBalance(pk));
145
- } catch {
146
- return 0;
147
- }
148
- });
149
- const postAccountStates = staticKeys.map((pk) => {
150
- try {
151
- const addr = pk.toBase58();
152
- const acc = context.svm.getAccount(pk);
153
- if (!acc) return { address: addr, post: null } as const;
154
- return {
155
- address: addr,
156
- post: {
157
- lamports: Number(acc.lamports || 0n),
158
- ownerProgram: new PublicKey(acc.owner).toBase58(),
159
- executable: !!acc.executable,
160
- rentEpoch: Number(acc.rentEpoch || 0),
161
- dataLen: acc.data?.length ?? 0,
162
- dataBase64: undefined,
163
- lastSlot: Number(context.slot),
164
- },
165
- } as const;
166
- } catch {
167
- return { address: pk.toBase58(), post: null } as const;
168
- }
169
- });
170
- try {
171
- if (process.env.DEBUG_TX_CAPTURE === "1") {
172
- console.debug(
173
- `[tx-capture] post snapshots: keys=${staticKeys.length} captured=${postAccountStates.length}`,
174
- );
175
- }
176
- } catch {}
177
- // Parsing, recording etc. are performed by send-transaction
139
+ let signature = (() => {
140
+ try {
141
+ const r = resp as Record<string, unknown>;
142
+ const v = r?.result;
143
+ return v == null ? "" : String(v);
144
+ } catch {
145
+ return "";
146
+ }
147
+ })();
148
+ if (!signature) {
149
+ signature = tx.signatures[0]
150
+ ? context.encodeBase58(tx.signatures[0])
151
+ : context.encodeBase58(new Uint8Array(64).fill(0));
152
+ }
153
+ try {
154
+ context.notifySignature(signature);
155
+ } catch {}
156
+ // Compute post balances and capture logs if available for explorer detail view
157
+ let postBalances = staticKeys.map((pk) => {
158
+ try {
159
+ return Number(context.svm.getBalance(pk));
160
+ } catch {
161
+ return 0;
162
+ }
163
+ });
164
+ const postAccountStates = staticKeys.map((pk) => {
165
+ try {
166
+ const addr = pk.toBase58();
167
+ const acc = context.svm.getAccount(pk);
168
+ if (!acc) return { address: addr, post: null } as const;
169
+ return {
170
+ address: addr,
171
+ post: {
172
+ lamports: Number(acc.lamports || 0n),
173
+ ownerProgram: new PublicKey(acc.owner).toBase58(),
174
+ executable: !!acc.executable,
175
+ rentEpoch: Number(acc.rentEpoch || 0),
176
+ dataLen: acc.data?.length ?? 0,
177
+ dataBase64: undefined,
178
+ lastSlot: Number(context.slot),
179
+ },
180
+ } as const;
181
+ } catch {
182
+ return { address: pk.toBase58(), post: null } as const;
183
+ }
184
+ });
185
+ try {
186
+ if (process.env.DEBUG_TX_CAPTURE === "1") {
187
+ console.debug(
188
+ `[tx-capture] post snapshots: keys=${staticKeys.length} captured=${postAccountStates.length}`,
189
+ );
190
+ }
191
+ } catch {}
192
+ // Parsing, recording etc. are performed by send-transaction
178
193
  // Verify recipient received lamports; retry once if not
179
194
  const afterTo =
180
195
  toIndex >= 0
@@ -234,18 +249,19 @@ export const requestAirdrop: RpcMethodHandler = async (id, params, context) => {
234
249
  }
235
250
 
236
251
  // Try to capture error again for accurate status reporting
237
- // No additional error capture; send-transaction has already recorded it
238
- // Pre/post snapshots are still useful for account cache; we can upsert
239
- try {
240
- const snapshots = new Map<string, { pre?: any; post?: any }>();
241
- for (const s of preAccountStates) snapshots.set(s.address, { pre: s.pre || null });
242
- for (const s of postAccountStates) {
243
- const e = snapshots.get(s.address) || {};
244
- e.post = s.post || null;
245
- snapshots.set(s.address, e);
246
- }
247
- // Not persisted here; DB already has the transaction via send-transaction
248
- } catch {}
252
+ // No additional error capture; send-transaction has already recorded it
253
+ // Pre/post snapshots are still useful for account cache; we can upsert
254
+ try {
255
+ const snapshots = new Map<string, { pre?: unknown; post?: unknown }>();
256
+ for (const s of preAccountStates)
257
+ snapshots.set(s.address, { pre: s.pre || null });
258
+ for (const s of postAccountStates) {
259
+ const e = snapshots.get(s.address) || {};
260
+ e.post = s.post || null;
261
+ snapshots.set(s.address, e);
262
+ }
263
+ // Not persisted here; DB already has the transaction via send-transaction
264
+ } catch {}
249
265
 
250
266
  return context.createSuccessResponse(id, signature);
251
267
  } catch (error: unknown) {
@@ -126,7 +126,7 @@ export const solforgeMintTo: RpcMethodHandler = async (id, params, context) => {
126
126
 
127
127
  // Capture preBalances for primary accounts referenced and token pre amount
128
128
  const trackedKeys = [faucet.publicKey, ata, mint, owner];
129
- const preBalances = trackedKeys.map((pk) => {
129
+ const _preBalances = trackedKeys.map((pk) => {
130
130
  try {
131
131
  return Number(context.svm.getBalance(pk) || 0n);
132
132
  } catch {
@@ -149,15 +149,30 @@ export const solforgeMintTo: RpcMethodHandler = async (id, params, context) => {
149
149
  }
150
150
  } catch {}
151
151
 
152
- // Send via the standard RPC sendTransaction path to unify capture/parsing
153
- const rawBase64ForRpc = Buffer.from(vtx.serialize()).toString("base64");
154
- const rpcResp = await (sendTxRpc as RpcMethodHandler)(
155
- id,
156
- [rawBase64ForRpc],
157
- context,
158
- );
159
- if ((rpcResp as any)?.error) return rpcResp;
160
- const signatureStr = String((rpcResp as any)?.result || "");
152
+ // Send via the standard RPC sendTransaction path to unify capture/parsing
153
+ const rawBase64ForRpc = Buffer.from(vtx.serialize()).toString("base64");
154
+ const rpcResp = await (sendTxRpc as RpcMethodHandler)(
155
+ id,
156
+ [rawBase64ForRpc],
157
+ context,
158
+ );
159
+ if (
160
+ rpcResp &&
161
+ typeof rpcResp === "object" &&
162
+ "error" in (rpcResp as Record<string, unknown>) &&
163
+ (rpcResp as Record<string, unknown>).error != null
164
+ ) {
165
+ return rpcResp;
166
+ }
167
+ const signatureStr = (() => {
168
+ try {
169
+ const r = rpcResp as Record<string, unknown>;
170
+ const v = r?.result;
171
+ return v == null ? "" : String(v);
172
+ } catch {
173
+ return "";
174
+ }
175
+ })();
161
176
 
162
177
  // Token balance deltas (pre/post) for ATA
163
178
  type UiTokenAmount = {
@@ -172,8 +187,8 @@ export const solforgeMintTo: RpcMethodHandler = async (id, params, context) => {
172
187
  owner: string;
173
188
  uiTokenAmount: UiTokenAmount;
174
189
  };
175
- let preTokenBalances: TokenBalance[] = [];
176
- let postTokenBalances: TokenBalance[] = [];
190
+ const _preTokenBalances: TokenBalance[] = [];
191
+ let _postTokenBalances: TokenBalance[] = [];
177
192
  try {
178
193
  const decs = decsForMint;
179
194
  const ui = (n: bigint) => ({
@@ -219,7 +234,7 @@ export const solforgeMintTo: RpcMethodHandler = async (id, params, context) => {
219
234
  uiTokenAmount: ui(preAmt),
220
235
  },
221
236
  ];
222
- postTokenBalances = [
237
+ _postTokenBalances = [
223
238
  {
224
239
  accountIndex: ataIndex >= 0 ? ataIndex : 0,
225
240
  mint: mint.toBase58(),
@@ -229,7 +244,7 @@ export const solforgeMintTo: RpcMethodHandler = async (id, params, context) => {
229
244
  ];
230
245
  } catch {}
231
246
 
232
- // send-transaction already records/announces signature and persists to DB
247
+ // send-transaction already records/announces signature and persists to DB
233
248
 
234
249
  return context.createSuccessResponse(id, {
235
250
  ok: true,