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.
- package/package.json +1 -1
- package/scripts/decode-b58.ts +10 -0
- package/server/lib/instruction-parser.ts +305 -219
- package/server/lib/parsers/spl-associated-token-account.ts +50 -0
- package/server/lib/parsers/spl-token.ts +315 -0
- package/server/methods/account/request-airdrop.ts +121 -105
- package/server/methods/admin/mint-to.ts +29 -14
- package/server/methods/transaction/get-transaction.ts +390 -326
- package/server/methods/transaction/inner-instructions.test.ts +91 -50
- package/server/methods/transaction/send-transaction.ts +281 -236
- package/server/rpc-server.ts +84 -74
- package/server/types.ts +56 -56
- package/src/db/schema/transactions.ts +29 -29
- package/src/db/schema/tx-account-states.ts +16 -14
- package/src/db/tx-store.ts +97 -99
- package/src/migrations-bundled.ts +4 -4
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
176
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
247
|
+
// send-transaction already records/announces signature and persists to DB
|
|
233
248
|
|
|
234
249
|
return context.createSuccessResponse(id, {
|
|
235
250
|
ok: true,
|