@vtx-labs/solana-explain 0.1.0
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/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +3317 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +3130 -0
- package/dist/index.js.map +1 -0
- package/dist/programs.d.ts +42 -0
- package/dist/programs.js +919 -0
- package/dist/programs.js.map +1 -0
- package/dist/render.d.ts +59 -0
- package/dist/render.js +300 -0
- package/dist/render.js.map +1 -0
- package/dist/types-MSKEy1VA.d.ts +482 -0
- package/package.json +83 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3130 @@
|
|
|
1
|
+
import bs58 from 'bs58';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var SolanaExplainError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
cause;
|
|
7
|
+
/** Optional, CLI-friendly remediation hint. */
|
|
8
|
+
hint;
|
|
9
|
+
constructor(code, message, options = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = new.target.name;
|
|
12
|
+
this.code = code;
|
|
13
|
+
if (options.cause !== void 0) this.cause = options.cause;
|
|
14
|
+
if (options.hint !== void 0) this.hint = options.hint;
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var RpcError = class extends SolanaExplainError {
|
|
19
|
+
};
|
|
20
|
+
var DecodeError = class extends SolanaExplainError {
|
|
21
|
+
};
|
|
22
|
+
var SimulationError = class extends SolanaExplainError {
|
|
23
|
+
/** Partial result when the simulation ran but reverted. */
|
|
24
|
+
result;
|
|
25
|
+
/** Tail of the node's program logs, for diagnostics. */
|
|
26
|
+
logs;
|
|
27
|
+
constructor(code, message, options = {}) {
|
|
28
|
+
super(code, message, options);
|
|
29
|
+
if (options.result !== void 0) this.result = options.result;
|
|
30
|
+
if (options.logs !== void 0) this.logs = options.logs;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var InputError = class extends SolanaExplainError {
|
|
34
|
+
};
|
|
35
|
+
function wrapError(err, fallbackCode, fallbackMessage) {
|
|
36
|
+
if (err instanceof SolanaExplainError) return err;
|
|
37
|
+
if (isAbortError(err)) {
|
|
38
|
+
return new RpcError("ABORTED", "The operation was aborted.", { cause: err });
|
|
39
|
+
}
|
|
40
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
41
|
+
return new SolanaExplainError(fallbackCode, `${fallbackMessage}: ${detail}`, { cause: err });
|
|
42
|
+
}
|
|
43
|
+
function isAbortError(err) {
|
|
44
|
+
return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
|
|
45
|
+
}
|
|
46
|
+
function decodeBase58(input) {
|
|
47
|
+
try {
|
|
48
|
+
return bs58.decode(input);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new InputError("INVALID_ENCODING", "Input is not valid base58.", { cause: err });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function encodeBase58(bytes) {
|
|
54
|
+
return bs58.encode(bytes);
|
|
55
|
+
}
|
|
56
|
+
function tryDecodeBase58(input) {
|
|
57
|
+
try {
|
|
58
|
+
const out = bs58.decode(input);
|
|
59
|
+
return out.length > 0 ? out : null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
65
|
+
function decodeBase64(input) {
|
|
66
|
+
const trimmed = input.trim();
|
|
67
|
+
if (!BASE64_RE.test(trimmed) || trimmed.length % 4 !== 0) {
|
|
68
|
+
throw new InputError("INVALID_ENCODING", "Input is not valid base64.");
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return new Uint8Array(Buffer.from(trimmed, "base64"));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new InputError("INVALID_ENCODING", "Input is not valid base64.", { cause: err });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function tryDecodeBase64(input) {
|
|
77
|
+
const trimmed = input.trim();
|
|
78
|
+
if (trimmed.length === 0 || trimmed.length % 4 !== 0 || !BASE64_RE.test(trimmed)) return null;
|
|
79
|
+
try {
|
|
80
|
+
const buf = Buffer.from(trimmed, "base64");
|
|
81
|
+
if (buf.length === 0) return null;
|
|
82
|
+
if (buf.toString("base64") !== trimmed) return null;
|
|
83
|
+
return new Uint8Array(buf);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function encodeBase64(bytes) {
|
|
89
|
+
return Buffer.from(bytes).toString("base64");
|
|
90
|
+
}
|
|
91
|
+
function looksLikeBase64(input) {
|
|
92
|
+
const trimmed = input.trim();
|
|
93
|
+
if (trimmed.length === 0) return false;
|
|
94
|
+
if (/[+/=]/.test(trimmed)) return true;
|
|
95
|
+
return BASE64_RE.test(trimmed);
|
|
96
|
+
}
|
|
97
|
+
function looksLikeBase58(input) {
|
|
98
|
+
const trimmed = input.trim();
|
|
99
|
+
if (trimmed.length === 0) return false;
|
|
100
|
+
return /^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed);
|
|
101
|
+
}
|
|
102
|
+
function normalizeRpcUrl(url) {
|
|
103
|
+
let u = url.trim();
|
|
104
|
+
if (u.length === 0) {
|
|
105
|
+
throw new InputError("INVALID_INPUT", "RPC URL is empty.");
|
|
106
|
+
}
|
|
107
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(u)) {
|
|
108
|
+
u = `https://${u}`;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = new URL(u);
|
|
112
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
113
|
+
throw new InputError(
|
|
114
|
+
"INVALID_INPUT",
|
|
115
|
+
`Unsupported RPC URL scheme "${parsed.protocol}". Use http(s).`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return parsed.toString();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err instanceof InputError) throw err;
|
|
121
|
+
throw new InputError("INVALID_INPUT", `Invalid RPC URL: ${url}`, { cause: err });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/rpc/http.ts
|
|
126
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
127
|
+
function sleep(ms, signal) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
if (signal?.aborted) {
|
|
130
|
+
reject(new RpcError("ABORTED", "Aborted while backing off."));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const t = setTimeout(resolve, ms);
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
clearTimeout(t);
|
|
136
|
+
reject(new RpcError("ABORTED", "Aborted while backing off."));
|
|
137
|
+
};
|
|
138
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function combineSignals(timeoutMs, external) {
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
let timedOut = false;
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
controller.abort();
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
const onExternalAbort = () => controller.abort();
|
|
149
|
+
if (external) {
|
|
150
|
+
if (external.aborted) controller.abort();
|
|
151
|
+
else external.addEventListener("abort", onExternalAbort, { once: true });
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
timedOut: () => timedOut,
|
|
156
|
+
cleanup: () => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
external?.removeEventListener("abort", onExternalAbort);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function createHttpRpc(url, opts = {}) {
|
|
163
|
+
const endpoint = normalizeRpcUrl(url);
|
|
164
|
+
const defaultTimeout = opts.timeoutMs ?? 3e4;
|
|
165
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
166
|
+
if (typeof fetchImpl !== "function") {
|
|
167
|
+
throw new RpcError(
|
|
168
|
+
"RPC_HTTP",
|
|
169
|
+
"global fetch is not available; use Node 18+ or pass a fetch implementation."
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
let idCounter = 1;
|
|
173
|
+
async function call(method, params, timeoutMs, externalSignal) {
|
|
174
|
+
const body = { jsonrpc: "2.0", id: idCounter++, method, params };
|
|
175
|
+
let attempt = 0;
|
|
176
|
+
for (; ; ) {
|
|
177
|
+
const { signal, timedOut, cleanup } = combineSignals(timeoutMs, externalSignal);
|
|
178
|
+
let res;
|
|
179
|
+
try {
|
|
180
|
+
res = await fetchImpl(endpoint, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "content-type": "application/json", ...opts.headers },
|
|
183
|
+
body: JSON.stringify(body),
|
|
184
|
+
signal
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
cleanup();
|
|
188
|
+
if (timedOut()) {
|
|
189
|
+
throw new RpcError("RPC_TIMEOUT", `RPC request "${method}" timed out after ${timeoutMs}ms.`, {
|
|
190
|
+
cause: err,
|
|
191
|
+
hint: "Increase --timeout or check your RPC URL."
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (isAbortError(err) || externalSignal?.aborted) {
|
|
195
|
+
throw new RpcError("ABORTED", `RPC request "${method}" was aborted.`, { cause: err });
|
|
196
|
+
}
|
|
197
|
+
throw new RpcError("RPC_HTTP", `Network error calling "${method}": ${describeErr(err)}`, {
|
|
198
|
+
cause: err,
|
|
199
|
+
hint: "Check connectivity and that the RPC URL is reachable."
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
cleanup();
|
|
203
|
+
if (RETRY_STATUSES.has(res.status) && attempt < 1) {
|
|
204
|
+
attempt++;
|
|
205
|
+
const backoff = 250 + Math.floor(Math.random() * 400);
|
|
206
|
+
await sleep(backoff, externalSignal);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const text = await res.text();
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
throw new RpcError(
|
|
212
|
+
"RPC_HTTP",
|
|
213
|
+
`RPC HTTP ${res.status} ${res.statusText} for "${method}". Body: ${snippet(text)}`,
|
|
214
|
+
res.status === 429 ? { hint: "Public endpoints are rate-limited; use a dedicated RPC." } : {}
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(text);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
throw new RpcError(
|
|
222
|
+
"RPC_JSON",
|
|
223
|
+
`RPC returned non-JSON for "${method}". Body: ${snippet(text)}`,
|
|
224
|
+
{ cause: err, hint: "Is the RPC URL correct? A captive portal/HTML page may be intercepting." }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (parsed.error) {
|
|
228
|
+
const msg = `${parsed.error.message} (code ${parsed.error.code})`;
|
|
229
|
+
if (/version|transaction version/i.test(parsed.error.message)) {
|
|
230
|
+
throw new RpcError("UNSUPPORTED_TX_VERSION", `RPC: ${msg}`, {
|
|
231
|
+
hint: "Raise --max-tx-version (e.g. 0) to fetch versioned transactions."
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
throw new RpcError("RPC_JSON", `JSON-RPC error for "${method}": ${msg}`);
|
|
235
|
+
}
|
|
236
|
+
return parsed.result;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
async getTransaction(sig, o) {
|
|
241
|
+
const config = {
|
|
242
|
+
encoding: "base64",
|
|
243
|
+
commitment: o.commitment ?? "confirmed"
|
|
244
|
+
};
|
|
245
|
+
if (o.maxSupportedTransactionVersion !== void 0) {
|
|
246
|
+
config["maxSupportedTransactionVersion"] = o.maxSupportedTransactionVersion;
|
|
247
|
+
}
|
|
248
|
+
return call(
|
|
249
|
+
"getTransaction",
|
|
250
|
+
[sig, config],
|
|
251
|
+
defaultTimeout,
|
|
252
|
+
o.signal
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
async simulateTransaction(txBase64, o) {
|
|
256
|
+
const config = {
|
|
257
|
+
encoding: "base64",
|
|
258
|
+
commitment: o.commitment ?? "confirmed",
|
|
259
|
+
sigVerify: o.sigVerify ?? false,
|
|
260
|
+
replaceRecentBlockhash: o.replaceRecentBlockhash ?? true
|
|
261
|
+
};
|
|
262
|
+
if (o.innerInstructions) config["innerInstructions"] = true;
|
|
263
|
+
if (o.accounts) {
|
|
264
|
+
config["accounts"] = {
|
|
265
|
+
addresses: o.accounts.addresses,
|
|
266
|
+
encoding: o.accounts.encoding ?? "jsonParsed"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return call(
|
|
270
|
+
"simulateTransaction",
|
|
271
|
+
[txBase64, config],
|
|
272
|
+
defaultTimeout,
|
|
273
|
+
o.signal
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
async getMultipleAccounts(addresses, o) {
|
|
277
|
+
if (addresses.length === 0) return [];
|
|
278
|
+
const config = {
|
|
279
|
+
commitment: o.commitment ?? "confirmed",
|
|
280
|
+
encoding: o.encoding ?? "jsonParsed"
|
|
281
|
+
};
|
|
282
|
+
const result = await call(
|
|
283
|
+
"getMultipleAccounts",
|
|
284
|
+
[addresses, config],
|
|
285
|
+
defaultTimeout,
|
|
286
|
+
o.signal
|
|
287
|
+
);
|
|
288
|
+
return result.value;
|
|
289
|
+
},
|
|
290
|
+
async getLatestBlockhash(o) {
|
|
291
|
+
const result = await call("getLatestBlockhash", [{ commitment: o?.commitment ?? "confirmed" }], defaultTimeout, o?.signal);
|
|
292
|
+
return result.value;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function snippet(text, n = 120) {
|
|
297
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
298
|
+
return oneLine.length > n ? `${oneLine.slice(0, n)}\u2026` : oneLine;
|
|
299
|
+
}
|
|
300
|
+
function describeErr(err) {
|
|
301
|
+
if (err instanceof Error) {
|
|
302
|
+
const code = err.cause?.code;
|
|
303
|
+
return code ? `${err.message} (${code})` : err.message;
|
|
304
|
+
}
|
|
305
|
+
return String(err);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/decode/known-programs.ts
|
|
309
|
+
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
310
|
+
var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
311
|
+
var TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
312
|
+
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
313
|
+
var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
314
|
+
var MEMO_V1_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
|
|
315
|
+
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
316
|
+
var KNOWN_PROGRAMS = Object.freeze({
|
|
317
|
+
// Core / decoded
|
|
318
|
+
[SYSTEM_PROGRAM_ID]: { name: "System", kind: "system" },
|
|
319
|
+
[TOKEN_PROGRAM_ID]: { name: "SPL Token", kind: "token" },
|
|
320
|
+
[TOKEN_2022_PROGRAM_ID]: { name: "Token-2022", kind: "token-2022" },
|
|
321
|
+
[ASSOCIATED_TOKEN_PROGRAM_ID]: { name: "Associated Token Account", kind: "ata" },
|
|
322
|
+
[MEMO_PROGRAM_ID]: { name: "Memo", kind: "memo" },
|
|
323
|
+
[MEMO_V1_PROGRAM_ID]: { name: "Memo (v1)", kind: "memo" },
|
|
324
|
+
[COMPUTE_BUDGET_PROGRAM_ID]: { name: "Compute Budget", kind: "compute-budget" },
|
|
325
|
+
// Native
|
|
326
|
+
Stake11111111111111111111111111111111111111: { name: "Stake", kind: "stake" },
|
|
327
|
+
Vote111111111111111111111111111111111111111: { name: "Vote", kind: "vote" },
|
|
328
|
+
BPFLoaderUpgradeab1e11111111111111111111111: { name: "BPF Upgradeable Loader", kind: "loader" },
|
|
329
|
+
BPFLoader2111111111111111111111111111111111: { name: "BPF Loader 2", kind: "loader" },
|
|
330
|
+
AddressLookupTab1e1111111111111111111111111: {
|
|
331
|
+
name: "Address Lookup Table",
|
|
332
|
+
kind: "other"
|
|
333
|
+
},
|
|
334
|
+
// Metaplex
|
|
335
|
+
metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s: {
|
|
336
|
+
name: "Metaplex Token Metadata",
|
|
337
|
+
kind: "nft"
|
|
338
|
+
},
|
|
339
|
+
BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY: {
|
|
340
|
+
name: "Metaplex Bubblegum",
|
|
341
|
+
kind: "nft"
|
|
342
|
+
},
|
|
343
|
+
cndy3Z4yxLzowg4ZTHTHJYjBmtkk3vd6yMJ4r8x7q3o: {
|
|
344
|
+
name: "Metaplex Candy Machine",
|
|
345
|
+
kind: "nft"
|
|
346
|
+
},
|
|
347
|
+
// Aggregators / DEX
|
|
348
|
+
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: { name: "Jupiter v6", kind: "aggregator" },
|
|
349
|
+
JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WconZX: { name: "Jupiter v4", kind: "aggregator" },
|
|
350
|
+
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": { name: "Raydium AMM v4", kind: "amm" },
|
|
351
|
+
CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK: { name: "Raydium CLMM", kind: "amm" },
|
|
352
|
+
whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: { name: "Orca Whirlpool", kind: "amm" },
|
|
353
|
+
"9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP": { name: "Orca v2", kind: "amm" },
|
|
354
|
+
PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY: { name: "Phoenix", kind: "amm" },
|
|
355
|
+
LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo: { name: "Meteora DLMM", kind: "amm" }
|
|
356
|
+
});
|
|
357
|
+
function knownProgram(programId) {
|
|
358
|
+
return KNOWN_PROGRAMS[programId];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/decode/reader.ts
|
|
362
|
+
var Reader = class {
|
|
363
|
+
view;
|
|
364
|
+
bytes;
|
|
365
|
+
offset = 0;
|
|
366
|
+
constructor(bytes) {
|
|
367
|
+
this.bytes = bytes;
|
|
368
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
369
|
+
}
|
|
370
|
+
get remaining() {
|
|
371
|
+
return this.bytes.length - this.offset;
|
|
372
|
+
}
|
|
373
|
+
ensure(n, field) {
|
|
374
|
+
if (this.offset + n > this.bytes.length) {
|
|
375
|
+
throw new DecodeError(
|
|
376
|
+
"DECODE_FAILED",
|
|
377
|
+
`unexpected end while reading ${field} (need ${n} bytes at offset ${this.offset}, have ${this.remaining})`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
u8(field = "u8") {
|
|
382
|
+
this.ensure(1, field);
|
|
383
|
+
const v = this.view.getUint8(this.offset);
|
|
384
|
+
this.offset += 1;
|
|
385
|
+
return v;
|
|
386
|
+
}
|
|
387
|
+
u16(field = "u16") {
|
|
388
|
+
this.ensure(2, field);
|
|
389
|
+
const v = this.view.getUint16(this.offset, true);
|
|
390
|
+
this.offset += 2;
|
|
391
|
+
return v;
|
|
392
|
+
}
|
|
393
|
+
u32(field = "u32") {
|
|
394
|
+
this.ensure(4, field);
|
|
395
|
+
const v = this.view.getUint32(this.offset, true);
|
|
396
|
+
this.offset += 4;
|
|
397
|
+
return v;
|
|
398
|
+
}
|
|
399
|
+
u64(field = "u64") {
|
|
400
|
+
this.ensure(8, field);
|
|
401
|
+
const v = this.view.getBigUint64(this.offset, true);
|
|
402
|
+
this.offset += 8;
|
|
403
|
+
return v;
|
|
404
|
+
}
|
|
405
|
+
i64(field = "i64") {
|
|
406
|
+
this.ensure(8, field);
|
|
407
|
+
const v = this.view.getBigInt64(this.offset, true);
|
|
408
|
+
this.offset += 8;
|
|
409
|
+
return v;
|
|
410
|
+
}
|
|
411
|
+
bytes_(n, field = "bytes") {
|
|
412
|
+
this.ensure(n, field);
|
|
413
|
+
const out = this.bytes.subarray(this.offset, this.offset + n);
|
|
414
|
+
this.offset += n;
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
/** Read a 32-byte pubkey and return base58. */
|
|
418
|
+
pubkey(field = "pubkey") {
|
|
419
|
+
return encodeBase58(this.bytes_(32, field));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Solana compact-u16 (a.k.a. ShortVec) length prefix. 1–3 bytes, 7 bits each.
|
|
423
|
+
*/
|
|
424
|
+
compactU16(field = "compact-u16") {
|
|
425
|
+
let value = 0;
|
|
426
|
+
let shift = 0;
|
|
427
|
+
for (let i = 0; i < 3; i++) {
|
|
428
|
+
const byte = this.u8(`${field} length byte`);
|
|
429
|
+
value |= (byte & 127) << shift;
|
|
430
|
+
if ((byte & 128) === 0) return value >>> 0;
|
|
431
|
+
shift += 7;
|
|
432
|
+
}
|
|
433
|
+
return value >>> 0;
|
|
434
|
+
}
|
|
435
|
+
/** Remaining bytes (no copy). */
|
|
436
|
+
rest() {
|
|
437
|
+
const out = this.bytes.subarray(this.offset);
|
|
438
|
+
this.offset = this.bytes.length;
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/decode/programs/system.ts
|
|
444
|
+
function acc(ix, i) {
|
|
445
|
+
return ix.accounts[i]?.pubkey;
|
|
446
|
+
}
|
|
447
|
+
function decodeSystem(ix) {
|
|
448
|
+
if (ix.data.length < 4) {
|
|
449
|
+
return { decoded: false, program: "System", warning: "instruction data too short" };
|
|
450
|
+
}
|
|
451
|
+
const r = new Reader(ix.data);
|
|
452
|
+
const disc = r.u32("discriminator");
|
|
453
|
+
try {
|
|
454
|
+
switch (disc) {
|
|
455
|
+
case 0: {
|
|
456
|
+
const lamports = r.u64("lamports");
|
|
457
|
+
const space = r.u64("space");
|
|
458
|
+
const owner = r.pubkey("owner");
|
|
459
|
+
const newAccount = acc(ix, 1) ?? "";
|
|
460
|
+
return {
|
|
461
|
+
program: "System",
|
|
462
|
+
type: "createAccount",
|
|
463
|
+
decoded: true,
|
|
464
|
+
args: { lamports: lamports.toString(), space: space.toString(), owner },
|
|
465
|
+
accountNames: ["funding", "new"],
|
|
466
|
+
effects: [
|
|
467
|
+
{
|
|
468
|
+
kind: "account-created",
|
|
469
|
+
address: newAccount,
|
|
470
|
+
owner,
|
|
471
|
+
lamports,
|
|
472
|
+
space: Number(space),
|
|
473
|
+
as: "system"
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
case 1: {
|
|
479
|
+
const owner = r.pubkey("owner");
|
|
480
|
+
return {
|
|
481
|
+
program: "System",
|
|
482
|
+
type: "assign",
|
|
483
|
+
decoded: true,
|
|
484
|
+
args: { owner },
|
|
485
|
+
accountNames: ["account"]
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
case 2: {
|
|
489
|
+
const lamports = r.u64("lamports");
|
|
490
|
+
const from = acc(ix, 0) ?? "";
|
|
491
|
+
const to = acc(ix, 1) ?? "";
|
|
492
|
+
return {
|
|
493
|
+
program: "System",
|
|
494
|
+
type: "transfer",
|
|
495
|
+
decoded: true,
|
|
496
|
+
args: { lamports: lamports.toString() },
|
|
497
|
+
accountNames: ["from", "to"],
|
|
498
|
+
effects: [{ kind: "sol-transfer", from, to, lamports }]
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
case 3: {
|
|
502
|
+
const base = r.pubkey("base");
|
|
503
|
+
const seedLen = Number(r.u64("seed length"));
|
|
504
|
+
const seed = new TextDecoder().decode(r.bytes_(seedLen, "seed"));
|
|
505
|
+
const lamports = r.u64("lamports");
|
|
506
|
+
const space = r.u64("space");
|
|
507
|
+
const owner = r.pubkey("owner");
|
|
508
|
+
const newAccount = acc(ix, 1) ?? "";
|
|
509
|
+
return {
|
|
510
|
+
program: "System",
|
|
511
|
+
type: "createAccountWithSeed",
|
|
512
|
+
decoded: true,
|
|
513
|
+
args: { base, seed, lamports: lamports.toString(), space: space.toString(), owner },
|
|
514
|
+
accountNames: ["funding", "new", "base"],
|
|
515
|
+
effects: [
|
|
516
|
+
{
|
|
517
|
+
kind: "account-created",
|
|
518
|
+
address: newAccount,
|
|
519
|
+
owner,
|
|
520
|
+
lamports,
|
|
521
|
+
space: Number(space),
|
|
522
|
+
as: "system"
|
|
523
|
+
}
|
|
524
|
+
]
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
case 8: {
|
|
528
|
+
const space = r.u64("space");
|
|
529
|
+
return {
|
|
530
|
+
program: "System",
|
|
531
|
+
type: "allocate",
|
|
532
|
+
decoded: true,
|
|
533
|
+
args: { space: space.toString() },
|
|
534
|
+
accountNames: ["account"]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
case 9: {
|
|
538
|
+
const base = r.pubkey("base");
|
|
539
|
+
const seedLen = Number(r.u64("seed length"));
|
|
540
|
+
const seed = new TextDecoder().decode(r.bytes_(seedLen, "seed"));
|
|
541
|
+
const space = r.u64("space");
|
|
542
|
+
const owner = r.pubkey("owner");
|
|
543
|
+
return {
|
|
544
|
+
program: "System",
|
|
545
|
+
type: "allocateWithSeed",
|
|
546
|
+
decoded: true,
|
|
547
|
+
args: { base, seed, space: space.toString(), owner },
|
|
548
|
+
accountNames: ["account", "base"]
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
case 10: {
|
|
552
|
+
const base = r.pubkey("base");
|
|
553
|
+
const seedLen = Number(r.u64("seed length"));
|
|
554
|
+
const seed = new TextDecoder().decode(r.bytes_(seedLen, "seed"));
|
|
555
|
+
const owner = r.pubkey("owner");
|
|
556
|
+
return {
|
|
557
|
+
program: "System",
|
|
558
|
+
type: "assignWithSeed",
|
|
559
|
+
decoded: true,
|
|
560
|
+
args: { base, seed, owner },
|
|
561
|
+
accountNames: ["account", "base"]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
case 11: {
|
|
565
|
+
const lamports = r.u64("lamports");
|
|
566
|
+
const seedLen = Number(r.u64("seed length"));
|
|
567
|
+
const fromSeed = new TextDecoder().decode(r.bytes_(seedLen, "fromSeed"));
|
|
568
|
+
const fromOwner = r.pubkey("fromOwner");
|
|
569
|
+
const from = acc(ix, 0) ?? "";
|
|
570
|
+
const to = acc(ix, 2) ?? "";
|
|
571
|
+
return {
|
|
572
|
+
program: "System",
|
|
573
|
+
type: "transferWithSeed",
|
|
574
|
+
decoded: true,
|
|
575
|
+
args: { lamports: lamports.toString(), fromSeed, fromOwner },
|
|
576
|
+
accountNames: ["from", "base", "to"],
|
|
577
|
+
effects: [{ kind: "sol-transfer", from, to, lamports }]
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
default: {
|
|
581
|
+
const names = {
|
|
582
|
+
4: "advanceNonceAccount",
|
|
583
|
+
5: "withdrawNonceAccount",
|
|
584
|
+
6: "initializeNonceAccount",
|
|
585
|
+
7: "authorizeNonceAccount"
|
|
586
|
+
};
|
|
587
|
+
return {
|
|
588
|
+
program: "System",
|
|
589
|
+
type: names[disc] ?? `unknown(${disc})`,
|
|
590
|
+
decoded: disc in names
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch (err) {
|
|
595
|
+
return {
|
|
596
|
+
program: "System",
|
|
597
|
+
decoded: false,
|
|
598
|
+
warning: err instanceof Error ? err.message : "system decode failed",
|
|
599
|
+
warningCode: "partial-decode"
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
var systemDecoder = {
|
|
604
|
+
programId: SYSTEM_PROGRAM_ID,
|
|
605
|
+
name: "System",
|
|
606
|
+
kind: "system",
|
|
607
|
+
decode: decodeSystem
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// src/decode/programs/spl-token.ts
|
|
611
|
+
var MAX_U64 = (1n << 64n) - 1n;
|
|
612
|
+
function acc2(ix, i) {
|
|
613
|
+
return ix.accounts[i]?.pubkey;
|
|
614
|
+
}
|
|
615
|
+
function decodeTokenInstruction(ix, programName, variant) {
|
|
616
|
+
if (ix.data.length < 1) {
|
|
617
|
+
return { decoded: false, program: programName, warning: "empty instruction data" };
|
|
618
|
+
}
|
|
619
|
+
const r = new Reader(ix.data);
|
|
620
|
+
const tag = r.u8("tag");
|
|
621
|
+
try {
|
|
622
|
+
switch (tag) {
|
|
623
|
+
case 0: {
|
|
624
|
+
const decimals = r.u8("decimals");
|
|
625
|
+
return {
|
|
626
|
+
program: programName,
|
|
627
|
+
type: "initializeMint",
|
|
628
|
+
decoded: true,
|
|
629
|
+
args: { decimals },
|
|
630
|
+
accountNames: ["mint", "rent"]
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
case 20: {
|
|
634
|
+
const decimals = r.u8("decimals");
|
|
635
|
+
return {
|
|
636
|
+
program: programName,
|
|
637
|
+
type: "initializeMint2",
|
|
638
|
+
decoded: true,
|
|
639
|
+
args: { decimals },
|
|
640
|
+
accountNames: ["mint"]
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
case 1:
|
|
644
|
+
case 16:
|
|
645
|
+
case 18: {
|
|
646
|
+
const account = acc2(ix, 0) ?? "";
|
|
647
|
+
const mint = acc2(ix, 1) ?? "";
|
|
648
|
+
const owner = acc2(ix, 2);
|
|
649
|
+
const type = tag === 1 ? "initializeAccount" : tag === 16 ? "initializeAccount2" : "initializeAccount3";
|
|
650
|
+
return {
|
|
651
|
+
program: programName,
|
|
652
|
+
type,
|
|
653
|
+
decoded: true,
|
|
654
|
+
accountNames: ["account", "mint", "owner"],
|
|
655
|
+
effects: [
|
|
656
|
+
{
|
|
657
|
+
kind: "account-created",
|
|
658
|
+
address: account,
|
|
659
|
+
owner: owner ?? mint,
|
|
660
|
+
as: "token-account"
|
|
661
|
+
}
|
|
662
|
+
]
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
case 3: {
|
|
666
|
+
const amount = r.u64("amount");
|
|
667
|
+
const from = acc2(ix, 0) ?? "";
|
|
668
|
+
const to = acc2(ix, 1) ?? "";
|
|
669
|
+
return {
|
|
670
|
+
program: programName,
|
|
671
|
+
type: "transfer",
|
|
672
|
+
decoded: true,
|
|
673
|
+
args: { amount: amount.toString() },
|
|
674
|
+
accountNames: ["source", "destination", "authority"],
|
|
675
|
+
// No decimals/mint in unchecked transfer — correlate fills from diff.
|
|
676
|
+
effects: [{ kind: "token-transfer", from, to, amount, tokenProgram: variant }],
|
|
677
|
+
warning: "unchecked transfer carries no mint/decimals",
|
|
678
|
+
warningCode: "ambiguous-amount"
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
case 12: {
|
|
682
|
+
const amount = r.u64("amount");
|
|
683
|
+
const decimals = r.u8("decimals");
|
|
684
|
+
const source = acc2(ix, 0) ?? "";
|
|
685
|
+
const mint = acc2(ix, 1) ?? "";
|
|
686
|
+
const dest = acc2(ix, 2) ?? "";
|
|
687
|
+
return {
|
|
688
|
+
program: programName,
|
|
689
|
+
type: "transferChecked",
|
|
690
|
+
decoded: true,
|
|
691
|
+
args: { amount: amount.toString(), decimals },
|
|
692
|
+
accountNames: ["source", "mint", "destination", "authority"],
|
|
693
|
+
effects: [
|
|
694
|
+
{ kind: "token-transfer", from: source, to: dest, mint, amount, decimals, tokenProgram: variant }
|
|
695
|
+
]
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
case 4: {
|
|
699
|
+
const amount = r.u64("amount");
|
|
700
|
+
const delegate = acc2(ix, 1) ?? "";
|
|
701
|
+
const owner = acc2(ix, 2) ?? "";
|
|
702
|
+
return {
|
|
703
|
+
program: programName,
|
|
704
|
+
type: "approve",
|
|
705
|
+
decoded: true,
|
|
706
|
+
args: { amount: amount.toString() },
|
|
707
|
+
accountNames: ["source", "delegate", "owner"],
|
|
708
|
+
effects: [
|
|
709
|
+
{
|
|
710
|
+
kind: "approval",
|
|
711
|
+
owner,
|
|
712
|
+
delegate,
|
|
713
|
+
mint: "",
|
|
714
|
+
amount: amount === MAX_U64 ? "unlimited" : amount
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
case 13: {
|
|
720
|
+
const amount = r.u64("amount");
|
|
721
|
+
const decimals = r.u8("decimals");
|
|
722
|
+
const mint = acc2(ix, 1) ?? "";
|
|
723
|
+
const delegate = acc2(ix, 2) ?? "";
|
|
724
|
+
const owner = acc2(ix, 3) ?? "";
|
|
725
|
+
return {
|
|
726
|
+
program: programName,
|
|
727
|
+
type: "approveChecked",
|
|
728
|
+
decoded: true,
|
|
729
|
+
args: { amount: amount.toString(), decimals },
|
|
730
|
+
accountNames: ["source", "mint", "delegate", "owner"],
|
|
731
|
+
effects: [
|
|
732
|
+
{
|
|
733
|
+
kind: "approval",
|
|
734
|
+
owner,
|
|
735
|
+
delegate,
|
|
736
|
+
mint,
|
|
737
|
+
amount: amount === MAX_U64 ? "unlimited" : amount
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
case 5: {
|
|
743
|
+
const owner = acc2(ix, 1) ?? "";
|
|
744
|
+
return {
|
|
745
|
+
program: programName,
|
|
746
|
+
type: "revoke",
|
|
747
|
+
decoded: true,
|
|
748
|
+
accountNames: ["source", "owner"],
|
|
749
|
+
effects: [{ kind: "approval", owner, delegate: "", mint: "", amount: 0n, revoke: true }]
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
case 6: {
|
|
753
|
+
return {
|
|
754
|
+
program: programName,
|
|
755
|
+
type: "setAuthority",
|
|
756
|
+
decoded: true,
|
|
757
|
+
accountNames: ["account", "currentAuthority"]
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
case 7: {
|
|
761
|
+
const amount = r.u64("amount");
|
|
762
|
+
const mint = acc2(ix, 0) ?? "";
|
|
763
|
+
const to = acc2(ix, 1) ?? "";
|
|
764
|
+
return {
|
|
765
|
+
program: programName,
|
|
766
|
+
type: "mintTo",
|
|
767
|
+
decoded: true,
|
|
768
|
+
args: { amount: amount.toString() },
|
|
769
|
+
accountNames: ["mint", "destination", "authority"],
|
|
770
|
+
effects: [{ kind: "mint", mint, to, amount }]
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
case 14: {
|
|
774
|
+
const amount = r.u64("amount");
|
|
775
|
+
const decimals = r.u8("decimals");
|
|
776
|
+
const mint = acc2(ix, 0) ?? "";
|
|
777
|
+
const to = acc2(ix, 1) ?? "";
|
|
778
|
+
return {
|
|
779
|
+
program: programName,
|
|
780
|
+
type: "mintToChecked",
|
|
781
|
+
decoded: true,
|
|
782
|
+
args: { amount: amount.toString(), decimals },
|
|
783
|
+
accountNames: ["mint", "destination", "authority"],
|
|
784
|
+
effects: [{ kind: "mint", mint, to, amount, decimals }]
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
case 8: {
|
|
788
|
+
const amount = r.u64("amount");
|
|
789
|
+
const from = acc2(ix, 0) ?? "";
|
|
790
|
+
const mint = acc2(ix, 1) ?? "";
|
|
791
|
+
return {
|
|
792
|
+
program: programName,
|
|
793
|
+
type: "burn",
|
|
794
|
+
decoded: true,
|
|
795
|
+
args: { amount: amount.toString() },
|
|
796
|
+
accountNames: ["account", "mint", "authority"],
|
|
797
|
+
effects: [{ kind: "burn", mint, from, amount }]
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
case 15: {
|
|
801
|
+
const amount = r.u64("amount");
|
|
802
|
+
const decimals = r.u8("decimals");
|
|
803
|
+
const from = acc2(ix, 0) ?? "";
|
|
804
|
+
const mint = acc2(ix, 1) ?? "";
|
|
805
|
+
return {
|
|
806
|
+
program: programName,
|
|
807
|
+
type: "burnChecked",
|
|
808
|
+
decoded: true,
|
|
809
|
+
args: { amount: amount.toString(), decimals },
|
|
810
|
+
accountNames: ["account", "mint", "authority"],
|
|
811
|
+
effects: [{ kind: "burn", mint, from, amount, decimals }]
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
case 9: {
|
|
815
|
+
const account = acc2(ix, 0) ?? "";
|
|
816
|
+
const destination = acc2(ix, 1) ?? "";
|
|
817
|
+
return {
|
|
818
|
+
program: programName,
|
|
819
|
+
type: "closeAccount",
|
|
820
|
+
decoded: true,
|
|
821
|
+
accountNames: ["account", "destination", "owner"],
|
|
822
|
+
effects: [{ kind: "close-account", account, destination }]
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
case 10:
|
|
826
|
+
return {
|
|
827
|
+
program: programName,
|
|
828
|
+
type: "freezeAccount",
|
|
829
|
+
decoded: true,
|
|
830
|
+
accountNames: ["account", "mint", "authority"]
|
|
831
|
+
};
|
|
832
|
+
case 11:
|
|
833
|
+
return {
|
|
834
|
+
program: programName,
|
|
835
|
+
type: "thawAccount",
|
|
836
|
+
decoded: true,
|
|
837
|
+
accountNames: ["account", "mint", "authority"]
|
|
838
|
+
};
|
|
839
|
+
case 17: {
|
|
840
|
+
const account = acc2(ix, 0) ?? "";
|
|
841
|
+
return {
|
|
842
|
+
program: programName,
|
|
843
|
+
type: "syncNative",
|
|
844
|
+
decoded: true,
|
|
845
|
+
accountNames: ["account"],
|
|
846
|
+
effects: [{ kind: "sync-native", account }]
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
default:
|
|
850
|
+
return {
|
|
851
|
+
program: programName,
|
|
852
|
+
type: `unknown(${tag})`,
|
|
853
|
+
decoded: false,
|
|
854
|
+
warning: `unrecognized ${programName} instruction tag ${tag}`,
|
|
855
|
+
warningCode: "partial-decode"
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
} catch (err) {
|
|
859
|
+
return {
|
|
860
|
+
program: programName,
|
|
861
|
+
decoded: false,
|
|
862
|
+
warning: err instanceof Error ? err.message : "token decode failed",
|
|
863
|
+
warningCode: "partial-decode"
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/decode/programs/token-2022.ts
|
|
869
|
+
var EXTENSION_TAGS = {
|
|
870
|
+
21: "getAccountDataSize",
|
|
871
|
+
22: "initializeMintCloseAuthority",
|
|
872
|
+
23: "transferFeeExtension",
|
|
873
|
+
24: "confidentialTransferExtension",
|
|
874
|
+
25: "defaultAccountStateExtension",
|
|
875
|
+
26: "reallocate",
|
|
876
|
+
27: "memoTransferExtension",
|
|
877
|
+
28: "createNativeMint",
|
|
878
|
+
29: "initializeNonTransferableMint",
|
|
879
|
+
30: "interestBearingMintExtension",
|
|
880
|
+
31: "cpiGuardExtension",
|
|
881
|
+
32: "initializePermanentDelegate",
|
|
882
|
+
33: "transferHookExtension",
|
|
883
|
+
34: "confidentialTransferFeeExtension",
|
|
884
|
+
35: "withdrawExcessLamports",
|
|
885
|
+
36: "metadataPointerExtension",
|
|
886
|
+
37: "groupPointerExtension",
|
|
887
|
+
38: "groupMemberPointerExtension"
|
|
888
|
+
};
|
|
889
|
+
function decodeToken2022(ix) {
|
|
890
|
+
if (ix.data.length < 1) {
|
|
891
|
+
return { decoded: false, program: "Token-2022", warning: "empty instruction data" };
|
|
892
|
+
}
|
|
893
|
+
const tag = ix.data[0];
|
|
894
|
+
if (tag >= 21) {
|
|
895
|
+
const name = EXTENSION_TAGS[tag] ?? `extension(${tag})`;
|
|
896
|
+
return {
|
|
897
|
+
program: "Token-2022",
|
|
898
|
+
type: name,
|
|
899
|
+
decoded: false,
|
|
900
|
+
warning: `Token-2022 extension instruction "${name}" not byte-decoded; effect visible in balance diff`,
|
|
901
|
+
warningCode: "token-2022-extension-unparsed"
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
return decodeTokenInstruction(ix, "Token-2022", "token-2022");
|
|
905
|
+
}
|
|
906
|
+
var token2022Decoder = {
|
|
907
|
+
programId: TOKEN_2022_PROGRAM_ID,
|
|
908
|
+
name: "Token-2022",
|
|
909
|
+
kind: "token-2022",
|
|
910
|
+
decode: decodeToken2022
|
|
911
|
+
};
|
|
912
|
+
var splTokenDecoder = {
|
|
913
|
+
programId: TOKEN_PROGRAM_ID,
|
|
914
|
+
name: "SPL Token",
|
|
915
|
+
kind: "token",
|
|
916
|
+
decode: (ix) => decodeTokenInstruction(ix, "SPL Token", "spl-token")
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/decode/programs/ata.ts
|
|
920
|
+
function acc3(ix, i) {
|
|
921
|
+
return ix.accounts[i]?.pubkey;
|
|
922
|
+
}
|
|
923
|
+
function decodeAta(ix) {
|
|
924
|
+
const tag = ix.data.length === 0 ? 0 : ix.data[0];
|
|
925
|
+
const type = tag === 0 ? "create" : tag === 1 ? "createIdempotent" : tag === 2 ? "recoverNested" : void 0;
|
|
926
|
+
if (type === void 0) {
|
|
927
|
+
return {
|
|
928
|
+
program: "Associated Token Account",
|
|
929
|
+
decoded: false,
|
|
930
|
+
warning: `unrecognized ATA instruction tag ${tag}`,
|
|
931
|
+
warningCode: "partial-decode"
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (type === "recoverNested") {
|
|
935
|
+
return {
|
|
936
|
+
program: "Associated Token Account",
|
|
937
|
+
type,
|
|
938
|
+
decoded: true,
|
|
939
|
+
accountNames: ["nestedAta", "nestedMint", "destinationAta", "ownerAta", "ownerMint", "wallet"]
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const ata = acc3(ix, 1) ?? "";
|
|
943
|
+
const owner = acc3(ix, 2) ?? "";
|
|
944
|
+
const tokenProgram = acc3(ix, 5);
|
|
945
|
+
return {
|
|
946
|
+
program: "Associated Token Account",
|
|
947
|
+
type,
|
|
948
|
+
decoded: true,
|
|
949
|
+
accountNames: ["payer", "ata", "owner", "mint", "systemProgram", "tokenProgram"],
|
|
950
|
+
effects: [
|
|
951
|
+
{
|
|
952
|
+
kind: "account-created",
|
|
953
|
+
address: ata,
|
|
954
|
+
owner: owner || (tokenProgram ?? ""),
|
|
955
|
+
as: "ata"
|
|
956
|
+
}
|
|
957
|
+
]
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
var ataDecoder = {
|
|
961
|
+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
962
|
+
name: "Associated Token Account",
|
|
963
|
+
kind: "ata",
|
|
964
|
+
decode: decodeAta
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
// src/decode/programs/memo.ts
|
|
968
|
+
var MAX_DISPLAY = 256;
|
|
969
|
+
function decodeMemoData(ix, name) {
|
|
970
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(ix.data);
|
|
971
|
+
const display = text.length > MAX_DISPLAY ? `${text.slice(0, MAX_DISPLAY)}\u2026` : text;
|
|
972
|
+
const truncated = text.length > MAX_DISPLAY;
|
|
973
|
+
return {
|
|
974
|
+
program: name,
|
|
975
|
+
type: "memo",
|
|
976
|
+
decoded: true,
|
|
977
|
+
args: { text: display, ...truncated ? { truncated: true } : {} },
|
|
978
|
+
effects: [{ kind: "memo", text: display }],
|
|
979
|
+
...truncated ? { warning: "memo truncated for display; full bytes available via --raw", warningCode: "partial-decode" } : {}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
var memoDecoder = {
|
|
983
|
+
programId: MEMO_PROGRAM_ID,
|
|
984
|
+
name: "Memo",
|
|
985
|
+
kind: "memo",
|
|
986
|
+
decode: (ix) => decodeMemoData(ix, "Memo")
|
|
987
|
+
};
|
|
988
|
+
var memoV1Decoder = {
|
|
989
|
+
programId: MEMO_V1_PROGRAM_ID,
|
|
990
|
+
name: "Memo (v1)",
|
|
991
|
+
kind: "memo",
|
|
992
|
+
decode: (ix) => decodeMemoData(ix, "Memo (v1)")
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// src/decode/programs/compute-budget.ts
|
|
996
|
+
function decodeComputeBudget(ix) {
|
|
997
|
+
if (ix.data.length < 1) {
|
|
998
|
+
return { decoded: false, program: "Compute Budget", warning: "empty instruction data" };
|
|
999
|
+
}
|
|
1000
|
+
const r = new Reader(ix.data);
|
|
1001
|
+
const tag = r.u8("tag");
|
|
1002
|
+
try {
|
|
1003
|
+
switch (tag) {
|
|
1004
|
+
case 1: {
|
|
1005
|
+
const bytes = r.u32("heap bytes");
|
|
1006
|
+
return {
|
|
1007
|
+
program: "Compute Budget",
|
|
1008
|
+
type: "requestHeapFrame",
|
|
1009
|
+
decoded: true,
|
|
1010
|
+
args: { bytes }
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
case 2: {
|
|
1014
|
+
const units = r.u32("unit limit");
|
|
1015
|
+
return {
|
|
1016
|
+
program: "Compute Budget",
|
|
1017
|
+
type: "setComputeUnitLimit",
|
|
1018
|
+
decoded: true,
|
|
1019
|
+
args: { units },
|
|
1020
|
+
effects: [{ kind: "compute-budget", unitLimit: units }]
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
case 3: {
|
|
1024
|
+
const price = r.u64("micro-lamports");
|
|
1025
|
+
return {
|
|
1026
|
+
program: "Compute Budget",
|
|
1027
|
+
type: "setComputeUnitPrice",
|
|
1028
|
+
decoded: true,
|
|
1029
|
+
args: { microLamports: price.toString() },
|
|
1030
|
+
effects: [{ kind: "compute-budget", unitPriceMicroLamports: price }]
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
case 4: {
|
|
1034
|
+
const bytes = r.u32("data size limit");
|
|
1035
|
+
return {
|
|
1036
|
+
program: "Compute Budget",
|
|
1037
|
+
type: "setLoadedAccountsDataSizeLimit",
|
|
1038
|
+
decoded: true,
|
|
1039
|
+
args: { bytes }
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
case 0:
|
|
1043
|
+
return {
|
|
1044
|
+
program: "Compute Budget",
|
|
1045
|
+
type: "requestUnits",
|
|
1046
|
+
decoded: true
|
|
1047
|
+
};
|
|
1048
|
+
default:
|
|
1049
|
+
return {
|
|
1050
|
+
program: "Compute Budget",
|
|
1051
|
+
type: `unknown(${tag})`,
|
|
1052
|
+
decoded: false,
|
|
1053
|
+
warningCode: "partial-decode"
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
return {
|
|
1058
|
+
program: "Compute Budget",
|
|
1059
|
+
decoded: false,
|
|
1060
|
+
warning: err instanceof Error ? err.message : "compute-budget decode failed",
|
|
1061
|
+
warningCode: "partial-decode"
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
var computeBudgetDecoder = {
|
|
1066
|
+
programId: COMPUTE_BUDGET_PROGRAM_ID,
|
|
1067
|
+
name: "Compute Budget",
|
|
1068
|
+
kind: "compute-budget",
|
|
1069
|
+
decode: decodeComputeBudget
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// src/decode/programs/recognize.ts
|
|
1073
|
+
var ALREADY_DECODED = /* @__PURE__ */ new Set([
|
|
1074
|
+
"11111111111111111111111111111111",
|
|
1075
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
1076
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
|
|
1077
|
+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
|
1078
|
+
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
|
|
1079
|
+
"Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo",
|
|
1080
|
+
"ComputeBudget111111111111111111111111111111"
|
|
1081
|
+
]);
|
|
1082
|
+
function buildRecognizers() {
|
|
1083
|
+
const decoders = [];
|
|
1084
|
+
for (const [programId, info] of Object.entries(KNOWN_PROGRAMS)) {
|
|
1085
|
+
if (ALREADY_DECODED.has(programId)) continue;
|
|
1086
|
+
decoders.push({
|
|
1087
|
+
programId,
|
|
1088
|
+
name: info.name,
|
|
1089
|
+
kind: info.kind,
|
|
1090
|
+
decode: () => ({
|
|
1091
|
+
program: info.name,
|
|
1092
|
+
decoded: false,
|
|
1093
|
+
// Honest signal: we know *who* but not the exact ix semantics.
|
|
1094
|
+
warning: `${info.name} recognized by program id; semantics inferred from balance diff (no IDL)`
|
|
1095
|
+
})
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
return decoders;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/decode/registry.ts
|
|
1102
|
+
var Registry = class _Registry {
|
|
1103
|
+
map;
|
|
1104
|
+
constructor(decoders) {
|
|
1105
|
+
this.map = /* @__PURE__ */ new Map();
|
|
1106
|
+
for (const d of decoders) this.map.set(d.programId, d);
|
|
1107
|
+
}
|
|
1108
|
+
get(programId) {
|
|
1109
|
+
return this.map.get(programId);
|
|
1110
|
+
}
|
|
1111
|
+
has(programId) {
|
|
1112
|
+
return this.map.has(programId);
|
|
1113
|
+
}
|
|
1114
|
+
list() {
|
|
1115
|
+
return [...this.map.values()];
|
|
1116
|
+
}
|
|
1117
|
+
merge(decoders) {
|
|
1118
|
+
const merged = new Map(this.map);
|
|
1119
|
+
for (const d of decoders) merged.set(d.programId, d);
|
|
1120
|
+
return new _Registry([...merged.values()]);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
var BUNDLED_DECODERS = [
|
|
1124
|
+
systemDecoder,
|
|
1125
|
+
splTokenDecoder,
|
|
1126
|
+
token2022Decoder,
|
|
1127
|
+
ataDecoder,
|
|
1128
|
+
memoDecoder,
|
|
1129
|
+
memoV1Decoder,
|
|
1130
|
+
computeBudgetDecoder
|
|
1131
|
+
];
|
|
1132
|
+
function createRegistry(decoders) {
|
|
1133
|
+
const base = [...BUNDLED_DECODERS, ...buildRecognizers()];
|
|
1134
|
+
const reg = new Registry(base);
|
|
1135
|
+
return decoders && decoders.length > 0 ? reg.merge(decoders) : reg;
|
|
1136
|
+
}
|
|
1137
|
+
var defaultRegistry = createRegistry();
|
|
1138
|
+
function decodeInstruction(ix, registry = defaultRegistry, precomputed) {
|
|
1139
|
+
const decoder = registry.get(ix.programId);
|
|
1140
|
+
const known = knownProgram(ix.programId);
|
|
1141
|
+
const baseAccounts = ix.accounts.map((a) => ({
|
|
1142
|
+
pubkey: a.pubkey,
|
|
1143
|
+
isSigner: a.isSigner,
|
|
1144
|
+
isWritable: a.isWritable
|
|
1145
|
+
}));
|
|
1146
|
+
if (!decoder) {
|
|
1147
|
+
return {
|
|
1148
|
+
index: ix.index,
|
|
1149
|
+
programId: ix.programId,
|
|
1150
|
+
...known ? { program: known.name } : {},
|
|
1151
|
+
decoded: false,
|
|
1152
|
+
accounts: baseAccounts,
|
|
1153
|
+
warning: known ? `${known.name} recognized by program id (no byte decoder)` : "unknown program (no decoder)"
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
let out;
|
|
1157
|
+
try {
|
|
1158
|
+
out = precomputed ?? decoder.decode(ix);
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
return {
|
|
1161
|
+
index: ix.index,
|
|
1162
|
+
programId: ix.programId,
|
|
1163
|
+
program: decoder.name,
|
|
1164
|
+
decoded: false,
|
|
1165
|
+
accounts: baseAccounts,
|
|
1166
|
+
warning: `decoder error: ${err instanceof Error ? err.message : String(err)}`
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
const accounts = baseAccounts.map((a, i) => {
|
|
1170
|
+
const name = out.accountNames?.[i];
|
|
1171
|
+
return name !== void 0 ? { ...a, name } : a;
|
|
1172
|
+
});
|
|
1173
|
+
return {
|
|
1174
|
+
index: ix.index,
|
|
1175
|
+
programId: ix.programId,
|
|
1176
|
+
...out.program !== void 0 ? { program: out.program } : {},
|
|
1177
|
+
...out.type !== void 0 ? { type: out.type } : {},
|
|
1178
|
+
decoded: out.decoded,
|
|
1179
|
+
accounts,
|
|
1180
|
+
...out.args !== void 0 ? { args: out.args } : {},
|
|
1181
|
+
...out.warning !== void 0 ? { warning: out.warning } : {}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function decodeInstructionRaw(ix, registry = defaultRegistry) {
|
|
1185
|
+
const decoder = registry.get(ix.programId);
|
|
1186
|
+
if (!decoder) return { decoded: decodeInstruction(ix, registry), effects: [] };
|
|
1187
|
+
let out;
|
|
1188
|
+
try {
|
|
1189
|
+
out = decoder.decode(ix);
|
|
1190
|
+
} catch {
|
|
1191
|
+
return { decoded: decodeInstruction(ix, registry), effects: [] };
|
|
1192
|
+
}
|
|
1193
|
+
const decoded = decodeInstruction(ix, registry, out);
|
|
1194
|
+
return {
|
|
1195
|
+
decoded,
|
|
1196
|
+
effects: out.effects ?? [],
|
|
1197
|
+
...out.warningCode !== void 0 ? { warningCode: out.warningCode } : {}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/decode/message.ts
|
|
1202
|
+
var VERSION_PREFIX_MASK = 128;
|
|
1203
|
+
function stripSignatures(bytes) {
|
|
1204
|
+
if (bytes.length === 0) {
|
|
1205
|
+
throw new DecodeError("DECODE_FAILED", "empty transaction buffer");
|
|
1206
|
+
}
|
|
1207
|
+
const reader = new Reader(bytes);
|
|
1208
|
+
let sigCount;
|
|
1209
|
+
try {
|
|
1210
|
+
sigCount = reader.compactU16("signature count");
|
|
1211
|
+
} catch {
|
|
1212
|
+
return bytes;
|
|
1213
|
+
}
|
|
1214
|
+
if (sigCount > 0 && sigCount <= 64) {
|
|
1215
|
+
const afterSigs = reader.offset + sigCount * 64;
|
|
1216
|
+
if (afterSigs < bytes.length) {
|
|
1217
|
+
const next = bytes[afterSigs];
|
|
1218
|
+
if (next !== void 0) {
|
|
1219
|
+
const looksVersioned = (next & VERSION_PREFIX_MASK) !== 0;
|
|
1220
|
+
const looksLegacyHeader = next > 0 && next <= 32;
|
|
1221
|
+
if (looksVersioned || looksLegacyHeader) {
|
|
1222
|
+
return bytes.subarray(afterSigs);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return bytes;
|
|
1228
|
+
}
|
|
1229
|
+
function parseMessage(messageBytes) {
|
|
1230
|
+
const r = new Reader(messageBytes);
|
|
1231
|
+
const firstByte = r.u8("version/header byte");
|
|
1232
|
+
let version;
|
|
1233
|
+
let numRequiredSignatures;
|
|
1234
|
+
if ((firstByte & VERSION_PREFIX_MASK) !== 0) {
|
|
1235
|
+
version = firstByte & 127;
|
|
1236
|
+
if (version !== 0) {
|
|
1237
|
+
throw new DecodeError(
|
|
1238
|
+
"DECODE_FAILED",
|
|
1239
|
+
`unsupported transaction message version v${version} (only legacy and v0 are supported)`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
numRequiredSignatures = r.u8("numRequiredSignatures");
|
|
1243
|
+
} else {
|
|
1244
|
+
version = "legacy";
|
|
1245
|
+
numRequiredSignatures = firstByte;
|
|
1246
|
+
}
|
|
1247
|
+
const numReadonlySignedAccounts = r.u8("numReadonlySignedAccounts");
|
|
1248
|
+
const numReadonlyUnsignedAccounts = r.u8("numReadonlyUnsignedAccounts");
|
|
1249
|
+
const keyCount = r.compactU16("account key count");
|
|
1250
|
+
if (keyCount > 256) {
|
|
1251
|
+
throw new DecodeError("DECODE_FAILED", `implausible account key count: ${keyCount}`);
|
|
1252
|
+
}
|
|
1253
|
+
const staticAccountKeys = [];
|
|
1254
|
+
for (let i = 0; i < keyCount; i++) {
|
|
1255
|
+
staticAccountKeys.push(r.pubkey(`account key #${i}`));
|
|
1256
|
+
}
|
|
1257
|
+
const recentBlockhash = r.pubkey("recentBlockhash");
|
|
1258
|
+
const ixCount = r.compactU16("instruction count");
|
|
1259
|
+
if (ixCount > 1024) {
|
|
1260
|
+
throw new DecodeError("DECODE_FAILED", `implausible instruction count: ${ixCount}`);
|
|
1261
|
+
}
|
|
1262
|
+
const compiled = [];
|
|
1263
|
+
for (let i = 0; i < ixCount; i++) {
|
|
1264
|
+
const programIdIndex = r.u8(`instruction #${i} programIdIndex`);
|
|
1265
|
+
const accCount = r.compactU16(`instruction #${i} account count`);
|
|
1266
|
+
const accountIndexes = [];
|
|
1267
|
+
for (let j = 0; j < accCount; j++) {
|
|
1268
|
+
accountIndexes.push(r.u8(`instruction #${i} account index #${j}`));
|
|
1269
|
+
}
|
|
1270
|
+
const dataLen = r.compactU16(`instruction #${i} data length`);
|
|
1271
|
+
const data = r.bytes_(dataLen, `instruction #${i} data`).slice();
|
|
1272
|
+
compiled.push({ programIdIndex, accountIndexes, data });
|
|
1273
|
+
}
|
|
1274
|
+
const addressTableLookups = [];
|
|
1275
|
+
let hasUnresolvedLut = false;
|
|
1276
|
+
if (version === 0) {
|
|
1277
|
+
const lutCount = r.compactU16("address table lookup count");
|
|
1278
|
+
for (let i = 0; i < lutCount; i++) {
|
|
1279
|
+
const accountKey = r.pubkey(`LUT #${i} account key`);
|
|
1280
|
+
const writableCount = r.compactU16(`LUT #${i} writable count`);
|
|
1281
|
+
const writableIndexes = [];
|
|
1282
|
+
for (let j = 0; j < writableCount; j++) {
|
|
1283
|
+
writableIndexes.push(r.u8(`LUT #${i} writable index #${j}`));
|
|
1284
|
+
}
|
|
1285
|
+
const readonlyCount = r.compactU16(`LUT #${i} readonly count`);
|
|
1286
|
+
const readonlyIndexes = [];
|
|
1287
|
+
for (let j = 0; j < readonlyCount; j++) {
|
|
1288
|
+
readonlyIndexes.push(r.u8(`LUT #${i} readonly index #${j}`));
|
|
1289
|
+
}
|
|
1290
|
+
addressTableLookups.push({ accountKey, writableIndexes, readonlyIndexes });
|
|
1291
|
+
if (writableIndexes.length + readonlyIndexes.length > 0) hasUnresolvedLut = true;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
version,
|
|
1296
|
+
staticAccountKeys,
|
|
1297
|
+
header: {
|
|
1298
|
+
numRequiredSignatures,
|
|
1299
|
+
numReadonlySignedAccounts,
|
|
1300
|
+
numReadonlyUnsignedAccounts
|
|
1301
|
+
},
|
|
1302
|
+
recentBlockhash,
|
|
1303
|
+
compiled,
|
|
1304
|
+
addressTableLookups,
|
|
1305
|
+
hasUnresolvedLut
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function buildAccountList(msg, loaded) {
|
|
1309
|
+
const loadedWritable = loaded?.writable ?? [];
|
|
1310
|
+
const loadedReadonly = loaded?.readonly ?? [];
|
|
1311
|
+
const accountKeys = [...msg.staticAccountKeys, ...loadedWritable, ...loadedReadonly];
|
|
1312
|
+
const { numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts } = msg.header;
|
|
1313
|
+
const staticCount = msg.staticAccountKeys.length;
|
|
1314
|
+
const isSigner = [];
|
|
1315
|
+
const isWritable = [];
|
|
1316
|
+
for (let i = 0; i < accountKeys.length; i++) {
|
|
1317
|
+
if (i < staticCount) {
|
|
1318
|
+
const signer = i < numRequiredSignatures;
|
|
1319
|
+
let writable;
|
|
1320
|
+
if (signer) {
|
|
1321
|
+
writable = i < numRequiredSignatures - numReadonlySignedAccounts;
|
|
1322
|
+
} else {
|
|
1323
|
+
const unsignedIndex = i - numRequiredSignatures;
|
|
1324
|
+
const unsignedWritableCount = staticCount - numRequiredSignatures - numReadonlyUnsignedAccounts;
|
|
1325
|
+
writable = unsignedIndex < unsignedWritableCount;
|
|
1326
|
+
}
|
|
1327
|
+
isSigner.push(signer);
|
|
1328
|
+
isWritable.push(writable);
|
|
1329
|
+
} else {
|
|
1330
|
+
const loadedIndex = i - staticCount;
|
|
1331
|
+
isSigner.push(false);
|
|
1332
|
+
isWritable.push(loadedIndex < loadedWritable.length);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return { accountKeys, isSigner, isWritable };
|
|
1336
|
+
}
|
|
1337
|
+
function toInstructionViews(msg, resolved) {
|
|
1338
|
+
const { accountKeys, isSigner, isWritable } = resolved;
|
|
1339
|
+
return msg.compiled.map((ix, index) => {
|
|
1340
|
+
const programId = accountKeys[ix.programIdIndex];
|
|
1341
|
+
if (programId === void 0) {
|
|
1342
|
+
throw new DecodeError(
|
|
1343
|
+
"DECODE_FAILED",
|
|
1344
|
+
`instruction #${index} programIdIndex ${ix.programIdIndex} out of range (have ${accountKeys.length} keys)`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const accounts = ix.accountIndexes.map((ai) => {
|
|
1348
|
+
const pubkey = accountKeys[ai];
|
|
1349
|
+
if (pubkey === void 0) {
|
|
1350
|
+
throw new DecodeError(
|
|
1351
|
+
"DECODE_FAILED",
|
|
1352
|
+
`instruction #${index} references account index ${ai} out of range (have ${accountKeys.length} keys)`
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
pubkey,
|
|
1357
|
+
isSigner: isSigner[ai] ?? false,
|
|
1358
|
+
isWritable: isWritable[ai] ?? false
|
|
1359
|
+
};
|
|
1360
|
+
});
|
|
1361
|
+
return { index, programId, accounts, data: ix.data };
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/input/detect.ts
|
|
1366
|
+
function detectInput(input) {
|
|
1367
|
+
if (Array.isArray(input)) {
|
|
1368
|
+
return { kind: "instruction-set", instructions: validateInstructions(input) };
|
|
1369
|
+
}
|
|
1370
|
+
if (input instanceof Uint8Array) {
|
|
1371
|
+
if (input.length === 0) {
|
|
1372
|
+
throw new InputError("EMPTY_INPUT", "Empty byte input.", {
|
|
1373
|
+
hint: "Pass a signature, a serialized transaction, or pipe data via --stdin."
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
assertWireMessage(input, "raw bytes");
|
|
1377
|
+
return { kind: "tx-bytes", bytes: input, encoding: "base64", ambiguous: false };
|
|
1378
|
+
}
|
|
1379
|
+
const trimmed = input.trim();
|
|
1380
|
+
if (trimmed.length === 0) {
|
|
1381
|
+
throw new InputError("EMPTY_INPUT", "Input is empty or whitespace only.", {
|
|
1382
|
+
hint: "Pass a signature, a serialized transaction, or pipe data via --stdin."
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
1386
|
+
let parsed;
|
|
1387
|
+
try {
|
|
1388
|
+
parsed = JSON.parse(trimmed);
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
throw new InputError("INVALID_INPUT", "Input looks like JSON but failed to parse.", {
|
|
1391
|
+
cause: err
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
const arr = Array.isArray(parsed) ? parsed : null;
|
|
1395
|
+
if (!arr) {
|
|
1396
|
+
throw new InputError(
|
|
1397
|
+
"INVALID_INPUT",
|
|
1398
|
+
"JSON instruction input must be an array of instructions."
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
return { kind: "instruction-set", instructions: validateInstructions(arr) };
|
|
1402
|
+
}
|
|
1403
|
+
if (looksLikeBase58(trimmed) && trimmed.length >= 64 && trimmed.length <= 88) {
|
|
1404
|
+
const decoded = tryDecodeBase58(trimmed);
|
|
1405
|
+
if (decoded && decoded.length === 64) {
|
|
1406
|
+
return { kind: "signature", signature: trimmed };
|
|
1407
|
+
}
|
|
1408
|
+
if (decoded && decoded.length !== 64) ;
|
|
1409
|
+
}
|
|
1410
|
+
const asB64 = tryDecodeBase64(trimmed);
|
|
1411
|
+
const asB58 = tryDecodeBase58(trimmed);
|
|
1412
|
+
const b64Ok = asB64 ? isWireMessage(asB64) : false;
|
|
1413
|
+
const b58Ok = asB58 ? isWireMessage(asB58) : false;
|
|
1414
|
+
if (b64Ok && b58Ok) {
|
|
1415
|
+
return { kind: "tx-bytes", bytes: asB64, encoding: "base64", ambiguous: true };
|
|
1416
|
+
}
|
|
1417
|
+
if (b64Ok) {
|
|
1418
|
+
return { kind: "tx-bytes", bytes: asB64, encoding: "base64", ambiguous: false };
|
|
1419
|
+
}
|
|
1420
|
+
if (b58Ok) {
|
|
1421
|
+
return { kind: "tx-bytes", bytes: asB58, encoding: "base58", ambiguous: false };
|
|
1422
|
+
}
|
|
1423
|
+
if (looksLikeBase58(trimmed)) {
|
|
1424
|
+
const decoded = tryDecodeBase58(trimmed);
|
|
1425
|
+
if (decoded) {
|
|
1426
|
+
throw new InputError(
|
|
1427
|
+
"INVALID_SIGNATURE",
|
|
1428
|
+
`Input decoded as ${decoded.length} bytes; a signature must be exactly 64 bytes, and it is not a valid transaction.`,
|
|
1429
|
+
{ hint: "Provide a 64-byte base58 signature or a base64/base58 serialized transaction." }
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
throw new InputError(
|
|
1434
|
+
"INVALID_INPUT",
|
|
1435
|
+
"Could not classify input as a signature, transaction, or instruction set.",
|
|
1436
|
+
{ hint: "Provide a base58 signature, a base64/base58 transaction, or a JSON instruction array." }
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
function detectTxBytes(input, encoding = "auto") {
|
|
1440
|
+
const trimmed = input.trim();
|
|
1441
|
+
if (trimmed.length === 0) {
|
|
1442
|
+
throw new InputError("EMPTY_INPUT", "Empty transaction input.");
|
|
1443
|
+
}
|
|
1444
|
+
if (encoding === "base64") {
|
|
1445
|
+
const bytes = tryDecodeBase64(trimmed);
|
|
1446
|
+
if (!bytes) throw new InputError("INVALID_ENCODING", "Input is not valid base64.");
|
|
1447
|
+
assertWireMessage(bytes, "base64 transaction");
|
|
1448
|
+
return { bytes, encoding: "base64", ambiguous: false };
|
|
1449
|
+
}
|
|
1450
|
+
if (encoding === "base58") {
|
|
1451
|
+
const bytes = tryDecodeBase58(trimmed);
|
|
1452
|
+
if (!bytes) throw new InputError("INVALID_ENCODING", "Input is not valid base58.");
|
|
1453
|
+
assertWireMessage(bytes, "base58 transaction");
|
|
1454
|
+
return { bytes, encoding: "base58", ambiguous: false };
|
|
1455
|
+
}
|
|
1456
|
+
const asB64 = tryDecodeBase64(trimmed);
|
|
1457
|
+
const asB58 = tryDecodeBase58(trimmed);
|
|
1458
|
+
const b64Ok = asB64 ? isWireMessage(asB64) : false;
|
|
1459
|
+
const b58Ok = asB58 ? isWireMessage(asB58) : false;
|
|
1460
|
+
if (b64Ok) return { bytes: asB64, encoding: "base64", ambiguous: b58Ok };
|
|
1461
|
+
if (b58Ok) return { bytes: asB58, encoding: "base58", ambiguous: false };
|
|
1462
|
+
throw new InputError("DECODE_FAILED", "Input did not parse as a serialized transaction.");
|
|
1463
|
+
}
|
|
1464
|
+
function isWireMessage(bytes) {
|
|
1465
|
+
try {
|
|
1466
|
+
const stripped = stripSignatures(bytes);
|
|
1467
|
+
const msg = parseMessage(stripped);
|
|
1468
|
+
return msg.staticAccountKeys.length > 0 && msg.compiled.length >= 0;
|
|
1469
|
+
} catch {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function assertWireMessage(bytes, label) {
|
|
1474
|
+
if (!isWireMessage(bytes)) {
|
|
1475
|
+
throw new InputError(
|
|
1476
|
+
"DECODE_FAILED",
|
|
1477
|
+
`${label} did not parse as a Solana wire transaction message.`,
|
|
1478
|
+
{ hint: "Ensure the input is a serialized (not deserialized) transaction." }
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function validateInstructions(arr) {
|
|
1483
|
+
if (arr.length === 0) {
|
|
1484
|
+
throw new InputError("EMPTY_INPUT", "Instruction set is empty.");
|
|
1485
|
+
}
|
|
1486
|
+
return arr.map((raw, i) => {
|
|
1487
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1488
|
+
throw new InputError("INVALID_INPUT", `Instruction #${i} is not an object.`);
|
|
1489
|
+
}
|
|
1490
|
+
const o = raw;
|
|
1491
|
+
if (typeof o["programId"] !== "string") {
|
|
1492
|
+
throw new InputError("INVALID_INPUT", `Instruction #${i} is missing a string "programId".`);
|
|
1493
|
+
}
|
|
1494
|
+
const accountsRaw = o["accounts"];
|
|
1495
|
+
if (!Array.isArray(accountsRaw)) {
|
|
1496
|
+
throw new InputError("INVALID_INPUT", `Instruction #${i} "accounts" must be an array.`);
|
|
1497
|
+
}
|
|
1498
|
+
const accounts = accountsRaw.map((a, j) => {
|
|
1499
|
+
if (typeof a !== "object" || a === null) {
|
|
1500
|
+
throw new InputError("INVALID_INPUT", `Instruction #${i} account #${j} is not an object.`);
|
|
1501
|
+
}
|
|
1502
|
+
const ao = a;
|
|
1503
|
+
if (typeof ao["pubkey"] !== "string") {
|
|
1504
|
+
throw new InputError(
|
|
1505
|
+
"INVALID_INPUT",
|
|
1506
|
+
`Instruction #${i} account #${j} is missing a string "pubkey".`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
pubkey: ao["pubkey"],
|
|
1511
|
+
isSigner: Boolean(ao["isSigner"]),
|
|
1512
|
+
isWritable: Boolean(ao["isWritable"])
|
|
1513
|
+
};
|
|
1514
|
+
});
|
|
1515
|
+
const data = o["data"];
|
|
1516
|
+
if (typeof data !== "string" && !Array.isArray(data) && !(data instanceof Uint8Array)) {
|
|
1517
|
+
throw new InputError(
|
|
1518
|
+
"INVALID_INPUT",
|
|
1519
|
+
`Instruction #${i} "data" must be a base64/base58 string, byte array, or Uint8Array.`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
programId: o["programId"],
|
|
1524
|
+
accounts,
|
|
1525
|
+
data
|
|
1526
|
+
};
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/explain/token-meta.ts
|
|
1531
|
+
var KNOWN_MINTS = Object.freeze({
|
|
1532
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { symbol: "USDC", decimals: 6 },
|
|
1533
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: { symbol: "USDT", decimals: 6 },
|
|
1534
|
+
So11111111111111111111111111111111111111112: { symbol: "wSOL", decimals: 9 },
|
|
1535
|
+
mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So: { symbol: "mSOL", decimals: 9 },
|
|
1536
|
+
"7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj": {
|
|
1537
|
+
symbol: "stSOL",
|
|
1538
|
+
decimals: 9
|
|
1539
|
+
},
|
|
1540
|
+
"4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R": {
|
|
1541
|
+
symbol: "RAY",
|
|
1542
|
+
decimals: 6
|
|
1543
|
+
},
|
|
1544
|
+
JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN: { symbol: "JUP", decimals: 6 },
|
|
1545
|
+
DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263: { symbol: "BONK", decimals: 5 }
|
|
1546
|
+
});
|
|
1547
|
+
var WSOL_MINT = "So11111111111111111111111111111111111111112";
|
|
1548
|
+
function resolveSymbol(mint) {
|
|
1549
|
+
return KNOWN_MINTS[mint]?.symbol;
|
|
1550
|
+
}
|
|
1551
|
+
function isWsol(mint) {
|
|
1552
|
+
return mint === WSOL_MINT;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/acquire/normalize.ts
|
|
1556
|
+
function tokenProgramKey(programId) {
|
|
1557
|
+
if (programId === TOKEN_PROGRAM_ID) return "spl-token";
|
|
1558
|
+
if (programId === TOKEN_2022_PROGRAM_ID) return "token-2022";
|
|
1559
|
+
return programId ?? "spl-token";
|
|
1560
|
+
}
|
|
1561
|
+
function tokenBalancesFromMeta(entries, accountKeys) {
|
|
1562
|
+
if (!entries) return [];
|
|
1563
|
+
const out = [];
|
|
1564
|
+
for (const e of entries) {
|
|
1565
|
+
const account = accountKeys[e.accountIndex];
|
|
1566
|
+
if (account === void 0) continue;
|
|
1567
|
+
const amount = BigInt(e.uiTokenAmount.amount);
|
|
1568
|
+
const symbol = resolveSymbol(e.mint);
|
|
1569
|
+
out.push({
|
|
1570
|
+
account,
|
|
1571
|
+
mint: e.mint,
|
|
1572
|
+
...e.owner !== void 0 ? { owner: e.owner } : {},
|
|
1573
|
+
amount,
|
|
1574
|
+
decimals: e.uiTokenAmount.decimals,
|
|
1575
|
+
tokenProgram: tokenProgramKey(e.programId),
|
|
1576
|
+
...symbol !== void 0 ? { symbol } : {}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
return out;
|
|
1580
|
+
}
|
|
1581
|
+
function tokenBalanceFromAccount(address, account) {
|
|
1582
|
+
if (!account) return null;
|
|
1583
|
+
const data = account.data;
|
|
1584
|
+
if (typeof data !== "object" || data === null || !("parsed" in data) || !("program" in data)) {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
const program = data.program;
|
|
1588
|
+
const parsed = data.parsed;
|
|
1589
|
+
if (program !== "spl-token" && program !== "spl-token-2022" || typeof parsed !== "object" || parsed === null) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const info = parsed.info;
|
|
1593
|
+
const type = parsed.type;
|
|
1594
|
+
if (type !== "account" || typeof info !== "object" || info === null) return null;
|
|
1595
|
+
const i = info;
|
|
1596
|
+
if (typeof i.mint !== "string" || !i.tokenAmount) return null;
|
|
1597
|
+
const amountStr = i.tokenAmount.amount;
|
|
1598
|
+
const decimals = i.tokenAmount.decimals;
|
|
1599
|
+
if (typeof amountStr !== "string" || typeof decimals !== "number") return null;
|
|
1600
|
+
const symbol = resolveSymbol(i.mint);
|
|
1601
|
+
return {
|
|
1602
|
+
account: address,
|
|
1603
|
+
mint: i.mint,
|
|
1604
|
+
...typeof i.owner === "string" ? { owner: i.owner } : {},
|
|
1605
|
+
amount: BigInt(amountStr),
|
|
1606
|
+
decimals,
|
|
1607
|
+
tokenProgram: program === "spl-token-2022" ? "token-2022" : "spl-token",
|
|
1608
|
+
...symbol !== void 0 ? { symbol } : {}
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function innerFromMeta(groups, accountKeys, decodeData, isSigner, isWritable) {
|
|
1612
|
+
if (!groups) return [];
|
|
1613
|
+
return groups.map((g) => ({
|
|
1614
|
+
index: g.index,
|
|
1615
|
+
instructions: g.instructions.map((ix, i) => {
|
|
1616
|
+
const programId = accountKeys[ix.programIdIndex] ?? "";
|
|
1617
|
+
return {
|
|
1618
|
+
index: i,
|
|
1619
|
+
programId,
|
|
1620
|
+
accounts: ix.accounts.map((ai) => ({
|
|
1621
|
+
pubkey: accountKeys[ai] ?? "",
|
|
1622
|
+
isSigner: isSigner[ai] ?? false,
|
|
1623
|
+
isWritable: isWritable[ai] ?? false
|
|
1624
|
+
})),
|
|
1625
|
+
data: decodeData(ix.data)
|
|
1626
|
+
};
|
|
1627
|
+
})
|
|
1628
|
+
}));
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/acquire/tx-error.ts
|
|
1632
|
+
function humanizeTxError(err, instructions) {
|
|
1633
|
+
return { raw: err, human: describe(err, instructions) };
|
|
1634
|
+
}
|
|
1635
|
+
function describe(err, instructions) {
|
|
1636
|
+
if (err === null || err === void 0) return "unknown error";
|
|
1637
|
+
if (typeof err === "string") return prettifyVariant(err);
|
|
1638
|
+
if (typeof err === "object") {
|
|
1639
|
+
const entries = Object.entries(err);
|
|
1640
|
+
if (entries.length === 0) return "unknown error";
|
|
1641
|
+
const [key, value] = entries[0];
|
|
1642
|
+
if (key === "InstructionError" && Array.isArray(value)) {
|
|
1643
|
+
const [idx, detail] = value;
|
|
1644
|
+
const ix = instructions?.[idx];
|
|
1645
|
+
const progInfo = ix ? knownProgram(ix.programId) : void 0;
|
|
1646
|
+
const where = `instruction #${idx}${progInfo ? ` (${progInfo.name})` : ix ? ` (${ix.programId})` : ""}`;
|
|
1647
|
+
if (typeof detail === "string") {
|
|
1648
|
+
return `${where} failed: ${prettifyVariant(detail)}`;
|
|
1649
|
+
}
|
|
1650
|
+
if (detail && typeof detail === "object") {
|
|
1651
|
+
const dEntries = Object.entries(detail);
|
|
1652
|
+
if (dEntries.length > 0) {
|
|
1653
|
+
const [dKey, dVal] = dEntries[0];
|
|
1654
|
+
if (dKey === "Custom") {
|
|
1655
|
+
return `${where} failed with custom program error ${String(dVal)}`;
|
|
1656
|
+
}
|
|
1657
|
+
return `${where} failed: ${dKey}${dVal !== null ? ` (${JSON.stringify(dVal)})` : ""}`;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return `${where} failed`;
|
|
1661
|
+
}
|
|
1662
|
+
if (key === "InsufficientFundsForRent") {
|
|
1663
|
+
const acc4 = value && typeof value === "object" && "account_index" in value ? ` (account #${String(value.account_index)})` : "";
|
|
1664
|
+
return `insufficient funds for rent${acc4}`;
|
|
1665
|
+
}
|
|
1666
|
+
if (value === null || typeof value === "object" && Object.keys(value).length === 0) {
|
|
1667
|
+
return prettifyVariant(key);
|
|
1668
|
+
}
|
|
1669
|
+
return `${prettifyVariant(key)}: ${safeJson(value)}`;
|
|
1670
|
+
}
|
|
1671
|
+
return String(err);
|
|
1672
|
+
}
|
|
1673
|
+
function prettifyVariant(name) {
|
|
1674
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase();
|
|
1675
|
+
}
|
|
1676
|
+
function safeJson(v) {
|
|
1677
|
+
try {
|
|
1678
|
+
return JSON.stringify(v);
|
|
1679
|
+
} catch {
|
|
1680
|
+
return String(v);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/acquire/from-signature.ts
|
|
1685
|
+
async function acquireFromSignature(args) {
|
|
1686
|
+
const { rpc, signature, commitment, maxSupportedTransactionVersion, signal, includeRaw } = args;
|
|
1687
|
+
const resp = await rpc.getTransaction(signature, {
|
|
1688
|
+
commitment,
|
|
1689
|
+
maxSupportedTransactionVersion,
|
|
1690
|
+
...signal ? { signal } : {}
|
|
1691
|
+
});
|
|
1692
|
+
if (resp === null) {
|
|
1693
|
+
throw new RpcError(
|
|
1694
|
+
"TX_NOT_FOUND",
|
|
1695
|
+
`Transaction ${signature} not found at commitment "${commitment}".`,
|
|
1696
|
+
{
|
|
1697
|
+
hint: "Try --commitment finalized, verify the signature, or check you are on the right cluster."
|
|
1698
|
+
}
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
return normalizeSignatureResponse(resp, signature, includeRaw ?? false);
|
|
1702
|
+
}
|
|
1703
|
+
function normalizeSignatureResponse(resp, signature, includeRaw) {
|
|
1704
|
+
const warnings = [];
|
|
1705
|
+
const meta = resp.meta;
|
|
1706
|
+
const messageBytes = decodeMessageBytes(resp.transaction.message);
|
|
1707
|
+
const parsed = parseMessage(messageBytes);
|
|
1708
|
+
const loaded = meta?.loadedAddresses ? { writable: meta.loadedAddresses.writable, readonly: meta.loadedAddresses.readonly } : void 0;
|
|
1709
|
+
if (parsed.version === 0 && parsed.hasUnresolvedLut && !loaded) {
|
|
1710
|
+
warnings.push({
|
|
1711
|
+
code: "lut-unresolved",
|
|
1712
|
+
message: "versioned tx uses address lookup tables not resolved by the node response"
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
const resolved = buildAccountList(parsed, loaded);
|
|
1716
|
+
const accountKeys = resolved.accountKeys;
|
|
1717
|
+
const instructions = toInstructionViews(parsed, resolved);
|
|
1718
|
+
const feePayer = accountKeys[0] ?? "";
|
|
1719
|
+
const preLamports = (meta?.preBalances ?? []).map((n) => BigInt(n));
|
|
1720
|
+
const postLamports = (meta?.postBalances ?? []).map((n) => BigInt(n));
|
|
1721
|
+
const preTokens = tokenBalancesFromMeta(meta?.preTokenBalances, accountKeys);
|
|
1722
|
+
const postTokens = tokenBalancesFromMeta(meta?.postTokenBalances, accountKeys);
|
|
1723
|
+
const inner = meta?.innerInstructions ? innerFromMeta(
|
|
1724
|
+
meta.innerInstructions,
|
|
1725
|
+
accountKeys,
|
|
1726
|
+
decodeBase58,
|
|
1727
|
+
resolved.isSigner,
|
|
1728
|
+
resolved.isWritable
|
|
1729
|
+
) : [];
|
|
1730
|
+
if (!meta?.innerInstructions) {
|
|
1731
|
+
warnings.push({
|
|
1732
|
+
code: "inner-instructions-missing",
|
|
1733
|
+
message: "node did not return inner instructions (CPI tree unavailable)"
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
const err = meta?.err ?? null;
|
|
1737
|
+
const success = err === null || err === void 0;
|
|
1738
|
+
const error = success ? void 0 : humanizeTxError(err, instructions);
|
|
1739
|
+
const result = {
|
|
1740
|
+
source: "signature",
|
|
1741
|
+
signature,
|
|
1742
|
+
feePayer,
|
|
1743
|
+
success,
|
|
1744
|
+
...error ? { error } : {},
|
|
1745
|
+
slot: resp.slot,
|
|
1746
|
+
blockTime: resp.blockTime,
|
|
1747
|
+
feeLamports: BigInt(meta?.fee ?? 0),
|
|
1748
|
+
...meta?.computeUnitsConsumed !== void 0 ? { computeUnits: meta.computeUnitsConsumed } : {},
|
|
1749
|
+
pre: { accountKeys, lamports: preLamports, tokenBalances: preTokens },
|
|
1750
|
+
post: { accountKeys, lamports: postLamports, tokenBalances: postTokens },
|
|
1751
|
+
instructions,
|
|
1752
|
+
innerInstructions: inner,
|
|
1753
|
+
warnings,
|
|
1754
|
+
...includeRaw ? { raw: resp } : {}
|
|
1755
|
+
};
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
function decodeMessageBytes(message) {
|
|
1759
|
+
if (Array.isArray(message) && typeof message[0] === "string") {
|
|
1760
|
+
const enc = message[1];
|
|
1761
|
+
if (enc === "base64") return decodeBase64(message[0]);
|
|
1762
|
+
if (enc === "base58") return decodeBase58(message[0]);
|
|
1763
|
+
return decodeBase64(message[0]);
|
|
1764
|
+
}
|
|
1765
|
+
if (typeof message === "string") {
|
|
1766
|
+
return decodeBase64(message);
|
|
1767
|
+
}
|
|
1768
|
+
throw new RpcError(
|
|
1769
|
+
"RPC_JSON",
|
|
1770
|
+
"Unexpected transaction.message shape; expected base64-encoded message."
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/acquire/from-simulation.ts
|
|
1775
|
+
function accountLamports(acc4) {
|
|
1776
|
+
return acc4 ? BigInt(acc4.lamports) : 0n;
|
|
1777
|
+
}
|
|
1778
|
+
async function acquireFromSimulation(args) {
|
|
1779
|
+
const { rpc, txBytes, commitment, replaceRecentBlockhash, sigVerify, signal, includeRaw } = args;
|
|
1780
|
+
const stripped = stripSignatures(txBytes);
|
|
1781
|
+
const parsed = parseMessage(stripped);
|
|
1782
|
+
const resolved = buildAccountList(parsed);
|
|
1783
|
+
const accountKeys = resolved.accountKeys;
|
|
1784
|
+
const instructions = toInstructionViews(parsed, resolved);
|
|
1785
|
+
const feePayer = accountKeys[0] ?? "";
|
|
1786
|
+
const warnings = [];
|
|
1787
|
+
if (parsed.version === 0 && parsed.hasUnresolvedLut) {
|
|
1788
|
+
warnings.push({
|
|
1789
|
+
code: "lut-unresolved",
|
|
1790
|
+
message: "versioned tx references address lookup tables; simulate path diffs only resolved writable keys"
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
const writable = accountKeys.filter((_, i) => resolved.isWritable[i]);
|
|
1794
|
+
const uniqueWritable = [...new Set(writable)];
|
|
1795
|
+
const preAccounts = await rpc.getMultipleAccounts(uniqueWritable, {
|
|
1796
|
+
commitment,
|
|
1797
|
+
encoding: "jsonParsed",
|
|
1798
|
+
...signal ? { signal } : {}
|
|
1799
|
+
});
|
|
1800
|
+
const txBase64 = encodeBase64(txBytes);
|
|
1801
|
+
const sim = await rpc.simulateTransaction(txBase64, {
|
|
1802
|
+
commitment,
|
|
1803
|
+
sigVerify,
|
|
1804
|
+
replaceRecentBlockhash,
|
|
1805
|
+
innerInstructions: true,
|
|
1806
|
+
accounts: { addresses: uniqueWritable, encoding: "jsonParsed" },
|
|
1807
|
+
...signal ? { signal } : {}
|
|
1808
|
+
});
|
|
1809
|
+
const value = sim.value;
|
|
1810
|
+
const postAccounts = value.accounts ?? [];
|
|
1811
|
+
const preLamportsByAddr = /* @__PURE__ */ new Map();
|
|
1812
|
+
const postLamportsByAddr = /* @__PURE__ */ new Map();
|
|
1813
|
+
const preTokens = [];
|
|
1814
|
+
const postTokens = [];
|
|
1815
|
+
uniqueWritable.forEach((addr, i) => {
|
|
1816
|
+
const preAcc = preAccounts[i] ?? null;
|
|
1817
|
+
const postAcc = postAccounts[i] ?? null;
|
|
1818
|
+
preLamportsByAddr.set(addr, accountLamports(preAcc));
|
|
1819
|
+
postLamportsByAddr.set(addr, postAcc ? accountLamports(postAcc) : accountLamports(preAcc));
|
|
1820
|
+
const preTb = tokenBalanceFromAccount(addr, preAcc);
|
|
1821
|
+
if (preTb) preTokens.push(preTb);
|
|
1822
|
+
const postTb = tokenBalanceFromAccount(addr, postAcc);
|
|
1823
|
+
if (postTb) postTokens.push(postTb);
|
|
1824
|
+
});
|
|
1825
|
+
const preLamports = accountKeys.map((k) => preLamportsByAddr.get(k) ?? 0n);
|
|
1826
|
+
const postLamports = accountKeys.map((k) => postLamportsByAddr.get(k) ?? preLamportsByAddr.get(k) ?? 0n);
|
|
1827
|
+
const inner = value.innerInstructions ? innerFromMeta(
|
|
1828
|
+
value.innerInstructions,
|
|
1829
|
+
accountKeys,
|
|
1830
|
+
decodeBase58,
|
|
1831
|
+
resolved.isSigner,
|
|
1832
|
+
resolved.isWritable
|
|
1833
|
+
) : [];
|
|
1834
|
+
const err = value.err ?? null;
|
|
1835
|
+
const success = err === null || err === void 0;
|
|
1836
|
+
const error = success ? void 0 : humanizeTxError(err, instructions);
|
|
1837
|
+
if (!success) {
|
|
1838
|
+
warnings.push({
|
|
1839
|
+
code: "simulation-failed",
|
|
1840
|
+
message: "simulation returned a runtime error; balance diff reflects the pre-state"
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
const result = {
|
|
1844
|
+
source: "simulation",
|
|
1845
|
+
feePayer,
|
|
1846
|
+
success,
|
|
1847
|
+
...error ? { error } : {},
|
|
1848
|
+
slot: sim.context.slot,
|
|
1849
|
+
blockTime: null,
|
|
1850
|
+
// Simulation does not surface a fee; fee impact is visible via lamport diff.
|
|
1851
|
+
feeLamports: 0n,
|
|
1852
|
+
...value.unitsConsumed !== void 0 ? { computeUnits: value.unitsConsumed } : {},
|
|
1853
|
+
pre: { accountKeys, lamports: preLamports, tokenBalances: preTokens },
|
|
1854
|
+
post: { accountKeys, lamports: postLamports, tokenBalances: postTokens },
|
|
1855
|
+
instructions,
|
|
1856
|
+
innerInstructions: inner,
|
|
1857
|
+
warnings,
|
|
1858
|
+
...includeRaw ? { raw: sim } : {}
|
|
1859
|
+
};
|
|
1860
|
+
return result;
|
|
1861
|
+
}
|
|
1862
|
+
function normalizeData(data) {
|
|
1863
|
+
if (data instanceof Uint8Array) return data;
|
|
1864
|
+
if (Array.isArray(data)) return Uint8Array.from(data);
|
|
1865
|
+
if (looksLikeBase64(data) && /[+/=]/.test(data)) {
|
|
1866
|
+
return decodeBase64(data);
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
return decodeBase58(data);
|
|
1870
|
+
} catch {
|
|
1871
|
+
return decodeBase64(data);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function writeCompactU16(out, value) {
|
|
1875
|
+
let v = value;
|
|
1876
|
+
for (; ; ) {
|
|
1877
|
+
let byte = v & 127;
|
|
1878
|
+
v >>= 7;
|
|
1879
|
+
if (v === 0) {
|
|
1880
|
+
out.push(byte);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
byte |= 128;
|
|
1884
|
+
out.push(byte);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function buildLegacyMessage(instructions, feePayer, recentBlockhash) {
|
|
1888
|
+
if (instructions.length === 0) {
|
|
1889
|
+
throw new InputError("EMPTY_INPUT", "No instructions to assemble.");
|
|
1890
|
+
}
|
|
1891
|
+
const metaByKey = /* @__PURE__ */ new Map();
|
|
1892
|
+
const upsert = (m) => {
|
|
1893
|
+
const cur = metaByKey.get(m.pubkey);
|
|
1894
|
+
if (cur) {
|
|
1895
|
+
cur.isSigner = cur.isSigner || m.isSigner;
|
|
1896
|
+
cur.isWritable = cur.isWritable || m.isWritable;
|
|
1897
|
+
} else {
|
|
1898
|
+
metaByKey.set(m.pubkey, { ...m });
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
upsert({ pubkey: feePayer, isSigner: true, isWritable: true });
|
|
1902
|
+
for (const ix of instructions) {
|
|
1903
|
+
for (const a of ix.accounts) upsert({ ...a });
|
|
1904
|
+
upsert({ pubkey: ix.programId, isSigner: false, isWritable: false });
|
|
1905
|
+
}
|
|
1906
|
+
const all = [...metaByKey.values()];
|
|
1907
|
+
all.sort((a, b) => {
|
|
1908
|
+
if (a.pubkey === feePayer) return -1;
|
|
1909
|
+
if (b.pubkey === feePayer) return 1;
|
|
1910
|
+
const rank = (m) => m.isSigner ? m.isWritable ? 0 : 1 : m.isWritable ? 2 : 3;
|
|
1911
|
+
const r = rank(a) - rank(b);
|
|
1912
|
+
if (r !== 0) return r;
|
|
1913
|
+
return a.pubkey.localeCompare(b.pubkey);
|
|
1914
|
+
});
|
|
1915
|
+
const numRequiredSignatures = all.filter((m) => m.isSigner).length;
|
|
1916
|
+
const numReadonlySignedAccounts = all.filter((m) => m.isSigner && !m.isWritable).length;
|
|
1917
|
+
const numReadonlyUnsignedAccounts = all.filter((m) => !m.isSigner && !m.isWritable).length;
|
|
1918
|
+
const keyIndex = /* @__PURE__ */ new Map();
|
|
1919
|
+
all.forEach((m, i) => keyIndex.set(m.pubkey, i));
|
|
1920
|
+
const out = [];
|
|
1921
|
+
out.push(numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts);
|
|
1922
|
+
writeCompactU16(out, all.length);
|
|
1923
|
+
for (const m of all) {
|
|
1924
|
+
const bytes = decodePubkey(m.pubkey);
|
|
1925
|
+
out.push(...bytes);
|
|
1926
|
+
}
|
|
1927
|
+
out.push(...decodePubkey(recentBlockhash));
|
|
1928
|
+
writeCompactU16(out, instructions.length);
|
|
1929
|
+
for (const ix of instructions) {
|
|
1930
|
+
const programIdIndex = keyIndex.get(ix.programId);
|
|
1931
|
+
if (programIdIndex === void 0) {
|
|
1932
|
+
throw new InputError("INVALID_INPUT", `programId ${ix.programId} not in account list`);
|
|
1933
|
+
}
|
|
1934
|
+
out.push(programIdIndex);
|
|
1935
|
+
writeCompactU16(out, ix.accounts.length);
|
|
1936
|
+
for (const a of ix.accounts) {
|
|
1937
|
+
const idx = keyIndex.get(a.pubkey);
|
|
1938
|
+
if (idx === void 0) {
|
|
1939
|
+
throw new InputError("INVALID_INPUT", `account ${a.pubkey} not in account list`);
|
|
1940
|
+
}
|
|
1941
|
+
out.push(idx);
|
|
1942
|
+
}
|
|
1943
|
+
const data = normalizeData(ix.data);
|
|
1944
|
+
writeCompactU16(out, data.length);
|
|
1945
|
+
out.push(...data);
|
|
1946
|
+
}
|
|
1947
|
+
const message = Uint8Array.from(out);
|
|
1948
|
+
const tx = [];
|
|
1949
|
+
writeCompactU16(tx, numRequiredSignatures);
|
|
1950
|
+
for (let i = 0; i < numRequiredSignatures; i++) {
|
|
1951
|
+
for (let j = 0; j < 64; j++) tx.push(0);
|
|
1952
|
+
}
|
|
1953
|
+
tx.push(...message);
|
|
1954
|
+
return Uint8Array.from(tx);
|
|
1955
|
+
}
|
|
1956
|
+
function decodePubkey(pubkey) {
|
|
1957
|
+
let bytes;
|
|
1958
|
+
try {
|
|
1959
|
+
bytes = bs58.decode(pubkey);
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
throw new InputError("INVALID_INPUT", `Invalid base58 pubkey: ${pubkey}`, { cause: err });
|
|
1962
|
+
}
|
|
1963
|
+
if (bytes.length !== 32) {
|
|
1964
|
+
throw new InputError(
|
|
1965
|
+
"INVALID_INPUT",
|
|
1966
|
+
`Pubkey ${pubkey} decoded to ${bytes.length} bytes (expected 32).`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
return bytes;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/render/format.ts
|
|
1973
|
+
function groupThousands(intStr) {
|
|
1974
|
+
const neg = intStr.startsWith("-");
|
|
1975
|
+
const digits = neg ? intStr.slice(1) : intStr;
|
|
1976
|
+
const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1977
|
+
return neg ? `-${grouped}` : grouped;
|
|
1978
|
+
}
|
|
1979
|
+
function formatUnits(amount, decimals) {
|
|
1980
|
+
const neg = amount < 0n;
|
|
1981
|
+
const abs = neg ? -amount : amount;
|
|
1982
|
+
if (decimals <= 0) {
|
|
1983
|
+
return (neg ? "-" : "") + groupThousands(abs.toString());
|
|
1984
|
+
}
|
|
1985
|
+
const base = 10n ** BigInt(decimals);
|
|
1986
|
+
const whole = abs / base;
|
|
1987
|
+
const frac = abs % base;
|
|
1988
|
+
let fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
1989
|
+
const wholeStr = groupThousands(whole.toString());
|
|
1990
|
+
const sign = neg ? "-" : "";
|
|
1991
|
+
return fracStr.length > 0 ? `${sign}${wholeStr}.${fracStr}` : `${sign}${wholeStr}`;
|
|
1992
|
+
}
|
|
1993
|
+
function lamportsToSol(lamports) {
|
|
1994
|
+
return formatUnits(lamports, 9);
|
|
1995
|
+
}
|
|
1996
|
+
function signedUi(delta, decimals, symbol) {
|
|
1997
|
+
const sign = delta > 0n ? "+" : "";
|
|
1998
|
+
const body = formatUnits(delta, decimals);
|
|
1999
|
+
const unit = symbol ? ` ${symbol}` : "";
|
|
2000
|
+
return `${sign}${body}${unit}`;
|
|
2001
|
+
}
|
|
2002
|
+
function shortAddr(addr, head = 4, tail = 4) {
|
|
2003
|
+
if (addr.length <= head + tail + 1) return addr;
|
|
2004
|
+
return `${addr.slice(0, head)}\u2026${addr.slice(-tail)}`;
|
|
2005
|
+
}
|
|
2006
|
+
function formatBlockTime(blockTime) {
|
|
2007
|
+
if (blockTime === null || blockTime === void 0) return void 0;
|
|
2008
|
+
const d = new Date(blockTime * 1e3);
|
|
2009
|
+
if (Number.isNaN(d.getTime())) return void 0;
|
|
2010
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
2011
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
|
|
2012
|
+
}
|
|
2013
|
+
function formatCount(n) {
|
|
2014
|
+
return groupThousands(Math.trunc(n).toString());
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/explain/diff.ts
|
|
2018
|
+
function tokenKey(e) {
|
|
2019
|
+
return `${e.account}::${e.mint}`;
|
|
2020
|
+
}
|
|
2021
|
+
function diffBalances(pre, post) {
|
|
2022
|
+
const deltas = [];
|
|
2023
|
+
const preLamportsByAddr = /* @__PURE__ */ new Map();
|
|
2024
|
+
for (let i = 0; i < pre.accountKeys.length; i++) {
|
|
2025
|
+
const key = pre.accountKeys[i];
|
|
2026
|
+
const lam = pre.lamports[i];
|
|
2027
|
+
if (key !== void 0 && lam !== void 0) preLamportsByAddr.set(key, lam);
|
|
2028
|
+
}
|
|
2029
|
+
const seenAddrs = /* @__PURE__ */ new Set();
|
|
2030
|
+
for (let i = 0; i < post.accountKeys.length; i++) {
|
|
2031
|
+
const account = post.accountKeys[i];
|
|
2032
|
+
const postLam = post.lamports[i];
|
|
2033
|
+
if (account === void 0 || postLam === void 0) continue;
|
|
2034
|
+
seenAddrs.add(account);
|
|
2035
|
+
const preLam = preLamportsByAddr.get(account) ?? 0n;
|
|
2036
|
+
const delta = postLam - preLam;
|
|
2037
|
+
if (delta !== 0n) {
|
|
2038
|
+
deltas.push({
|
|
2039
|
+
account,
|
|
2040
|
+
asset: { kind: "SOL" },
|
|
2041
|
+
pre: preLam,
|
|
2042
|
+
post: postLam,
|
|
2043
|
+
delta,
|
|
2044
|
+
uiDelta: signedUi(delta, 9, "SOL")
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
for (const [account, preLam] of preLamportsByAddr) {
|
|
2049
|
+
if (seenAddrs.has(account)) continue;
|
|
2050
|
+
const delta = 0n - preLam;
|
|
2051
|
+
if (delta !== 0n) {
|
|
2052
|
+
deltas.push({
|
|
2053
|
+
account,
|
|
2054
|
+
asset: { kind: "SOL" },
|
|
2055
|
+
pre: preLam,
|
|
2056
|
+
post: 0n,
|
|
2057
|
+
delta,
|
|
2058
|
+
uiDelta: signedUi(delta, 9, "SOL")
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
const preTok = /* @__PURE__ */ new Map();
|
|
2063
|
+
for (const e of pre.tokenBalances) preTok.set(tokenKey(e), e);
|
|
2064
|
+
const postTok = /* @__PURE__ */ new Map();
|
|
2065
|
+
for (const e of post.tokenBalances) postTok.set(tokenKey(e), e);
|
|
2066
|
+
const allKeys = /* @__PURE__ */ new Set([...preTok.keys(), ...postTok.keys()]);
|
|
2067
|
+
for (const key of allKeys) {
|
|
2068
|
+
const p = preTok.get(key);
|
|
2069
|
+
const q = postTok.get(key);
|
|
2070
|
+
const meta = q ?? p;
|
|
2071
|
+
if (!meta) continue;
|
|
2072
|
+
const preAmt = p?.amount ?? 0n;
|
|
2073
|
+
const postAmt = q?.amount ?? 0n;
|
|
2074
|
+
const delta = postAmt - preAmt;
|
|
2075
|
+
if (delta === 0n) continue;
|
|
2076
|
+
const decimals = meta.decimals;
|
|
2077
|
+
deltas.push({
|
|
2078
|
+
account: meta.account,
|
|
2079
|
+
asset: {
|
|
2080
|
+
kind: "token",
|
|
2081
|
+
mint: meta.mint,
|
|
2082
|
+
decimals,
|
|
2083
|
+
...meta.symbol !== void 0 ? { symbol: meta.symbol } : {},
|
|
2084
|
+
tokenProgram: meta.tokenProgram
|
|
2085
|
+
},
|
|
2086
|
+
pre: preAmt,
|
|
2087
|
+
post: postAmt,
|
|
2088
|
+
delta,
|
|
2089
|
+
uiDelta: signedUi(delta, decimals, meta.symbol),
|
|
2090
|
+
...meta.owner !== void 0 ? { owner: meta.owner } : {}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
deltas.sort((a, b) => {
|
|
2094
|
+
if (a.asset.kind !== b.asset.kind) return a.asset.kind === "SOL" ? -1 : 1;
|
|
2095
|
+
const am = a.delta < 0n ? -a.delta : a.delta;
|
|
2096
|
+
const bm = b.delta < 0n ? -b.delta : b.delta;
|
|
2097
|
+
return am > bm ? -1 : am < bm ? 1 : 0;
|
|
2098
|
+
});
|
|
2099
|
+
return deltas;
|
|
2100
|
+
}
|
|
2101
|
+
function netSolByAccount(deltas) {
|
|
2102
|
+
const m = /* @__PURE__ */ new Map();
|
|
2103
|
+
for (const d of deltas) {
|
|
2104
|
+
if (d.asset.kind === "SOL") m.set(d.account, (m.get(d.account) ?? 0n) + d.delta);
|
|
2105
|
+
}
|
|
2106
|
+
return m;
|
|
2107
|
+
}
|
|
2108
|
+
function netTokenByOwnerMint(deltas) {
|
|
2109
|
+
const m = /* @__PURE__ */ new Map();
|
|
2110
|
+
for (const d of deltas) {
|
|
2111
|
+
if (d.asset.kind !== "token") continue;
|
|
2112
|
+
const owner = d.owner ?? d.account;
|
|
2113
|
+
const key = `${owner}::${d.asset.mint}`;
|
|
2114
|
+
const cur = m.get(key);
|
|
2115
|
+
if (cur) {
|
|
2116
|
+
cur.delta += d.delta;
|
|
2117
|
+
} else {
|
|
2118
|
+
m.set(key, {
|
|
2119
|
+
owner,
|
|
2120
|
+
mint: d.asset.mint,
|
|
2121
|
+
decimals: d.asset.decimals,
|
|
2122
|
+
...d.asset.symbol !== void 0 ? { symbol: d.asset.symbol } : {},
|
|
2123
|
+
delta: d.delta
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return m;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/explain/correlate.ts
|
|
2131
|
+
function mintDecimals(tokenBalances) {
|
|
2132
|
+
const m = /* @__PURE__ */ new Map();
|
|
2133
|
+
for (const tb of tokenBalances) {
|
|
2134
|
+
if (!m.has(tb.mint)) m.set(tb.mint, tb.decimals);
|
|
2135
|
+
}
|
|
2136
|
+
return m;
|
|
2137
|
+
}
|
|
2138
|
+
function tokenAccountMeta(tokenBalances) {
|
|
2139
|
+
const m = /* @__PURE__ */ new Map();
|
|
2140
|
+
for (const tb of tokenBalances) {
|
|
2141
|
+
m.set(tb.account, {
|
|
2142
|
+
mint: tb.mint,
|
|
2143
|
+
...tb.owner !== void 0 ? { owner: tb.owner } : {},
|
|
2144
|
+
decimals: tb.decimals,
|
|
2145
|
+
tokenProgram: tb.tokenProgram
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
return m;
|
|
2149
|
+
}
|
|
2150
|
+
function uiAmount(amount, decimals) {
|
|
2151
|
+
return formatUnits(amount, decimals);
|
|
2152
|
+
}
|
|
2153
|
+
function correlate(input) {
|
|
2154
|
+
const { instructions, innerInstructions, deltas, tokenBalances, registry } = input;
|
|
2155
|
+
const decoded = [];
|
|
2156
|
+
const actions = [];
|
|
2157
|
+
const accountsCreated = [];
|
|
2158
|
+
const approvals = [];
|
|
2159
|
+
const warnings = [];
|
|
2160
|
+
const invocationCounts = /* @__PURE__ */ new Map();
|
|
2161
|
+
const decByMint = mintDecimals(tokenBalances);
|
|
2162
|
+
const taMeta = tokenAccountMeta(tokenBalances);
|
|
2163
|
+
const innerByIndex = /* @__PURE__ */ new Map();
|
|
2164
|
+
for (const grp of innerInstructions ?? []) innerByIndex.set(grp.index, grp.instructions);
|
|
2165
|
+
const seenSyncNative = /* @__PURE__ */ new Set();
|
|
2166
|
+
const handleEffect = (eff) => {
|
|
2167
|
+
switch (eff.kind) {
|
|
2168
|
+
case "sol-transfer": {
|
|
2169
|
+
actions.push({
|
|
2170
|
+
kind: "sol-transfer",
|
|
2171
|
+
from: eff.from,
|
|
2172
|
+
to: eff.to,
|
|
2173
|
+
lamports: eff.lamports,
|
|
2174
|
+
sol: lamportsToSol(eff.lamports)
|
|
2175
|
+
});
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2178
|
+
case "token-transfer": {
|
|
2179
|
+
const srcMeta = taMeta.get(eff.from);
|
|
2180
|
+
const dstMeta = taMeta.get(eff.to);
|
|
2181
|
+
const mint = eff.mint || srcMeta?.mint || dstMeta?.mint || "";
|
|
2182
|
+
const decimals = eff.decimals ?? srcMeta?.decimals ?? dstMeta?.decimals ?? decByMint.get(mint);
|
|
2183
|
+
const symbol = mint ? resolveSymbol(mint) : void 0;
|
|
2184
|
+
if (decimals === void 0) {
|
|
2185
|
+
warnings.push({
|
|
2186
|
+
code: "ambiguous-amount",
|
|
2187
|
+
message: `token transfer of ${eff.amount} raw units lacks decimals; shown in base units`
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
const dec = decimals ?? 0;
|
|
2191
|
+
const action = {
|
|
2192
|
+
kind: "token-transfer",
|
|
2193
|
+
from: eff.from,
|
|
2194
|
+
to: eff.to,
|
|
2195
|
+
mint,
|
|
2196
|
+
amount: eff.amount,
|
|
2197
|
+
uiAmount: uiAmount(eff.amount, dec),
|
|
2198
|
+
decimals: dec,
|
|
2199
|
+
tokenProgram: eff.tokenProgram,
|
|
2200
|
+
...symbol !== void 0 ? { symbol } : {}
|
|
2201
|
+
};
|
|
2202
|
+
actions.push(action);
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
case "mint": {
|
|
2206
|
+
const decimals = eff.decimals ?? decByMint.get(eff.mint) ?? 0;
|
|
2207
|
+
actions.push({
|
|
2208
|
+
kind: "mint",
|
|
2209
|
+
mint: eff.mint,
|
|
2210
|
+
to: eff.to,
|
|
2211
|
+
amount: eff.amount,
|
|
2212
|
+
uiAmount: uiAmount(eff.amount, decimals)
|
|
2213
|
+
});
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
case "burn": {
|
|
2217
|
+
const decimals = eff.decimals ?? decByMint.get(eff.mint) ?? 0;
|
|
2218
|
+
actions.push({
|
|
2219
|
+
kind: "burn",
|
|
2220
|
+
mint: eff.mint,
|
|
2221
|
+
from: eff.from,
|
|
2222
|
+
amount: eff.amount,
|
|
2223
|
+
uiAmount: uiAmount(eff.amount, decimals)
|
|
2224
|
+
});
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
case "approval": {
|
|
2228
|
+
const mint = eff.mint || taMeta.get(eff.owner)?.mint || "";
|
|
2229
|
+
const ap = {
|
|
2230
|
+
owner: eff.owner,
|
|
2231
|
+
delegate: eff.delegate,
|
|
2232
|
+
mint,
|
|
2233
|
+
amount: eff.amount,
|
|
2234
|
+
...eff.revoke !== void 0 ? { revoke: eff.revoke } : {}
|
|
2235
|
+
};
|
|
2236
|
+
approvals.push(ap);
|
|
2237
|
+
actions.push({ kind: "approval", ...ap });
|
|
2238
|
+
break;
|
|
2239
|
+
}
|
|
2240
|
+
case "close-account": {
|
|
2241
|
+
const d = deltas.find((x) => x.account === eff.account && x.asset.kind === "SOL");
|
|
2242
|
+
const reclaimed = d ? -d.delta : 0n;
|
|
2243
|
+
actions.push({
|
|
2244
|
+
kind: "close-account",
|
|
2245
|
+
account: eff.account,
|
|
2246
|
+
destination: eff.destination,
|
|
2247
|
+
reclaimedLamports: reclaimed > 0n ? reclaimed : 0n
|
|
2248
|
+
});
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
case "account-created": {
|
|
2252
|
+
const meta = taMeta.get(eff.address);
|
|
2253
|
+
const ac = {
|
|
2254
|
+
address: eff.address,
|
|
2255
|
+
owner: eff.owner,
|
|
2256
|
+
lamports: eff.lamports ?? 0n,
|
|
2257
|
+
...eff.space !== void 0 ? { space: eff.space } : {},
|
|
2258
|
+
...eff.as !== void 0 ? { as: eff.as } : meta ? { as: "token-account" } : {}
|
|
2259
|
+
};
|
|
2260
|
+
accountsCreated.push(ac);
|
|
2261
|
+
actions.push({ kind: "account-created", ...ac });
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
case "memo": {
|
|
2265
|
+
actions.push({ kind: "memo", text: eff.text });
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
case "compute-budget": {
|
|
2269
|
+
actions.push({
|
|
2270
|
+
kind: "compute-budget",
|
|
2271
|
+
...eff.unitLimit !== void 0 ? { unitLimit: eff.unitLimit } : {},
|
|
2272
|
+
...eff.unitPriceMicroLamports !== void 0 ? { unitPriceMicroLamports: eff.unitPriceMicroLamports } : {}
|
|
2273
|
+
});
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
case "sync-native": {
|
|
2277
|
+
seenSyncNative.add(eff.account);
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
const decodeInner = (views) => views.map((view) => {
|
|
2283
|
+
invocationCounts.set(view.programId, (invocationCounts.get(view.programId) ?? 0) + 1);
|
|
2284
|
+
const { decoded: dec, effects, warningCode } = decodeInstructionRaw(view, registry);
|
|
2285
|
+
if (!dec.decoded) {
|
|
2286
|
+
const known = knownProgram(view.programId);
|
|
2287
|
+
warnings.push({
|
|
2288
|
+
code: known ? "partial-decode" : "unknown-program",
|
|
2289
|
+
message: dec.warning ?? (known ? `${known.name} not byte-decoded` : "unknown program"),
|
|
2290
|
+
instructionIndex: view.index
|
|
2291
|
+
});
|
|
2292
|
+
} else if (warningCode) {
|
|
2293
|
+
warnings.push({
|
|
2294
|
+
code: warningCode,
|
|
2295
|
+
message: dec.warning ?? "partial decode",
|
|
2296
|
+
instructionIndex: view.index
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
for (const eff of effects) handleEffect(eff);
|
|
2300
|
+
return dec;
|
|
2301
|
+
});
|
|
2302
|
+
for (const view of instructions) {
|
|
2303
|
+
invocationCounts.set(view.programId, (invocationCounts.get(view.programId) ?? 0) + 1);
|
|
2304
|
+
const { decoded: dec, effects, warningCode } = decodeInstructionRaw(view, registry);
|
|
2305
|
+
if (!dec.decoded) {
|
|
2306
|
+
const known = knownProgram(view.programId);
|
|
2307
|
+
warnings.push({
|
|
2308
|
+
code: known ? "partial-decode" : "unknown-program",
|
|
2309
|
+
message: dec.warning ?? (known ? `${known.name} not byte-decoded` : "unknown program"),
|
|
2310
|
+
instructionIndex: view.index
|
|
2311
|
+
});
|
|
2312
|
+
actions.push({
|
|
2313
|
+
kind: "program-call",
|
|
2314
|
+
programId: view.programId,
|
|
2315
|
+
...known ? { program: known.name } : {},
|
|
2316
|
+
...known ? { note: "effect inferred from balance diff (no IDL)" } : { note: "unknown program; see balance changes" }
|
|
2317
|
+
});
|
|
2318
|
+
} else if (warningCode) {
|
|
2319
|
+
warnings.push({
|
|
2320
|
+
code: warningCode,
|
|
2321
|
+
message: dec.warning ?? "partial decode",
|
|
2322
|
+
instructionIndex: view.index
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
for (const eff of effects) handleEffect(eff);
|
|
2326
|
+
const inner = innerByIndex.get(view.index);
|
|
2327
|
+
if (inner && inner.length > 0) {
|
|
2328
|
+
dec.inner = decodeInner(inner);
|
|
2329
|
+
}
|
|
2330
|
+
decoded.push(dec);
|
|
2331
|
+
}
|
|
2332
|
+
if (seenSyncNative.size > 0) {
|
|
2333
|
+
for (const action of actions) {
|
|
2334
|
+
if (action.kind === "token-transfer" && isWsol(action.mint)) {
|
|
2335
|
+
if (!action.symbol) action.symbol = "wSOL";
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
const createdAddrs = new Set(accountsCreated.map((a) => a.address));
|
|
2340
|
+
for (const action of actions) {
|
|
2341
|
+
if (action.kind === "close-account" && createdAddrs.has(action.account)) {
|
|
2342
|
+
warnings.push({
|
|
2343
|
+
code: "partial-decode",
|
|
2344
|
+
message: `account ${action.account} was created and closed within this transaction (temporary ATA)`
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
for (const action of actions) {
|
|
2349
|
+
if ((action.kind === "sol-transfer" || action.kind === "token-transfer") && action.from === action.to) {
|
|
2350
|
+
warnings.push({
|
|
2351
|
+
code: "ambiguous-amount",
|
|
2352
|
+
message: "no net token movement (self-transfer)"
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const programsInvoked = [...invocationCounts.entries()].map(
|
|
2357
|
+
([programId, count]) => {
|
|
2358
|
+
const known = knownProgram(programId);
|
|
2359
|
+
return { programId, ...known ? { name: known.name } : {}, count };
|
|
2360
|
+
}
|
|
2361
|
+
);
|
|
2362
|
+
return { decoded, actions, accountsCreated, approvals, programsInvoked, warnings };
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/explain/summarize.ts
|
|
2366
|
+
function aggregatorName(programs) {
|
|
2367
|
+
const agg = programs.find((p) => p.name && isAggregator(p.name));
|
|
2368
|
+
if (agg?.name) return agg.name;
|
|
2369
|
+
const amm = programs.find((p) => p.name && isAmm(p.name));
|
|
2370
|
+
return amm?.name;
|
|
2371
|
+
}
|
|
2372
|
+
function isAggregator(name) {
|
|
2373
|
+
return /jupiter/i.test(name);
|
|
2374
|
+
}
|
|
2375
|
+
function isAmm(name) {
|
|
2376
|
+
return /raydium|orca|whirlpool|phoenix|meteora/i.test(name);
|
|
2377
|
+
}
|
|
2378
|
+
function summarize(input) {
|
|
2379
|
+
const { actions, deltas, programsInvoked, feePayer, feeLamports, success, error, focusAccount } = input;
|
|
2380
|
+
const focus = focusAccount ?? feePayer;
|
|
2381
|
+
const feeSol = lamportsToSol(feeLamports);
|
|
2382
|
+
const feeSuffix = feeLamports > 0n ? `, paid ${feeSol} SOL fee` : "";
|
|
2383
|
+
if (!success) {
|
|
2384
|
+
const why = error?.human ? `: ${error.human}` : "";
|
|
2385
|
+
return `Transaction FAILED${why}. ${feeLamports > 0n ? `Fee of ${feeSol} SOL was still charged.` : ""}`.trim();
|
|
2386
|
+
}
|
|
2387
|
+
const solByAcc = netSolByAccount(deltas);
|
|
2388
|
+
const tokByOwner = netTokenByOwnerMint(deltas);
|
|
2389
|
+
const meaningfulDeltas = deltas.filter((d) => {
|
|
2390
|
+
if (d.asset.kind === "token") return true;
|
|
2391
|
+
if (d.account !== feePayer) return true;
|
|
2392
|
+
return d.delta !== -feeLamports;
|
|
2393
|
+
});
|
|
2394
|
+
const hasDex = programsInvoked.some((p) => p.name && (isAggregator(p.name) || isAmm(p.name)));
|
|
2395
|
+
if (hasDex) {
|
|
2396
|
+
const dexName = aggregatorName(programsInvoked) ?? "a DEX";
|
|
2397
|
+
const focusGains = [];
|
|
2398
|
+
const focusLosses = [];
|
|
2399
|
+
const solDelta = solByAcc.get(focus);
|
|
2400
|
+
if (solDelta !== void 0 && solDelta !== 0n) {
|
|
2401
|
+
const label = `${formatUnits(absBig(solDelta), 9)} SOL`;
|
|
2402
|
+
(solDelta > 0n ? focusGains : focusLosses).push(label);
|
|
2403
|
+
}
|
|
2404
|
+
for (const t of tokByOwner.values()) {
|
|
2405
|
+
if (t.owner !== focus || t.delta === 0n) continue;
|
|
2406
|
+
const label = `${formatUnits(absBig(t.delta), t.decimals)} ${t.symbol ?? shortAddr(t.mint)}`;
|
|
2407
|
+
(t.delta > 0n ? focusGains : focusLosses).push(label);
|
|
2408
|
+
}
|
|
2409
|
+
if (focusLosses.length > 0 && focusGains.length > 0) {
|
|
2410
|
+
return `Swapped ${focusLosses.join(" + ")} for ${focusGains.join(" + ")} via ${dexName}${feeSuffix}.`;
|
|
2411
|
+
}
|
|
2412
|
+
if (focusGains.length > 0 || focusLosses.length > 0) {
|
|
2413
|
+
const moved = [...focusGains, ...focusLosses].join(", ");
|
|
2414
|
+
return `Routed ${moved} via ${dexName}${feeSuffix}.`;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const tokenTransfers = actions.filter((a) => a.kind === "token-transfer");
|
|
2418
|
+
const solTransfers = actions.filter((a) => a.kind === "sol-transfer");
|
|
2419
|
+
if (tokenTransfers.length > 0) {
|
|
2420
|
+
const biggest = tokenTransfers.slice().sort((a, b) => a.amount > b.amount ? -1 : 1)[0];
|
|
2421
|
+
const sym = biggest.symbol ?? shortAddr(biggest.mint);
|
|
2422
|
+
const dirNote = biggest.from === biggest.to ? " (self-transfer, no net movement)" : "";
|
|
2423
|
+
const count = tokenTransfers.length > 1 ? ` (+${tokenTransfers.length - 1} more transfer${tokenTransfers.length - 1 > 1 ? "s" : ""})` : "";
|
|
2424
|
+
return `Transferred ${biggest.uiAmount} ${sym} from ${shortAddr(biggest.from)} to ${shortAddr(biggest.to)}${dirNote}${count}${feeSuffix}.`;
|
|
2425
|
+
}
|
|
2426
|
+
if (solTransfers.length > 0) {
|
|
2427
|
+
const total = solTransfers.reduce((acc4, a) => acc4 + a.lamports, 0n);
|
|
2428
|
+
if (solTransfers.length === 1) {
|
|
2429
|
+
const a = solTransfers[0];
|
|
2430
|
+
return `Sent ${a.sol} SOL from ${shortAddr(a.from)} to ${shortAddr(a.to)}${feeSuffix}.`;
|
|
2431
|
+
}
|
|
2432
|
+
return `Made ${solTransfers.length} SOL transfers totaling ${lamportsToSol(total)} SOL${feeSuffix}.`;
|
|
2433
|
+
}
|
|
2434
|
+
const mints = actions.filter((a) => a.kind === "mint");
|
|
2435
|
+
const burns = actions.filter((a) => a.kind === "burn");
|
|
2436
|
+
if (mints.length > 0) {
|
|
2437
|
+
const m = mints[0];
|
|
2438
|
+
return `Minted ${m.uiAmount} of ${shortAddr(m.mint)} to ${shortAddr(m.to)}${feeSuffix}.`;
|
|
2439
|
+
}
|
|
2440
|
+
if (burns.length > 0) {
|
|
2441
|
+
const b = burns[0];
|
|
2442
|
+
return `Burned ${b.uiAmount} of ${shortAddr(b.mint)}${feeSuffix}.`;
|
|
2443
|
+
}
|
|
2444
|
+
const approvals = actions.filter((a) => a.kind === "approval");
|
|
2445
|
+
if (approvals.length > 0) {
|
|
2446
|
+
const ap = approvals[0];
|
|
2447
|
+
if (ap.revoke) {
|
|
2448
|
+
return `Revoked token delegate approval${feeSuffix}.`;
|
|
2449
|
+
}
|
|
2450
|
+
const amt = ap.amount === "unlimited" ? "unlimited" : ap.amount.toString();
|
|
2451
|
+
return `Approved delegate ${shortAddr(ap.delegate)} to spend ${amt} tokens${feeSuffix}.`;
|
|
2452
|
+
}
|
|
2453
|
+
const created = actions.filter((a) => a.kind === "account-created");
|
|
2454
|
+
if (created.length > 0) {
|
|
2455
|
+
const what = created.some((c) => c.kind === "account-created" && c.as === "ata") ? "associated token account" : "account";
|
|
2456
|
+
const plural = created.length > 1 ? `${created.length} accounts` : `an ${what}`;
|
|
2457
|
+
return `Created ${plural}${feeSuffix}.`;
|
|
2458
|
+
}
|
|
2459
|
+
const memos = actions.filter((a) => a.kind === "memo");
|
|
2460
|
+
const cb = actions.filter((a) => a.kind === "compute-budget");
|
|
2461
|
+
if (memos.length > 0 && meaningfulDeltas.length === 0) {
|
|
2462
|
+
const text = memos[0].kind === "memo" ? memos[0].text : "";
|
|
2463
|
+
const memoNote = `attached memo "${text}"`;
|
|
2464
|
+
return cb.length > 0 ? `Set compute budget; ${memoNote}${feeSuffix}.` : `${capitalize(memoNote)}${feeSuffix}.`;
|
|
2465
|
+
}
|
|
2466
|
+
if (cb.length > 0 && meaningfulDeltas.length === 0 && actions.every((a) => a.kind === "compute-budget")) {
|
|
2467
|
+
return `Set compute budget only; no balance changes${feeSuffix}.`;
|
|
2468
|
+
}
|
|
2469
|
+
const calls = actions.filter((a) => a.kind === "program-call");
|
|
2470
|
+
if (calls.length > 0) {
|
|
2471
|
+
const named = calls.find((c) => c.kind === "program-call" && c.program);
|
|
2472
|
+
const name = named && named.kind === "program-call" && named.program ? named.program : shortAddr(calls[0].kind === "program-call" ? calls[0].programId : "");
|
|
2473
|
+
if (deltas.length > 0) {
|
|
2474
|
+
return `Interacted with ${name}; see balance changes for the net effect${feeSuffix}.`;
|
|
2475
|
+
}
|
|
2476
|
+
return `Called ${name} (no net balance change)${feeSuffix}.`;
|
|
2477
|
+
}
|
|
2478
|
+
if (meaningfulDeltas.length > 0) {
|
|
2479
|
+
return `Transaction changed ${meaningfulDeltas.length} balance${meaningfulDeltas.length > 1 ? "s" : ""}${feeSuffix}.`;
|
|
2480
|
+
}
|
|
2481
|
+
return `Transaction succeeded with no net balance change${feeSuffix}.`;
|
|
2482
|
+
}
|
|
2483
|
+
function absBig(n) {
|
|
2484
|
+
return n < 0n ? -n : n;
|
|
2485
|
+
}
|
|
2486
|
+
function capitalize(s) {
|
|
2487
|
+
return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/explain.ts
|
|
2491
|
+
function resolveRpc(rpc, timeoutMs) {
|
|
2492
|
+
if (typeof rpc === "string") {
|
|
2493
|
+
return createHttpRpc(rpc, { timeoutMs });
|
|
2494
|
+
}
|
|
2495
|
+
return rpc;
|
|
2496
|
+
}
|
|
2497
|
+
function resolveRegistry(opts) {
|
|
2498
|
+
if (opts.registry) {
|
|
2499
|
+
return defaultRegistry.merge(opts.registry.list());
|
|
2500
|
+
}
|
|
2501
|
+
return defaultRegistry;
|
|
2502
|
+
}
|
|
2503
|
+
function clampTimeout(timeoutMs) {
|
|
2504
|
+
const def = 3e4;
|
|
2505
|
+
if (timeoutMs === void 0) return { value: def };
|
|
2506
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
2507
|
+
return { value: def, warning: `invalid timeout ${timeoutMs}; using ${def}ms` };
|
|
2508
|
+
}
|
|
2509
|
+
const min = 1e3;
|
|
2510
|
+
const max = 6e5;
|
|
2511
|
+
if (timeoutMs < min) return { value: min, warning: `timeout clamped up to ${min}ms` };
|
|
2512
|
+
if (timeoutMs > max) return { value: max, warning: `timeout clamped down to ${max}ms` };
|
|
2513
|
+
return { value: timeoutMs };
|
|
2514
|
+
}
|
|
2515
|
+
function buildExplanation(input) {
|
|
2516
|
+
const deltas = diffBalances(input.pre, input.post);
|
|
2517
|
+
for (const d of deltas) {
|
|
2518
|
+
if (d.asset.kind === "token" && d.asset.symbol === void 0) {
|
|
2519
|
+
const sym = resolveSymbol(d.asset.mint);
|
|
2520
|
+
if (sym) d.asset.symbol = sym;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
const allTokenBalances = [...input.pre.tokenBalances, ...input.post.tokenBalances];
|
|
2524
|
+
const correlation = correlate({
|
|
2525
|
+
instructions: input.instructions,
|
|
2526
|
+
...input.innerInstructions ? { innerInstructions: input.innerInstructions } : {},
|
|
2527
|
+
deltas,
|
|
2528
|
+
tokenBalances: allTokenBalances,
|
|
2529
|
+
registry: input.registry
|
|
2530
|
+
});
|
|
2531
|
+
const summary = summarize({
|
|
2532
|
+
actions: correlation.actions,
|
|
2533
|
+
deltas,
|
|
2534
|
+
programsInvoked: correlation.programsInvoked,
|
|
2535
|
+
feePayer: input.feePayer,
|
|
2536
|
+
feeLamports: input.feeLamports,
|
|
2537
|
+
success: input.success,
|
|
2538
|
+
...input.error ? { error: input.error } : {},
|
|
2539
|
+
...input.focusAccount ? { focusAccount: input.focusAccount } : {}
|
|
2540
|
+
});
|
|
2541
|
+
const warnings = [...input.warnings ?? [], ...correlation.warnings];
|
|
2542
|
+
const result = {
|
|
2543
|
+
source: input.source,
|
|
2544
|
+
...input.signature ? { signature: input.signature } : {},
|
|
2545
|
+
success: input.success,
|
|
2546
|
+
...input.error ? { error: input.error } : {},
|
|
2547
|
+
...input.slot !== void 0 ? { slot: input.slot } : {},
|
|
2548
|
+
...input.blockTime !== void 0 ? { blockTime: input.blockTime } : {},
|
|
2549
|
+
feeLamports: input.feeLamports,
|
|
2550
|
+
...input.computeUnits !== void 0 ? { computeUnits: input.computeUnits } : {},
|
|
2551
|
+
feePayer: input.feePayer,
|
|
2552
|
+
summary,
|
|
2553
|
+
actions: correlation.actions,
|
|
2554
|
+
balanceChanges: deltas,
|
|
2555
|
+
instructions: correlation.decoded,
|
|
2556
|
+
accountsCreated: correlation.accountsCreated,
|
|
2557
|
+
approvals: correlation.approvals,
|
|
2558
|
+
programsInvoked: correlation.programsInvoked,
|
|
2559
|
+
warnings,
|
|
2560
|
+
...input.includeRaw && input.raw !== void 0 ? { raw: input.raw } : {}
|
|
2561
|
+
};
|
|
2562
|
+
return result;
|
|
2563
|
+
}
|
|
2564
|
+
function normalizedToResult(norm, registry, opts, extraWarnings) {
|
|
2565
|
+
const input = {
|
|
2566
|
+
source: norm.source,
|
|
2567
|
+
...norm.signature ? { signature: norm.signature } : {},
|
|
2568
|
+
feePayer: norm.feePayer,
|
|
2569
|
+
success: norm.success,
|
|
2570
|
+
...norm.error ? { error: norm.error } : {},
|
|
2571
|
+
...norm.slot !== void 0 ? { slot: norm.slot } : {},
|
|
2572
|
+
blockTime: norm.blockTime ?? null,
|
|
2573
|
+
feeLamports: norm.feeLamports,
|
|
2574
|
+
...norm.computeUnits !== void 0 ? { computeUnits: norm.computeUnits } : {},
|
|
2575
|
+
pre: norm.pre,
|
|
2576
|
+
post: norm.post,
|
|
2577
|
+
instructions: norm.instructions,
|
|
2578
|
+
...norm.innerInstructions ? { innerInstructions: norm.innerInstructions } : {},
|
|
2579
|
+
registry,
|
|
2580
|
+
...opts.focusAccount ? { focusAccount: opts.focusAccount } : {},
|
|
2581
|
+
warnings: [...norm.warnings, ...extraWarnings],
|
|
2582
|
+
...opts.includeRaw && norm.raw !== void 0 ? { raw: norm.raw } : {},
|
|
2583
|
+
...opts.includeRaw ? { includeRaw: true } : {}
|
|
2584
|
+
};
|
|
2585
|
+
const result = buildExplanation(input);
|
|
2586
|
+
if (norm.source === "signature" && opts.commitment) {
|
|
2587
|
+
result.commitment = opts.commitment;
|
|
2588
|
+
}
|
|
2589
|
+
return result;
|
|
2590
|
+
}
|
|
2591
|
+
async function explainSignature(signature, options) {
|
|
2592
|
+
try {
|
|
2593
|
+
const { value: timeoutMs, warning } = clampTimeout(options.timeoutMs);
|
|
2594
|
+
const rpc = resolveRpc(options.rpc, timeoutMs);
|
|
2595
|
+
const registry = resolveRegistry(options);
|
|
2596
|
+
const commitment = options.commitment ?? "confirmed";
|
|
2597
|
+
const norm = await acquireFromSignature({
|
|
2598
|
+
rpc,
|
|
2599
|
+
signature,
|
|
2600
|
+
commitment,
|
|
2601
|
+
maxSupportedTransactionVersion: options.maxSupportedTransactionVersion ?? 0,
|
|
2602
|
+
...options.signal ? { signal: options.signal } : {},
|
|
2603
|
+
...options.includeRaw ? { includeRaw: true } : {}
|
|
2604
|
+
});
|
|
2605
|
+
const extra = warning ? [{ code: "version-skew", message: warning }] : [];
|
|
2606
|
+
return normalizedToResult(norm, registry, options, extra);
|
|
2607
|
+
} catch (err) {
|
|
2608
|
+
throw asExplainError(err, "failed to explain signature");
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
async function explainTransaction(tx, options) {
|
|
2612
|
+
try {
|
|
2613
|
+
const { value: timeoutMs, warning } = clampTimeout(options.timeoutMs);
|
|
2614
|
+
const rpc = resolveRpc(options.rpc, timeoutMs);
|
|
2615
|
+
const registry = resolveRegistry(options);
|
|
2616
|
+
const commitment = options.commitment ?? "confirmed";
|
|
2617
|
+
let bytes;
|
|
2618
|
+
let ambiguous = false;
|
|
2619
|
+
if (tx instanceof Uint8Array) {
|
|
2620
|
+
const det = detectInput(tx);
|
|
2621
|
+
if (det.kind !== "tx-bytes") {
|
|
2622
|
+
throw new InputError("INVALID_INPUT", "Provided bytes are not a serialized transaction.");
|
|
2623
|
+
}
|
|
2624
|
+
bytes = det.bytes;
|
|
2625
|
+
} else {
|
|
2626
|
+
const det = detectTxBytes(tx, options.encoding ?? "auto");
|
|
2627
|
+
bytes = det.bytes;
|
|
2628
|
+
ambiguous = det.ambiguous;
|
|
2629
|
+
}
|
|
2630
|
+
const norm = await acquireFromSimulation({
|
|
2631
|
+
rpc,
|
|
2632
|
+
txBytes: bytes,
|
|
2633
|
+
commitment,
|
|
2634
|
+
replaceRecentBlockhash: options.replaceRecentBlockhash ?? true,
|
|
2635
|
+
sigVerify: options.sigVerify ?? false,
|
|
2636
|
+
...options.signal ? { signal: options.signal } : {},
|
|
2637
|
+
...options.includeRaw ? { includeRaw: true } : {}
|
|
2638
|
+
});
|
|
2639
|
+
const extra = [];
|
|
2640
|
+
if (ambiguous) {
|
|
2641
|
+
extra.push({
|
|
2642
|
+
code: "version-skew",
|
|
2643
|
+
message: "input was decodable as both base64 and base58; interpreted as base64"
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
if (warning) extra.push({ code: "version-skew", message: warning });
|
|
2647
|
+
return normalizedToResult(norm, registry, options, extra);
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
throw asExplainError(err, "failed to explain transaction");
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
async function explainInstructions(instructions, options) {
|
|
2653
|
+
try {
|
|
2654
|
+
if (!options.feePayer || options.feePayer.trim().length === 0) {
|
|
2655
|
+
throw new InputError(
|
|
2656
|
+
"INVALID_INPUT",
|
|
2657
|
+
"feePayer is required to assemble a simulatable message from instructions.",
|
|
2658
|
+
{ hint: "Pass options.feePayer with the payer pubkey." }
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
if (!Array.isArray(instructions) || instructions.length === 0) {
|
|
2662
|
+
throw new InputError("EMPTY_INPUT", "No instructions provided.");
|
|
2663
|
+
}
|
|
2664
|
+
const { value: timeoutMs, warning } = clampTimeout(options.timeoutMs);
|
|
2665
|
+
const rpc = resolveRpc(options.rpc, timeoutMs);
|
|
2666
|
+
const registry = resolveRegistry(options);
|
|
2667
|
+
const commitment = options.commitment ?? "confirmed";
|
|
2668
|
+
let blockhash;
|
|
2669
|
+
try {
|
|
2670
|
+
const bh = await rpc.getLatestBlockhash({
|
|
2671
|
+
commitment,
|
|
2672
|
+
...options.signal ? { signal: options.signal } : {}
|
|
2673
|
+
});
|
|
2674
|
+
blockhash = bh.blockhash;
|
|
2675
|
+
} catch {
|
|
2676
|
+
blockhash = "11111111111111111111111111111111";
|
|
2677
|
+
}
|
|
2678
|
+
const txBytes = buildLegacyMessage(instructions, options.feePayer, blockhash);
|
|
2679
|
+
const norm = await acquireFromSimulation({
|
|
2680
|
+
rpc,
|
|
2681
|
+
txBytes,
|
|
2682
|
+
commitment,
|
|
2683
|
+
replaceRecentBlockhash: true,
|
|
2684
|
+
sigVerify: false,
|
|
2685
|
+
...options.signal ? { signal: options.signal } : {},
|
|
2686
|
+
...options.includeRaw ? { includeRaw: true } : {}
|
|
2687
|
+
});
|
|
2688
|
+
const extra = warning ? [{ code: "version-skew", message: warning }] : [];
|
|
2689
|
+
return normalizedToResult(norm, registry, options, extra);
|
|
2690
|
+
} catch (err) {
|
|
2691
|
+
throw asExplainError(err, "failed to explain instructions");
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
async function explain(input, options) {
|
|
2695
|
+
try {
|
|
2696
|
+
const detected = detectInput(input);
|
|
2697
|
+
switch (detected.kind) {
|
|
2698
|
+
case "signature":
|
|
2699
|
+
return await explainSignature(detected.signature, options);
|
|
2700
|
+
case "tx-bytes":
|
|
2701
|
+
return await explainTransaction(detected.bytes, {
|
|
2702
|
+
...options,
|
|
2703
|
+
encoding: detected.encoding
|
|
2704
|
+
});
|
|
2705
|
+
case "instruction-set": {
|
|
2706
|
+
const opts = options;
|
|
2707
|
+
if (!opts.feePayer) {
|
|
2708
|
+
throw new InputError(
|
|
2709
|
+
"INVALID_INPUT",
|
|
2710
|
+
"Instruction-set input requires options.feePayer.",
|
|
2711
|
+
{ hint: "Add feePayer to options." }
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
return await explainInstructions(detected.instructions, opts);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
throw asExplainError(err, "failed to explain input");
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function asExplainError(err, context) {
|
|
2722
|
+
if (err instanceof SolanaExplainError) return err;
|
|
2723
|
+
return wrapError(err, "INVALID_INPUT", context);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/render/ansi.ts
|
|
2727
|
+
var ESC = `${String.fromCharCode(27)}[`;
|
|
2728
|
+
var RESET = `${ESC}0m`;
|
|
2729
|
+
var CODES = {
|
|
2730
|
+
bold: `${ESC}1m`,
|
|
2731
|
+
dim: `${ESC}2m`,
|
|
2732
|
+
red: `${ESC}31m`,
|
|
2733
|
+
green: `${ESC}32m`,
|
|
2734
|
+
yellow: `${ESC}33m`,
|
|
2735
|
+
blue: `${ESC}34m`,
|
|
2736
|
+
magenta: `${ESC}35m`,
|
|
2737
|
+
cyan: `${ESC}36m`,
|
|
2738
|
+
gray: `${ESC}90m`,
|
|
2739
|
+
// VTX brand blue (#3182ce) via 256-color approximation.
|
|
2740
|
+
brand: `${ESC}38;5;33m`
|
|
2741
|
+
};
|
|
2742
|
+
function wrap(enabled, code, s) {
|
|
2743
|
+
return enabled ? `${code}${s}${RESET}` : s;
|
|
2744
|
+
}
|
|
2745
|
+
function createStyler(enabled) {
|
|
2746
|
+
return {
|
|
2747
|
+
enabled,
|
|
2748
|
+
bold: (s) => wrap(enabled, CODES.bold, s),
|
|
2749
|
+
dim: (s) => wrap(enabled, CODES.dim, s),
|
|
2750
|
+
red: (s) => wrap(enabled, CODES.red, s),
|
|
2751
|
+
green: (s) => wrap(enabled, CODES.green, s),
|
|
2752
|
+
yellow: (s) => wrap(enabled, CODES.yellow, s),
|
|
2753
|
+
blue: (s) => wrap(enabled, CODES.blue, s),
|
|
2754
|
+
magenta: (s) => wrap(enabled, CODES.magenta, s),
|
|
2755
|
+
cyan: (s) => wrap(enabled, CODES.cyan, s),
|
|
2756
|
+
gray: (s) => wrap(enabled, CODES.gray, s),
|
|
2757
|
+
brand: (s) => wrap(enabled, CODES.brand, s)
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
var ANSI_RE = new RegExp(`${String.fromCharCode(27)}[[0-9;]*m`, "g");
|
|
2761
|
+
function stripAnsi(s) {
|
|
2762
|
+
return s.replace(ANSI_RE, "");
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/render/index.ts
|
|
2766
|
+
var INDENT = " ";
|
|
2767
|
+
function renderText(result, opts = {}) {
|
|
2768
|
+
const s = createStyler(opts.color ?? false);
|
|
2769
|
+
const lines = [];
|
|
2770
|
+
if (opts.quiet) {
|
|
2771
|
+
return result.summary;
|
|
2772
|
+
}
|
|
2773
|
+
const commitmentNote = result.source === "simulation" ? "simulation" : result.commitment ?? "confirmed";
|
|
2774
|
+
const when = formatBlockTime(result.blockTime);
|
|
2775
|
+
const headParts = [
|
|
2776
|
+
`Solana transaction`,
|
|
2777
|
+
commitmentNote,
|
|
2778
|
+
result.slot !== void 0 ? `slot ${formatCount(result.slot)}` : void 0,
|
|
2779
|
+
when
|
|
2780
|
+
].filter(Boolean);
|
|
2781
|
+
lines.push("");
|
|
2782
|
+
lines.push(`${INDENT}${s.brand(headParts.join(" \xB7 "))}`);
|
|
2783
|
+
const status = result.success ? s.green("\u2713 Success") : s.red("\u2717 Failed");
|
|
2784
|
+
if (result.signature) {
|
|
2785
|
+
lines.push(`${INDENT}${s.dim("Signature")} ${shortAddr(result.signature, 6, 6)} ${status}`);
|
|
2786
|
+
} else {
|
|
2787
|
+
lines.push(`${INDENT}${s.dim("Source")} ${result.source} ${status}`);
|
|
2788
|
+
}
|
|
2789
|
+
lines.push("");
|
|
2790
|
+
lines.push(`${INDENT}${s.bold("Summary")} ${result.summary}`);
|
|
2791
|
+
if (!result.success && result.error) {
|
|
2792
|
+
lines.push(`${INDENT}${s.red("Error")} ${result.error.human}`);
|
|
2793
|
+
}
|
|
2794
|
+
if (result.balanceChanges.length > 0) {
|
|
2795
|
+
lines.push("");
|
|
2796
|
+
lines.push(`${INDENT}${s.bold("Balance changes")}`);
|
|
2797
|
+
for (const d of groupBalances(result.balanceChanges, result.feePayer)) {
|
|
2798
|
+
const label = d.label;
|
|
2799
|
+
const focusTag = d.account === result.feePayer ? s.dim(" (fee payer)") : "";
|
|
2800
|
+
const colored = colorDelta(d.text, s);
|
|
2801
|
+
lines.push(`${INDENT}${INDENT}${padRight(label, 22)}${colored}${focusTag}`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (result.actions.length > 0) {
|
|
2805
|
+
lines.push("");
|
|
2806
|
+
lines.push(`${INDENT}${s.bold("Actions")}`);
|
|
2807
|
+
result.actions.forEach((a, i) => {
|
|
2808
|
+
lines.push(`${INDENT}${INDENT}${i + 1}. ${describeAction(a)}`);
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
if (opts.verbose && result.instructions.length > 0) {
|
|
2812
|
+
lines.push("");
|
|
2813
|
+
lines.push(`${INDENT}${s.bold("Instructions")}`);
|
|
2814
|
+
for (const ix of result.instructions) {
|
|
2815
|
+
renderInstruction(ix, lines, s, 2);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (result.programsInvoked.length > 0) {
|
|
2819
|
+
const names = result.programsInvoked.map((p) => p.name ?? shortAddr(p.programId)).filter((v, idx, arr) => arr.indexOf(v) === idx);
|
|
2820
|
+
lines.push("");
|
|
2821
|
+
lines.push(`${INDENT}${s.bold("Programs")} ${names.join(" \xB7 ")}`);
|
|
2822
|
+
}
|
|
2823
|
+
const feeStr = `${lamportsToSol(result.feeLamports)} SOL`;
|
|
2824
|
+
const computeStr = result.computeUnits !== void 0 ? `${formatCount(result.computeUnits)} units` : "n/a";
|
|
2825
|
+
lines.push(`${INDENT}${s.bold("Fee")} ${feeStr}${" ".repeat(8)}${s.bold("Compute")} ${computeStr}`);
|
|
2826
|
+
if (result.warnings.length > 0) {
|
|
2827
|
+
lines.push("");
|
|
2828
|
+
lines.push(`${INDENT}${s.yellow(`\u26A0 ${result.warnings.length} warning${result.warnings.length > 1 ? "s" : ""}`)}`);
|
|
2829
|
+
for (const w of result.warnings) {
|
|
2830
|
+
const at = w.instructionIndex !== void 0 ? ` [ix #${w.instructionIndex}]` : "";
|
|
2831
|
+
lines.push(`${INDENT}${INDENT}${s.dim("\xB7")} ${w.message}${s.dim(at)}`);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
lines.push("");
|
|
2835
|
+
return lines.join("\n");
|
|
2836
|
+
}
|
|
2837
|
+
function renderInstruction(ix, lines, s, depth) {
|
|
2838
|
+
const pad = INDENT.repeat(depth);
|
|
2839
|
+
const name = ix.program ?? shortAddr(ix.programId);
|
|
2840
|
+
const type = ix.type ? `.${ix.type}` : "";
|
|
2841
|
+
const mark = ix.decoded ? "" : s.dim(" (not decoded)");
|
|
2842
|
+
lines.push(`${pad}${s.cyan(`#${ix.index}`)} ${name}${type}${mark}`);
|
|
2843
|
+
if (ix.args && Object.keys(ix.args).length > 0) {
|
|
2844
|
+
const argStr = Object.entries(ix.args).map(([k, v]) => `${k}=${String(v)}`).join(", ");
|
|
2845
|
+
lines.push(`${pad}${INDENT}${s.dim(argStr)}`);
|
|
2846
|
+
}
|
|
2847
|
+
if (ix.inner) {
|
|
2848
|
+
for (const inner of ix.inner) renderInstruction(inner, lines, s, depth + 1);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
function groupBalances(deltas, feePayer) {
|
|
2852
|
+
const byAccount = /* @__PURE__ */ new Map();
|
|
2853
|
+
for (const d of deltas) {
|
|
2854
|
+
const list = byAccount.get(d.account) ?? [];
|
|
2855
|
+
list.push(d);
|
|
2856
|
+
byAccount.set(d.account, list);
|
|
2857
|
+
}
|
|
2858
|
+
const out = [];
|
|
2859
|
+
for (const [account, list] of byAccount) {
|
|
2860
|
+
const text = list.map((d) => d.uiDelta).join(" ");
|
|
2861
|
+
const isFee = account === feePayer;
|
|
2862
|
+
out.push({ account, label: shortAddr(account, 4, 4) + (isFee ? "" : ""), text });
|
|
2863
|
+
}
|
|
2864
|
+
return out;
|
|
2865
|
+
}
|
|
2866
|
+
function colorDelta(text, s) {
|
|
2867
|
+
return text.split(" ").map(
|
|
2868
|
+
(part) => part.trim().startsWith("-") ? s.red(part) : part.trim().startsWith("+") ? s.green(part) : part
|
|
2869
|
+
).join(" ");
|
|
2870
|
+
}
|
|
2871
|
+
function padRight(str, width) {
|
|
2872
|
+
const visible = stripAnsi(str);
|
|
2873
|
+
return visible.length >= width ? str : str + " ".repeat(width - visible.length);
|
|
2874
|
+
}
|
|
2875
|
+
function describeAction(a) {
|
|
2876
|
+
switch (a.kind) {
|
|
2877
|
+
case "sol-transfer":
|
|
2878
|
+
return `Transfer ${a.sol} SOL ${shortAddr(a.from)} \u2192 ${shortAddr(a.to)}`;
|
|
2879
|
+
case "token-transfer": {
|
|
2880
|
+
const sym = a.symbol ?? shortAddr(a.mint);
|
|
2881
|
+
const self = a.from === a.to ? " (self-transfer)" : "";
|
|
2882
|
+
return `Transfer ${a.uiAmount} ${sym} ${shortAddr(a.from)} \u2192 ${shortAddr(a.to)}${self}`;
|
|
2883
|
+
}
|
|
2884
|
+
case "mint":
|
|
2885
|
+
return `Mint ${a.uiAmount} of ${shortAddr(a.mint)} \u2192 ${shortAddr(a.to)}`;
|
|
2886
|
+
case "burn":
|
|
2887
|
+
return `Burn ${a.uiAmount} of ${shortAddr(a.mint)} from ${shortAddr(a.from)}`;
|
|
2888
|
+
case "account-created": {
|
|
2889
|
+
const as = a.as ? ` (${a.as})` : "";
|
|
2890
|
+
return `Create account ${shortAddr(a.address)}${as} owned by ${shortAddr(a.owner)}`;
|
|
2891
|
+
}
|
|
2892
|
+
case "approval":
|
|
2893
|
+
if (a.revoke) return `Revoke delegate on ${shortAddr(a.owner)}`;
|
|
2894
|
+
return `Approve ${a.amount === "unlimited" ? "unlimited" : a.amount.toString()} to delegate ${shortAddr(a.delegate)}`;
|
|
2895
|
+
case "close-account":
|
|
2896
|
+
return `Close account ${shortAddr(a.account)} \u2192 reclaim ${lamportsToSol(a.reclaimedLamports)} SOL to ${shortAddr(a.destination)}`;
|
|
2897
|
+
case "memo":
|
|
2898
|
+
return `Memo: "${a.text}"`;
|
|
2899
|
+
case "compute-budget": {
|
|
2900
|
+
const parts = [];
|
|
2901
|
+
if (a.unitLimit !== void 0) parts.push(`unit limit ${groupThousands(a.unitLimit.toString())}`);
|
|
2902
|
+
if (a.unitPriceMicroLamports !== void 0)
|
|
2903
|
+
parts.push(`price ${a.unitPriceMicroLamports.toString()} \xB5-lamports`);
|
|
2904
|
+
return `Set compute budget: ${parts.join(", ") || "(no-op)"}`;
|
|
2905
|
+
}
|
|
2906
|
+
case "program-call": {
|
|
2907
|
+
const name = a.program ?? shortAddr(a.programId);
|
|
2908
|
+
const note = a.note ? ` \u2014 ${a.note}` : "";
|
|
2909
|
+
return `Call ${name}${a.instruction ? `.${a.instruction}` : ""}${note}`;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
function renderMarkdown(result) {
|
|
2914
|
+
const lines = [];
|
|
2915
|
+
lines.push(`### Solana transaction \u2014 ${result.success ? "\u2705 Success" : "\u274C Failed"}`);
|
|
2916
|
+
lines.push("");
|
|
2917
|
+
if (result.signature) lines.push(`**Signature:** \`${result.signature}\` `);
|
|
2918
|
+
if (result.slot !== void 0) lines.push(`**Slot:** ${formatCount(result.slot)} `);
|
|
2919
|
+
const when = formatBlockTime(result.blockTime);
|
|
2920
|
+
if (when) lines.push(`**Time:** ${when} `);
|
|
2921
|
+
lines.push(`**Fee:** ${lamportsToSol(result.feeLamports)} SOL `);
|
|
2922
|
+
if (result.computeUnits !== void 0)
|
|
2923
|
+
lines.push(`**Compute:** ${formatCount(result.computeUnits)} units `);
|
|
2924
|
+
lines.push("");
|
|
2925
|
+
lines.push(`> ${result.summary}`);
|
|
2926
|
+
lines.push("");
|
|
2927
|
+
if (!result.success && result.error) {
|
|
2928
|
+
lines.push(`**Error:** ${result.error.human}`);
|
|
2929
|
+
lines.push("");
|
|
2930
|
+
}
|
|
2931
|
+
if (result.balanceChanges.length > 0) {
|
|
2932
|
+
lines.push("#### Balance changes");
|
|
2933
|
+
lines.push("");
|
|
2934
|
+
lines.push("| Account | Asset | Change |");
|
|
2935
|
+
lines.push("| --- | --- | --- |");
|
|
2936
|
+
for (const d of result.balanceChanges) {
|
|
2937
|
+
const asset = d.asset.kind === "SOL" ? "SOL" : `${d.asset.symbol ?? shortAddr(d.asset.mint)}`;
|
|
2938
|
+
lines.push(`| \`${shortAddr(d.account, 6, 6)}\` | ${asset} | ${d.uiDelta} |`);
|
|
2939
|
+
}
|
|
2940
|
+
lines.push("");
|
|
2941
|
+
}
|
|
2942
|
+
if (result.actions.length > 0) {
|
|
2943
|
+
lines.push("#### Actions");
|
|
2944
|
+
lines.push("");
|
|
2945
|
+
result.actions.forEach((a, i) => lines.push(`${i + 1}. ${describeAction(a)}`));
|
|
2946
|
+
lines.push("");
|
|
2947
|
+
}
|
|
2948
|
+
if (result.programsInvoked.length > 0) {
|
|
2949
|
+
lines.push("#### Programs");
|
|
2950
|
+
lines.push("");
|
|
2951
|
+
for (const p of result.programsInvoked) {
|
|
2952
|
+
lines.push(`- ${p.name ?? `\`${p.programId}\``} \xD7${p.count}`);
|
|
2953
|
+
}
|
|
2954
|
+
lines.push("");
|
|
2955
|
+
}
|
|
2956
|
+
if (result.warnings.length > 0) {
|
|
2957
|
+
lines.push("#### Warnings");
|
|
2958
|
+
lines.push("");
|
|
2959
|
+
for (const w of result.warnings) lines.push(`- \u26A0\uFE0F ${w.message} \`(${w.code})\``);
|
|
2960
|
+
lines.push("");
|
|
2961
|
+
}
|
|
2962
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
2963
|
+
}
|
|
2964
|
+
function renderJson(result, opts) {
|
|
2965
|
+
const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
2966
|
+
return JSON.stringify(result, replacer, opts?.pretty ? 2 : void 0);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/rpc/web3-adapter.ts
|
|
2970
|
+
function isKitRpc(rpc) {
|
|
2971
|
+
if (typeof rpc !== "object" || rpc === null) return false;
|
|
2972
|
+
const gt = rpc["getTransaction"];
|
|
2973
|
+
if (typeof gt !== "function") return false;
|
|
2974
|
+
try {
|
|
2975
|
+
return typeof rpc["getMultipleAccounts"] === "function" && typeof rpc["getMultipleAccountsInfo"] !== "function";
|
|
2976
|
+
} catch {
|
|
2977
|
+
return false;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
function fromWeb3Rpc(rpc) {
|
|
2981
|
+
if (typeof rpc !== "object" || rpc === null) {
|
|
2982
|
+
throw new RpcError("RPC_HTTP", "fromWeb3Rpc expects a Connection or RPC object.");
|
|
2983
|
+
}
|
|
2984
|
+
if (isKitRpc(rpc)) {
|
|
2985
|
+
return fromKit(rpc);
|
|
2986
|
+
}
|
|
2987
|
+
return fromV1Connection(rpc);
|
|
2988
|
+
}
|
|
2989
|
+
function fromV1Connection(conn) {
|
|
2990
|
+
if (typeof conn.getTransaction !== "function") {
|
|
2991
|
+
throw new RpcError(
|
|
2992
|
+
"RPC_HTTP",
|
|
2993
|
+
"fromWeb3Rpc: object does not look like a web3.js Connection (missing getTransaction)."
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
return {
|
|
2997
|
+
async getTransaction(sig, o) {
|
|
2998
|
+
try {
|
|
2999
|
+
const res = await conn.getTransaction(sig, {
|
|
3000
|
+
commitment: o.commitment ?? "confirmed",
|
|
3001
|
+
maxSupportedTransactionVersion: o.maxSupportedTransactionVersion ?? 0
|
|
3002
|
+
});
|
|
3003
|
+
return res ?? null;
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
throw wrapError(err, "RPC_HTTP", "getTransaction failed");
|
|
3006
|
+
}
|
|
3007
|
+
},
|
|
3008
|
+
async simulateTransaction(txBase64, o) {
|
|
3009
|
+
try {
|
|
3010
|
+
const res = await conn.simulateTransaction(txBase64, {
|
|
3011
|
+
sigVerify: o.sigVerify ?? false,
|
|
3012
|
+
replaceRecentBlockhash: o.replaceRecentBlockhash ?? true,
|
|
3013
|
+
commitment: o.commitment ?? "confirmed",
|
|
3014
|
+
innerInstructions: o.innerInstructions ?? true,
|
|
3015
|
+
accounts: o.accounts ? { addresses: o.accounts.addresses, encoding: o.accounts.encoding ?? "jsonParsed" } : void 0
|
|
3016
|
+
});
|
|
3017
|
+
return res;
|
|
3018
|
+
} catch (err) {
|
|
3019
|
+
throw wrapError(err, "SIMULATION_REJECTED", "simulateTransaction failed");
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
async getMultipleAccounts(addresses, o) {
|
|
3023
|
+
if (addresses.length === 0) return [];
|
|
3024
|
+
try {
|
|
3025
|
+
if (typeof conn.getMultipleParsedAccounts === "function") {
|
|
3026
|
+
const res = await conn.getMultipleParsedAccounts(addresses, {
|
|
3027
|
+
commitment: o.commitment ?? "confirmed"
|
|
3028
|
+
});
|
|
3029
|
+
const arr = Array.isArray(res) ? res : res.value ?? [];
|
|
3030
|
+
return arr.map((a) => normalizeV1Account(a));
|
|
3031
|
+
}
|
|
3032
|
+
if (typeof conn.getMultipleAccountsInfo === "function") {
|
|
3033
|
+
throw new RpcError(
|
|
3034
|
+
"RPC_HTTP",
|
|
3035
|
+
"fromWeb3Rpc: this web3.js v1 Connection only exposes getMultipleAccountsInfo, which returns raw Buffers \u2014 token balances cannot be parsed and would be silently dropped. Use a Connection that supports getMultipleParsedAccounts, or pass an RPC URL string / kit RPC instead."
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
throw new RpcError("RPC_HTTP", "Connection lacks getMultipleParsedAccounts.");
|
|
3039
|
+
} catch (err) {
|
|
3040
|
+
throw wrapError(err, "RPC_HTTP", "getMultipleAccounts failed");
|
|
3041
|
+
}
|
|
3042
|
+
},
|
|
3043
|
+
async getLatestBlockhash(o) {
|
|
3044
|
+
try {
|
|
3045
|
+
const res = await conn.getLatestBlockhash({
|
|
3046
|
+
commitment: o?.commitment ?? "confirmed"
|
|
3047
|
+
});
|
|
3048
|
+
return res;
|
|
3049
|
+
} catch (err) {
|
|
3050
|
+
throw wrapError(err, "RPC_HTTP", "getLatestBlockhash failed");
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
function fromKit(rpc) {
|
|
3056
|
+
const send = async (builder) => {
|
|
3057
|
+
return await builder.send();
|
|
3058
|
+
};
|
|
3059
|
+
return {
|
|
3060
|
+
async getTransaction(sig, o) {
|
|
3061
|
+
try {
|
|
3062
|
+
const builder = rpc.getTransaction(sig, {
|
|
3063
|
+
encoding: "base64",
|
|
3064
|
+
commitment: o.commitment ?? "confirmed",
|
|
3065
|
+
maxSupportedTransactionVersion: o.maxSupportedTransactionVersion ?? 0
|
|
3066
|
+
});
|
|
3067
|
+
return await send(builder) ?? null;
|
|
3068
|
+
} catch (err) {
|
|
3069
|
+
throw wrapError(err, "RPC_HTTP", "getTransaction failed");
|
|
3070
|
+
}
|
|
3071
|
+
},
|
|
3072
|
+
async simulateTransaction(txBase64, o) {
|
|
3073
|
+
try {
|
|
3074
|
+
const builder = rpc.simulateTransaction(txBase64, {
|
|
3075
|
+
encoding: "base64",
|
|
3076
|
+
sigVerify: o.sigVerify ?? false,
|
|
3077
|
+
replaceRecentBlockhash: o.replaceRecentBlockhash ?? true,
|
|
3078
|
+
commitment: o.commitment ?? "confirmed",
|
|
3079
|
+
innerInstructions: o.innerInstructions ?? true,
|
|
3080
|
+
accounts: o.accounts ? { addresses: o.accounts.addresses, encoding: o.accounts.encoding ?? "jsonParsed" } : void 0
|
|
3081
|
+
});
|
|
3082
|
+
return await send(builder);
|
|
3083
|
+
} catch (err) {
|
|
3084
|
+
throw wrapError(err, "SIMULATION_REJECTED", "simulateTransaction failed");
|
|
3085
|
+
}
|
|
3086
|
+
},
|
|
3087
|
+
async getMultipleAccounts(addresses, o) {
|
|
3088
|
+
if (addresses.length === 0) return [];
|
|
3089
|
+
try {
|
|
3090
|
+
const builder = rpc.getMultipleAccounts(addresses, {
|
|
3091
|
+
commitment: o.commitment ?? "confirmed",
|
|
3092
|
+
encoding: o.encoding ?? "jsonParsed"
|
|
3093
|
+
});
|
|
3094
|
+
const res = await send(builder);
|
|
3095
|
+
return res.value;
|
|
3096
|
+
} catch (err) {
|
|
3097
|
+
throw wrapError(err, "RPC_HTTP", "getMultipleAccounts failed");
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
async getLatestBlockhash(o) {
|
|
3101
|
+
try {
|
|
3102
|
+
const builder = rpc.getLatestBlockhash({ commitment: o?.commitment ?? "confirmed" });
|
|
3103
|
+
const res = await send(
|
|
3104
|
+
builder
|
|
3105
|
+
);
|
|
3106
|
+
return res.value;
|
|
3107
|
+
} catch (err) {
|
|
3108
|
+
throw wrapError(err, "RPC_HTTP", "getLatestBlockhash failed");
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
function normalizeV1Account(a) {
|
|
3114
|
+
if (a === null || typeof a !== "object") return null;
|
|
3115
|
+
const acc4 = a;
|
|
3116
|
+
if (typeof acc4.lamports !== "number") return null;
|
|
3117
|
+
const owner = typeof acc4.owner === "string" ? acc4.owner : acc4.owner != null && typeof acc4.owner.toString === "function" ? acc4.owner.toString() : "";
|
|
3118
|
+
return {
|
|
3119
|
+
lamports: acc4.lamports,
|
|
3120
|
+
owner,
|
|
3121
|
+
data: acc4.data,
|
|
3122
|
+
executable: Boolean(acc4.executable),
|
|
3123
|
+
...typeof acc4.rentEpoch === "number" || typeof acc4.rentEpoch === "string" ? { rentEpoch: acc4.rentEpoch } : {},
|
|
3124
|
+
...typeof acc4.space === "number" ? { space: acc4.space } : {}
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
export { DecodeError, InputError, KNOWN_PROGRAMS, RpcError, SimulationError, SolanaExplainError, buildExplanation, createHttpRpc, createRegistry, decodeInstruction, defaultRegistry, diffBalances, explain, explainInstructions, explainSignature, explainTransaction, fromWeb3Rpc, renderJson, renderMarkdown, renderText };
|
|
3129
|
+
//# sourceMappingURL=index.js.map
|
|
3130
|
+
//# sourceMappingURL=index.js.map
|