@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VTX Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
```
|
|
2
|
+
██╗ ██╗████████╗██╗ ██╗ ██╗ █████╗ ██████╗ ███████╗
|
|
3
|
+
██║ ██║╚══██╔══╝╚██╗██╔╝ ██║ ██╔══██╗██╔══██╗██╔════╝
|
|
4
|
+
██║ ██║ ██║ ╚███╔╝ ██║ ███████║██████╔╝███████╗
|
|
5
|
+
╚██╗ ██╔╝ ██║ ██╔██╗ ██║ ██╔══██║██╔══██╗╚════██║
|
|
6
|
+
╚████╔╝ ██║ ██╔╝ ██╗ ███████╗██║ ██║██████╔╝███████║
|
|
7
|
+
╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
# solana-explain
|
|
11
|
+
|
|
12
|
+
### Plain-English "what this actually does" reports for any Solana transaction — no IDLs required.
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/@vtx-labs/solana-explain)
|
|
15
|
+
[](https://github.com/VTX-Labs/solana-explain/actions)
|
|
16
|
+
[](./LICENSE)
|
|
17
|
+
[](https://nodejs.org)
|
|
18
|
+
[](https://www.typescriptlang.org)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
`@vtx-labs/solana-explain` turns any Solana **transaction signature**, **raw/base64 transaction**, or **unsigned instruction set** into a clear report of what it actually does — net balance deltas, SOL & SPL/Token-2022 transfers, account creations, approvals/revokes, and program invocations.
|
|
23
|
+
|
|
24
|
+
It **fetches** (by signature) or **simulates** (for unsigned/raw) a transaction over any RPC URL, diffs pre/post account + token balances, and decodes instructions with bundled byte-level decoders for the most common programs (System, SPL Token, Token-2022, ATA, Memo, Compute Budget) plus best-effort recognition for Metaplex, Jupiter, Raydium, Orca, Stake, Vote, and more.
|
|
25
|
+
|
|
26
|
+
- **No user-supplied IDLs.** The _truth_ of "what changed" is the balance diff; decoding adds names and intent.
|
|
27
|
+
- **Dependency-light.** One runtime dep (`bs58`). `@solana/web3.js` is an optional peer — bring your own RPC or use the built-in `fetch` client.
|
|
28
|
+
- **Library + CLI.** A structured `ExplainResult` for programs, and a colorized CLI for humans.
|
|
29
|
+
- **`npx` zero-config.** `npx @vtx-labs/solana-explain <signature> --rpc <url>`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @vtx-labs/solana-explain
|
|
37
|
+
# or run the CLI with no install:
|
|
38
|
+
npx @vtx-labs/solana-explain <signature> --rpc https://your-rpc
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> Requires Node 18+ (uses the global `fetch`).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick start (library)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { explainSignature, renderText } from "@vtx-labs/solana-explain";
|
|
49
|
+
|
|
50
|
+
const result = await explainSignature("5Nq...d8", {
|
|
51
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
52
|
+
commitment: "confirmed",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log(result.summary);
|
|
56
|
+
// "Swapped 1.500015 SOL for 248.91 USDC via Jupiter v6, paid 0.000005 SOL fee."
|
|
57
|
+
|
|
58
|
+
console.log(renderText(result, { color: true }));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Simulate an **unsigned** transaction (base64 / base58 / `Uint8Array`):
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { explainTransaction } from "@vtx-labs/solana-explain";
|
|
65
|
+
|
|
66
|
+
const result = await explainTransaction(base64Tx, {
|
|
67
|
+
rpc: process.env.SOLANA_RPC_URL!,
|
|
68
|
+
// sigVerify defaults to false, replaceRecentBlockhash defaults to true,
|
|
69
|
+
// so unsigned txs simulate cleanly.
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Explain raw **instructions** before signing in a wallet:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { explainInstructions } from "@vtx-labs/solana-explain";
|
|
77
|
+
|
|
78
|
+
const result = await explainInstructions(instructions, {
|
|
79
|
+
rpc: rpcUrl,
|
|
80
|
+
feePayer: walletPubkey, // required to assemble a simulatable message
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Auto-detect the input kind:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { explain } from "@vtx-labs/solana-explain";
|
|
88
|
+
|
|
89
|
+
const result = await explain(inputThatCouldBeAnything, { rpc: rpcUrl });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## CLI
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
solana-explain <input> [options]
|
|
98
|
+
solana-explain --stdin [options]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`<input>` is auto-detected: a base58 signature, a base64/base58 serialized transaction, a file path, or `-`/`--stdin` for piped data.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
-r, --rpc <url> RPC endpoint URL. Falls back to $SOLANA_RPC_URL, then $RPC_URL.
|
|
105
|
+
-c, --commitment <c> processed | confirmed | finalized (default: confirmed)
|
|
106
|
+
--cluster <name> Preset for -r: mainnet | devnet | testnet | localnet
|
|
107
|
+
--simulate Force the simulate path even for a signature input
|
|
108
|
+
--focus <pubkey> Phrase balance deltas from this account's perspective
|
|
109
|
+
-j, --json Emit machine-readable JSON (the ExplainResult; BigInts as strings)
|
|
110
|
+
--markdown Emit Markdown (great for issues / PRs / docs)
|
|
111
|
+
--raw Include the raw RPC payload under result.raw (with -j)
|
|
112
|
+
--no-color Disable ANSI color (auto-off when !isTTY or $NO_COLOR)
|
|
113
|
+
--max-tx-version <n> Max supported tx version for getTransaction (default: 0)
|
|
114
|
+
--timeout <ms> Per-run network timeout (default: 30000)
|
|
115
|
+
--stdin Read input from stdin
|
|
116
|
+
--file <path> Read input from a file
|
|
117
|
+
--fee-payer <pubkey> Fee payer for JSON instruction-set input
|
|
118
|
+
-q, --quiet Only the summary line + nonzero exit on failure
|
|
119
|
+
-v, --verbose Show every instruction incl. inner/CPI tree + raw args
|
|
120
|
+
--version Print version and exit
|
|
121
|
+
-h, --help Show help and exit
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Example output
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Solana transaction · confirmed · slot 287,330,114 · 2026-05-30 14:02 UTC
|
|
128
|
+
Signature 5Nq...d8 ✓ Success
|
|
129
|
+
|
|
130
|
+
Summary Swapped 1.500015 SOL for 248.91 USDC via Jupiter v6, paid 0.000005 SOL fee.
|
|
131
|
+
|
|
132
|
+
Balance changes
|
|
133
|
+
7Bf…a21 -1.500015 SOL +248.91 USDC
|
|
134
|
+
9Qm…f0 +1.500000 SOL -248.91 USDC
|
|
135
|
+
|
|
136
|
+
Actions
|
|
137
|
+
1. Set compute budget: unit limit 200,000, price 1,000 µ-lamports
|
|
138
|
+
2. Transfer 1.500015 SOL 7Bf…a21 → 9Qm…f0
|
|
139
|
+
3. Call Jupiter v6 — effect inferred from balance diff (no IDL)
|
|
140
|
+
4. Transfer 248.91 USDC 9Qm…f0 → 7Bf…a21
|
|
141
|
+
|
|
142
|
+
Programs Compute Budget · System · SPL Token · Jupiter v6
|
|
143
|
+
Fee 0.000005 SOL Compute 142,318 units
|
|
144
|
+
|
|
145
|
+
⚠ 1 warning
|
|
146
|
+
· Jupiter v6 recognized by program id; semantics inferred from balance diff (no IDL).
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Exit codes
|
|
150
|
+
|
|
151
|
+
| Code | Meaning |
|
|
152
|
+
| ----- | ------------------------------------------------------------------------------------ |
|
|
153
|
+
| `0` | Success — explained, on-chain/sim success |
|
|
154
|
+
| `2` | Explained, but the transaction **failed/reverted** (`success:false`) — still printed |
|
|
155
|
+
| `3` | Transaction not found at the requested commitment |
|
|
156
|
+
| `4` | Invalid input (bad signature/encoding/empty) |
|
|
157
|
+
| `5` | RPC/network error (HTTP, JSON-RPC, timeout) |
|
|
158
|
+
| `6` | Simulation rejected by the node (e.g. bad blockhash) |
|
|
159
|
+
| `1` | Unexpected internal error |
|
|
160
|
+
| `130` | Aborted via SIGINT (Ctrl-C) |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## API
|
|
165
|
+
|
|
166
|
+
All exports are **named** (no default exports). Types are `type`-importable.
|
|
167
|
+
|
|
168
|
+
### Primary helpers
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
function explainSignature(
|
|
172
|
+
signature: string,
|
|
173
|
+
options: ExplainSignatureOptions,
|
|
174
|
+
): Promise<ExplainResult>;
|
|
175
|
+
function explainTransaction(
|
|
176
|
+
tx: string | Uint8Array,
|
|
177
|
+
options: ExplainTransactionOptions,
|
|
178
|
+
): Promise<ExplainResult>;
|
|
179
|
+
function explainInstructions(
|
|
180
|
+
instructions: ExplainInstruction[],
|
|
181
|
+
options: ExplainInstructionsOptions,
|
|
182
|
+
): Promise<ExplainResult>;
|
|
183
|
+
function explain(
|
|
184
|
+
input: string | Uint8Array | ExplainInstruction[],
|
|
185
|
+
options: ExplainOptions,
|
|
186
|
+
): Promise<ExplainResult>;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Building blocks (pure, RPC-free where possible)
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
function buildExplanation(input: ExplainInput): ExplainResult; // diff + decode + narrate
|
|
193
|
+
function decodeInstruction(
|
|
194
|
+
ix: CompiledInstructionView,
|
|
195
|
+
registry?: ProgramRegistry,
|
|
196
|
+
): DecodedInstruction;
|
|
197
|
+
function diffBalances(
|
|
198
|
+
pre: BalanceSnapshot,
|
|
199
|
+
post: BalanceSnapshot,
|
|
200
|
+
): BalanceDelta[];
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Rendering (pure) — also at `@vtx-labs/solana-explain/render`
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
function renderText(result: ExplainResult, opts?: RenderOptions): string; // colorized when opts.color
|
|
207
|
+
function renderMarkdown(result: ExplainResult): string;
|
|
208
|
+
function renderJson(result: ExplainResult, opts?: { pretty?: boolean }): string; // BigInt-safe
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### RPC abstraction
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
function createHttpRpc(url: string, opts?: HttpRpcOptions): RpcClient; // zero-dep fetch client
|
|
215
|
+
function fromWeb3Rpc(rpc: unknown): RpcClient; // adapt web3.js v1 / @solana/kit
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The library only ever talks to the `RpcClient` interface, so it is network-source-agnostic and `@solana/web3.js` stays an optional peer dependency.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { fromWeb3Rpc } from "@vtx-labs/solana-explain";
|
|
222
|
+
import { Connection } from "@solana/web3.js";
|
|
223
|
+
|
|
224
|
+
const rpc = fromWeb3Rpc(new Connection(url));
|
|
225
|
+
const result = await explainSignature(sig, { rpc });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Program registry — also at `@vtx-labs/solana-explain/programs`
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const defaultRegistry: ProgramRegistry;
|
|
232
|
+
function createRegistry(decoders?: ProgramDecoder[]): ProgramRegistry;
|
|
233
|
+
const KNOWN_PROGRAMS: Readonly<Record<string, KnownProgramInfo>>;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Register a custom byte-level decoder without an IDL:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { createRegistry } from "@vtx-labs/solana-explain/programs";
|
|
240
|
+
import type { ProgramDecoder } from "@vtx-labs/solana-explain";
|
|
241
|
+
|
|
242
|
+
const myDecoder: ProgramDecoder = {
|
|
243
|
+
programId: "MyProgram1111111111111111111111111111111111",
|
|
244
|
+
name: "My Program",
|
|
245
|
+
kind: "other",
|
|
246
|
+
decode(ix) {
|
|
247
|
+
// ix.data is a Uint8Array; return { decoded: false } on anything unexpected.
|
|
248
|
+
return { program: "My Program", type: "doThing", decoded: true, args: {} };
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = await explainSignature(sig, {
|
|
253
|
+
rpc: rpcUrl,
|
|
254
|
+
registry: createRegistry([myDecoder]),
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Errors
|
|
259
|
+
|
|
260
|
+
Every public async function rejects **only** with a `SolanaExplainError` subclass; the original is preserved on `.cause`.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
class SolanaExplainError extends Error {
|
|
264
|
+
code: ErrorCode;
|
|
265
|
+
cause?: unknown;
|
|
266
|
+
hint?: string;
|
|
267
|
+
}
|
|
268
|
+
class RpcError extends SolanaExplainError {} // transport / JSON-RPC
|
|
269
|
+
class DecodeError extends SolanaExplainError {} // malformed tx bytes
|
|
270
|
+
class SimulationError extends SolanaExplainError {} // sim rejected / reverted
|
|
271
|
+
class InputError extends SolanaExplainError {} // bad signature / encoding / empty
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
A **failed / reverted** transaction does **not** throw — it returns a fully-populated `ExplainResult` with `success:false`, the decoded program error, and all balance deltas (fees were charged), because you usually want to know _why_ it failed.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## How it works
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
input → detect → acquire (signature | simulate) → decode message → diff balances
|
|
282
|
+
→ correlate decoded ix + deltas → summarize → render
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
1. **Input** — classify signature vs tx-bytes vs instruction-set, normalizing encodings.
|
|
286
|
+
2. **RPC** — a tiny `RpcClient` over `fetch` (timeout, single jittered retry on 429/5xx, strict JSON-RPC mapping), or your own web3.js connection.
|
|
287
|
+
3. **Acquire** — _signature path_ uses `getTransaction` (authoritative pre/post token balances, fee, inner instructions, `err`); _simulate path_ fetches pre-state via `getMultipleAccounts` and post-state via `simulateTransaction`'s returned accounts, then diffs identically.
|
|
288
|
+
4. **Decode** — a hand-written legacy + v0 wire-message parser plus byte-level decoders (no `borsh`/`buffer-layout`; tiny fixed-byte enums via `DataView`).
|
|
289
|
+
5. **Diff + narrate** — signed lamport/token deltas, fused with decoded instructions into high-confidence `Action`s, then a one-line headline.
|
|
290
|
+
6. **Render** — sectioned text (hand-rolled ANSI, no `chalk`), Markdown, or BigInt-safe JSON.
|
|
291
|
+
|
|
292
|
+
All numeric on-chain quantities are `bigint`; `ui*` string fields carry human/decimal formatting so callers never lose precision.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Edge cases handled
|
|
297
|
+
|
|
298
|
+
- Empty / whitespace input, wrong-length signatures (reports decoded byte count), garbage base64/base58.
|
|
299
|
+
- Ambiguous encoding (valid as both base58 and base64) — prefers the wire-message-parsing interpretation, warns on tie.
|
|
300
|
+
- Transaction not found at the requested commitment (`TX_NOT_FOUND`, suggests `--commitment finalized`).
|
|
301
|
+
- **Failed/reverted** transactions — `success:false`, decoded `InstructionError` → human text, deltas still shown.
|
|
302
|
+
- Versioned (v0) txs with address lookup tables — resolved via `loadedAddresses` on the signature path; warns `lut-unresolved` on the simulate path while still diffing resolved writable accounts.
|
|
303
|
+
- `maxSupportedTransactionVersion` too low → `UNSUPPORTED_TX_VERSION` with a hint.
|
|
304
|
+
- Token-2022 extensions (transfer fees, hooks, confidential transfer) — base transfer decoded; extension ixs flagged, fee impact still visible in the diff.
|
|
305
|
+
- Memos with invalid UTF-8 / huge payloads — lossy-decoded with replacement chars, truncated for display.
|
|
306
|
+
- Unchecked SPL transfers without decimals — pulled from token-balance entries; `ambiguous-amount` warning when unavailable.
|
|
307
|
+
- wSOL wrap/unwrap (`syncNative`), self-transfers (no net movement), zero-amount transfers, account created-and-closed in one tx.
|
|
308
|
+
- RPC HTML error pages, 429/5xx (one retry then `RPC_HTTP`), timeouts (`RPC_TIMEOUT`), SIGINT cancellation (exit 130).
|
|
309
|
+
- BigInt JSON serialization (never throws), Compute Budget prices beyond `Number.MAX_SAFE_INTEGER` kept as `bigint`.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Scripts
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm run build # tsup → ESM + .d.ts
|
|
317
|
+
npm test # vitest
|
|
318
|
+
npm run typecheck # tsc --noEmit (strict)
|
|
319
|
+
npm run dev # tsx src/cli/index.ts
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
<div align="center">
|
|
325
|
+
|
|
326
|
+
Built by [**VTX Labs**](https://vtxlabs.dev) · MIT
|
|
327
|
+
|
|
328
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* solana-explain CLI. Thin orchestrator over the library:
|
|
4
|
+
* arg parsing (util.parseArgs), stdin/file reading, env-var RPC fallback,
|
|
5
|
+
* SIGINT → exit 130, and exit-code mapping.
|
|
6
|
+
*/
|
|
7
|
+
declare function run(argv: string[], io: {
|
|
8
|
+
stdout: NodeJS.WriteStream;
|
|
9
|
+
stderr: NodeJS.WriteStream;
|
|
10
|
+
env: NodeJS.ProcessEnv;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}): Promise<number>;
|
|
13
|
+
|
|
14
|
+
export { run };
|