solana-resilience-kit 0.1.0 โ 1.0.1
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/README.md +63 -3
- package/dist/cli/index.d.ts +43 -0
- package/dist/cli/index.js +223 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# solana-resilience-kit
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/solana-resilience-kit)
|
|
4
|
+
[](https://www.npmjs.com/package/solana-resilience-kit)
|
|
5
|
+
[](https://www.npmjs.com/package/solana-resilience-kit)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
3
8
|
A vendor-neutral, **client-side resilience and observability layer for Solana dApps**, built on `@solana/kit` (web3.js v2). It unifies the reliability work that is today either left as a do-it-yourself recipe by the official SDK or locked inside a single provider: health-aware multi-RPC failover, a correct transaction send/confirm state machine, simulate-based fee/CU estimation, Jito/MEV routing with automatic RPC fallback, and standardized OpenTelemetry/Datadog telemetry โ behind one clean API that works on top of any set of providers.
|
|
4
9
|
|
|
5
10
|
> **๐ฌ Live demo: [solana-rpc-sdk.pages.dev](https://solana-rpc-sdk.pages.dev)** โ the *RPC Resilience Lab* runs the real SDK in your browser: inject faults against the simulation harness, flip the kit on/off to compare landing rates, or connect a wallet and land a real transaction on devnet. ([source](./demo))
|
|
@@ -7,7 +12,7 @@ A vendor-neutral, **client-side resilience and observability layer for Solana dA
|
|
|
7
12
|
- **Vendor-neutral** โ works with any RPC provider; no gateway, no proprietary key required.
|
|
8
13
|
- **Correct by construction** โ implements the send/confirm semantics most clients get wrong (no double-charge, bounded by `lastValidBlockHeight`).
|
|
9
14
|
- **Built on `@solana/kit`** โ the pool *is* a kit `RpcTransport`, so it drops into existing kit code.
|
|
10
|
-
- **Deterministically tested** โ an in-memory fault-injection cluster reproduces drops, expiry, 429s, desync, and MEV failures;
|
|
15
|
+
- **Deterministically tested** โ an in-memory fault-injection cluster reproduces drops, expiry, 429s, desync, and MEV failures; 102 specs, coverage-gated.
|
|
11
16
|
- **Observable** โ first-class client telemetry to OpenTelemetry / Datadog.
|
|
12
17
|
|
|
13
18
|
## Problem
|
|
@@ -61,7 +66,7 @@ The decisive finding: every robust mitigation today is **either a DIY recipe in
|
|
|
61
66
|
| `JitoRouter` + `TipEstimator` | `src/jito/*` | Bundle routing, dynamic tips, automatic RPC fallback |
|
|
62
67
|
| `OtelMetrics` / `InMemoryMetrics` | `src/observability/metrics.ts` | Client telemetry (latency, failures, slot lag, landings) โ OTel/Datadog |
|
|
63
68
|
| `ResilientWalletAdapter` | `src/wallet/adapter.ts` | Wallet-signed transactions through the resilient pipeline |
|
|
64
|
-
| `Diagnostics` | `src/cli/diagnose.ts` | Probe provider health; explain why a transaction did or didn't land |
|
|
69
|
+
| `Diagnostics` + `solana-resilience-diagnose` CLI | `src/cli/diagnose.ts`, `src/cli/index.ts` | Probe provider health; explain why a transaction did or didn't land (see [Diagnostics CLI](#diagnostics-cli)) |
|
|
65
70
|
|
|
66
71
|
## Architecture
|
|
67
72
|
|
|
@@ -132,6 +137,61 @@ const result = await sender.sendAndConfirm({
|
|
|
132
137
|
// resend error on an already-landed tx as non-terminal.
|
|
133
138
|
```
|
|
134
139
|
|
|
140
|
+
## Diagnostics CLI
|
|
141
|
+
|
|
142
|
+
The package ships an executable, `solana-resilience-diagnose`, built on the same
|
|
143
|
+
`Diagnostics` core (`src/cli/diagnose.ts`). It answers the two questions an
|
|
144
|
+
operator asks when a Solana dApp misbehaves โ *which of my providers is healthy
|
|
145
|
+
and freshest?* and *did this transaction land, expire, or is it still pending?* โ
|
|
146
|
+
without writing any code. Run it with `npx` (no install) or from a dependency's
|
|
147
|
+
`node_modules/.bin`:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Probe provider health across one or more endpoints (reuses the pool's own
|
|
151
|
+
# slot-freshness ranking, so "freshest" matches what routing would pick):
|
|
152
|
+
npx solana-resilience-diagnose probe \
|
|
153
|
+
--rpc https://api.mainnet-beta.solana.com \
|
|
154
|
+
--rpc https://my-backup.rpc
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
ENDPOINT HEALTH SLOT LATENCY FRESHEST
|
|
159
|
+
https://api.mainnet-beta.solana.com ok 287654812 142ms *
|
|
160
|
+
https://my-backup.rpc down - 19ms
|
|
161
|
+
|
|
162
|
+
Freshest: https://api.mainnet-beta.solana.com ยท 1/2 healthy.
|
|
163
|
+
https://my-backup.rpc: fetch failed
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Explain a transaction's outcome point-in-time (no polling loop): it compares
|
|
168
|
+
# the current signature status and block height against lastValidBlockHeight โ
|
|
169
|
+
# the canonical Solana rule โ and never re-signs.
|
|
170
|
+
npx solana-resilience-diagnose explain \
|
|
171
|
+
--rpc https://api.mainnet-beta.solana.com \
|
|
172
|
+
--sig 5xRe...your-signature \
|
|
173
|
+
--lvbh 287654321
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Signature: 5xRe...your-signature
|
|
178
|
+
Verdict: EXPIRED
|
|
179
|
+
block height 287654400 exceeded lastValidBlockHeight 287654321; the blockhash
|
|
180
|
+
expired before the transaction landed (silent drop or congestion). Rebuild with
|
|
181
|
+
a fresh blockhash โ do NOT re-sign the same one.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
| Flag | Command | Meaning |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `--rpc <url>` | both | RPC endpoint URL. Repeat for `probe`; exactly one for `explain`. Accepts `--rpc=<url>` too. |
|
|
187
|
+
| `--sig <sig>` | `explain` | Transaction signature to explain. |
|
|
188
|
+
| `--lvbh <n>` | `explain` | `lastValidBlockHeight` the transaction was built against. |
|
|
189
|
+
|
|
190
|
+
**Exit codes:** `0` success ยท `1` a substantive failure (no healthy endpoint, or an
|
|
191
|
+
expired transaction) ยท `2` a usage error. Run with no command, `help`, or
|
|
192
|
+
`--help` to print usage. The argv parser is a pure, network-free function and is
|
|
193
|
+
unit-tested in isolation (`test/cli/argv.test.ts`).
|
|
194
|
+
|
|
135
195
|
## Testing your own code against the fault harness
|
|
136
196
|
|
|
137
197
|
The deterministic Solana cluster simulator the SDK is tested with is shipped as a
|
|
@@ -173,7 +233,7 @@ Solana's failure modes โ silent drops, blockhash expiry, 429s, lagging-node de
|
|
|
173
233
|
- **Injected `sleep`.** Time-based loops take a `sleep` dependency; tests pass one that advances the mock clock, so the whole state machine runs instantly and deterministically.
|
|
174
234
|
|
|
175
235
|
```bash
|
|
176
|
-
npm test # full suite (harness + all modules),
|
|
236
|
+
npm test # full suite (harness + all modules), 102 specs
|
|
177
237
|
npm run test:cov # coverage with the thresholds enforced
|
|
178
238
|
npm run typecheck # tsc --noEmit
|
|
179
239
|
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { Rpc, SolanaRpcApi } from "@solana/kit";
|
|
3
|
+
import { SdkError } from "../errors.js";
|
|
4
|
+
import { Diagnostics } from "./diagnose.js";
|
|
5
|
+
import type { ProbeReport, TxDiagnosis } from "./diagnose.js";
|
|
6
|
+
/** A malformed invocation (unknown command, missing/invalid flag). Message carries usage text. */
|
|
7
|
+
export declare class CliUsageError extends SdkError {
|
|
8
|
+
}
|
|
9
|
+
export interface ProbeCommand {
|
|
10
|
+
command: "probe";
|
|
11
|
+
rpcUrls: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface ExplainCommand {
|
|
14
|
+
command: "explain";
|
|
15
|
+
rpcUrl: string;
|
|
16
|
+
signature: string;
|
|
17
|
+
lastValidBlockHeight: bigint;
|
|
18
|
+
}
|
|
19
|
+
export type ParsedCommand = ProbeCommand | ExplainCommand;
|
|
20
|
+
export declare const USAGE = "solana-resilience-diagnose \u2014 probe RPC health and explain transaction outcomes\n\nUsage:\n solana-resilience-diagnose probe --rpc <url> [--rpc <url> ...]\n solana-resilience-diagnose explain --rpc <url> --sig <signature> --lvbh <lastValidBlockHeight>\n\nCommands:\n probe Probe each endpoint's slot / latency / health and report the freshest.\n explain Point-in-time verdict (confirmed | expired | pending) for one signature.\n\nFlags:\n --rpc <url> RPC endpoint URL. Repeat for probe; exactly one for explain.\n --sig <sig> Transaction signature to explain.\n --lvbh <n> lastValidBlockHeight the transaction was built against.\n\nExamples:\n solana-resilience-diagnose probe --rpc https://api.mainnet-beta.solana.com --rpc https://my-backup.rpc\n solana-resilience-diagnose explain --rpc https://api.mainnet-beta.solana.com --sig 5xRe... --lvbh 287654321";
|
|
21
|
+
/**
|
|
22
|
+
* Pure argv parser. Accepts argv WITHOUT the `node script` prefix (i.e.
|
|
23
|
+
* `process.argv.slice(2)`). Supports `--flag value` and `--flag=value`. Throws
|
|
24
|
+
* {@link CliUsageError} (with help text) on anything malformed. No I/O.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseArgs(argv: string[]): ParsedCommand;
|
|
27
|
+
/** Render a {@link ProbeReport} as an aligned text table plus a summary line. */
|
|
28
|
+
export declare function formatProbeReport(report: ProbeReport): string;
|
|
29
|
+
/** Render an {@link TxDiagnosis} as a human verdict. */
|
|
30
|
+
export declare function formatDiagnosis(signature: string, diag: TxDiagnosis): string;
|
|
31
|
+
export interface CliDeps {
|
|
32
|
+
/** Build a kit RPC from a URL. Defaults to `createSolanaRpc`. */
|
|
33
|
+
createRpc?: (url: string) => Rpc<SolanaRpcApi>;
|
|
34
|
+
/** Sink for output lines. Defaults to `console.log`. */
|
|
35
|
+
log?: (line: string) => void;
|
|
36
|
+
/** Override the diagnostics core (tests inject a shared HealthMonitor here). */
|
|
37
|
+
diagnostics?: Diagnostics;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Execute one parsed command. Returns the process exit code. All side effects
|
|
41
|
+
* (RPC, stdout) go through {@link CliDeps}, so tests run it network-free.
|
|
42
|
+
*/
|
|
43
|
+
export declare function run(argv: string[], deps?: CliDeps): Promise<number>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* solana-resilience-diagnose โ the executable CLI over the {@link Diagnostics} core.
|
|
4
|
+
*
|
|
5
|
+
* Two commands map 1:1 to the two questions an operator asks when a Solana dApp
|
|
6
|
+
* misbehaves:
|
|
7
|
+
*
|
|
8
|
+
* probe --rpc <url> [--rpc <url> ...] โ which providers are healthy, and which is freshest?
|
|
9
|
+
* explain --rpc <url> --sig <signature> --lvbh <n> โ did this tx land, expire, or is it still pending?
|
|
10
|
+
*
|
|
11
|
+
* Layering: {@link parseArgs} is a PURE, network-free function (turns argv into a
|
|
12
|
+
* typed command or throws {@link CliUsageError}); {@link formatProbeReport} /
|
|
13
|
+
* {@link formatDiagnosis} are pure renderers; {@link run} wires kit RPC + stdout
|
|
14
|
+
* through injectable deps so it stays deterministic under the harness. Only the
|
|
15
|
+
* process bootstrap at the very bottom touches process.argv / real RPC, and is
|
|
16
|
+
* integration-only โ nothing else executes at import time.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes: 0 success ยท 1 a substantive failure (no healthy endpoint / expired
|
|
19
|
+
* tx) ยท 2 a usage error.
|
|
20
|
+
*/
|
|
21
|
+
import { createSolanaRpc } from "@solana/kit";
|
|
22
|
+
import { pathToFileURL } from "node:url";
|
|
23
|
+
import { SdkError } from "../errors.js";
|
|
24
|
+
import { Diagnostics } from "./diagnose.js";
|
|
25
|
+
/** A malformed invocation (unknown command, missing/invalid flag). Message carries usage text. */
|
|
26
|
+
export class CliUsageError extends SdkError {
|
|
27
|
+
}
|
|
28
|
+
export const USAGE = `solana-resilience-diagnose โ probe RPC health and explain transaction outcomes
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
solana-resilience-diagnose probe --rpc <url> [--rpc <url> ...]
|
|
32
|
+
solana-resilience-diagnose explain --rpc <url> --sig <signature> --lvbh <lastValidBlockHeight>
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
probe Probe each endpoint's slot / latency / health and report the freshest.
|
|
36
|
+
explain Point-in-time verdict (confirmed | expired | pending) for one signature.
|
|
37
|
+
|
|
38
|
+
Flags:
|
|
39
|
+
--rpc <url> RPC endpoint URL. Repeat for probe; exactly one for explain.
|
|
40
|
+
--sig <sig> Transaction signature to explain.
|
|
41
|
+
--lvbh <n> lastValidBlockHeight the transaction was built against.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
solana-resilience-diagnose probe --rpc https://api.mainnet-beta.solana.com --rpc https://my-backup.rpc
|
|
45
|
+
solana-resilience-diagnose explain --rpc https://api.mainnet-beta.solana.com --sig 5xRe... --lvbh 287654321`;
|
|
46
|
+
/**
|
|
47
|
+
* Pure argv parser. Accepts argv WITHOUT the `node script` prefix (i.e.
|
|
48
|
+
* `process.argv.slice(2)`). Supports `--flag value` and `--flag=value`. Throws
|
|
49
|
+
* {@link CliUsageError} (with help text) on anything malformed. No I/O.
|
|
50
|
+
*/
|
|
51
|
+
export function parseArgs(argv) {
|
|
52
|
+
const [command, ...rest] = argv;
|
|
53
|
+
if (command === undefined ||
|
|
54
|
+
command === "help" ||
|
|
55
|
+
command === "--help" ||
|
|
56
|
+
command === "-h") {
|
|
57
|
+
throw usage();
|
|
58
|
+
}
|
|
59
|
+
switch (command) {
|
|
60
|
+
case "probe":
|
|
61
|
+
return parseProbe(rest);
|
|
62
|
+
case "explain":
|
|
63
|
+
return parseExplain(rest);
|
|
64
|
+
default:
|
|
65
|
+
throw usage(`unknown command "${command}".`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function parseProbe(args) {
|
|
69
|
+
const flags = collectFlags(args);
|
|
70
|
+
const rpcUrls = flags.get("rpc") ?? [];
|
|
71
|
+
if (rpcUrls.length === 0) {
|
|
72
|
+
throw usage('"probe" requires at least one --rpc <url>.');
|
|
73
|
+
}
|
|
74
|
+
return { command: "probe", rpcUrls };
|
|
75
|
+
}
|
|
76
|
+
function parseExplain(args) {
|
|
77
|
+
const flags = collectFlags(args);
|
|
78
|
+
const rpcUrl = single(flags, "rpc");
|
|
79
|
+
const signature = single(flags, "sig");
|
|
80
|
+
const lvbhRaw = single(flags, "lvbh");
|
|
81
|
+
let lastValidBlockHeight;
|
|
82
|
+
try {
|
|
83
|
+
lastValidBlockHeight = BigInt(lvbhRaw);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw usage(`--lvbh must be an integer, got "${lvbhRaw}".`);
|
|
87
|
+
}
|
|
88
|
+
if (lastValidBlockHeight < 0n) {
|
|
89
|
+
throw usage(`--lvbh must be non-negative, got "${lvbhRaw}".`);
|
|
90
|
+
}
|
|
91
|
+
return { command: "explain", rpcUrl, signature, lastValidBlockHeight };
|
|
92
|
+
}
|
|
93
|
+
/** Parse repeated `--flag value` / `--flag=value` tokens into a multimap. */
|
|
94
|
+
function collectFlags(args) {
|
|
95
|
+
const flags = new Map();
|
|
96
|
+
const queue = [...args];
|
|
97
|
+
while (queue.length > 0) {
|
|
98
|
+
const tok = queue.shift();
|
|
99
|
+
if (!tok.startsWith("--")) {
|
|
100
|
+
throw usage(`unexpected argument "${tok}".`);
|
|
101
|
+
}
|
|
102
|
+
const body = tok.slice(2);
|
|
103
|
+
const eq = body.indexOf("=");
|
|
104
|
+
let key;
|
|
105
|
+
let value;
|
|
106
|
+
if (eq >= 0) {
|
|
107
|
+
key = body.slice(0, eq);
|
|
108
|
+
value = body.slice(eq + 1);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
key = body;
|
|
112
|
+
const next = queue[0];
|
|
113
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
114
|
+
value = queue.shift();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (value === undefined || value === "") {
|
|
118
|
+
throw usage(`flag --${key} requires a value.`);
|
|
119
|
+
}
|
|
120
|
+
const list = flags.get(key) ?? [];
|
|
121
|
+
list.push(value);
|
|
122
|
+
flags.set(key, list);
|
|
123
|
+
}
|
|
124
|
+
return flags;
|
|
125
|
+
}
|
|
126
|
+
/** Read a flag expected exactly once. */
|
|
127
|
+
function single(flags, key) {
|
|
128
|
+
const values = flags.get(key);
|
|
129
|
+
if (values === undefined || values.length === 0) {
|
|
130
|
+
throw usage(`"explain" requires --${key} <value>.`);
|
|
131
|
+
}
|
|
132
|
+
if (values.length > 1) {
|
|
133
|
+
throw usage(`--${key} may be given only once.`);
|
|
134
|
+
}
|
|
135
|
+
return values[0];
|
|
136
|
+
}
|
|
137
|
+
function usage(detail) {
|
|
138
|
+
return new CliUsageError(detail ? `${detail}\n\n${USAGE}` : USAGE);
|
|
139
|
+
}
|
|
140
|
+
/** Render a {@link ProbeReport} as an aligned text table plus a summary line. */
|
|
141
|
+
export function formatProbeReport(report) {
|
|
142
|
+
const header = ["ENDPOINT", "HEALTH", "SLOT", "LATENCY", "FRESHEST"];
|
|
143
|
+
const rows = report.endpoints.map((e) => [
|
|
144
|
+
e.name,
|
|
145
|
+
e.ok ? "ok" : "down",
|
|
146
|
+
e.slot === null ? "-" : e.slot.toString(),
|
|
147
|
+
`${e.latencyMs}ms`,
|
|
148
|
+
e.ok && e.name === report.freshest ? "*" : "",
|
|
149
|
+
]);
|
|
150
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
151
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
152
|
+
const lines = [fmt(header), ...rows.map(fmt), ""];
|
|
153
|
+
lines.push(report.healthyCount === 0
|
|
154
|
+
? `No healthy endpoints (0/${report.endpoints.length} up).`
|
|
155
|
+
: `Freshest: ${report.freshest} ยท ${report.healthyCount}/${report.endpoints.length} healthy.`);
|
|
156
|
+
for (const e of report.endpoints) {
|
|
157
|
+
if (!e.ok && e.error)
|
|
158
|
+
lines.push(` ${e.name}: ${e.error}`);
|
|
159
|
+
}
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
/** Render an {@link TxDiagnosis} as a human verdict. */
|
|
163
|
+
export function formatDiagnosis(signature, diag) {
|
|
164
|
+
const head = `Signature: ${signature}`;
|
|
165
|
+
switch (diag.status) {
|
|
166
|
+
case "confirmed":
|
|
167
|
+
return `${head}\nVerdict: CONFIRMED (landed in slot ${diag.slot})`;
|
|
168
|
+
case "expired":
|
|
169
|
+
return `${head}\nVerdict: EXPIRED\n${diag.reason}`;
|
|
170
|
+
case "pending":
|
|
171
|
+
return `${head}\nVerdict: PENDING\n${diag.reason}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/* v8 ignore start -- real-RPC + stdout wiring; exercised by the installed binary, not unit-tested */
|
|
175
|
+
const defaultCreateRpc = (url) => createSolanaRpc(url);
|
|
176
|
+
const defaultLog = (line) => {
|
|
177
|
+
console.log(line);
|
|
178
|
+
};
|
|
179
|
+
/* v8 ignore stop */
|
|
180
|
+
/**
|
|
181
|
+
* Execute one parsed command. Returns the process exit code. All side effects
|
|
182
|
+
* (RPC, stdout) go through {@link CliDeps}, so tests run it network-free.
|
|
183
|
+
*/
|
|
184
|
+
export async function run(argv, deps = {}) {
|
|
185
|
+
const log = deps.log ?? defaultLog;
|
|
186
|
+
/* v8 ignore next -- default RPC factory is real-network, covered via the binary */
|
|
187
|
+
const createRpc = deps.createRpc ?? defaultCreateRpc;
|
|
188
|
+
let parsed;
|
|
189
|
+
try {
|
|
190
|
+
parsed = parseArgs(argv);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err instanceof CliUsageError) {
|
|
194
|
+
log(err.message);
|
|
195
|
+
return 2;
|
|
196
|
+
}
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
const diag = deps.diagnostics ?? new Diagnostics();
|
|
200
|
+
if (parsed.command === "probe") {
|
|
201
|
+
const targets = parsed.rpcUrls.map((url) => ({ name: url, rpc: createRpc(url) }));
|
|
202
|
+
const report = await diag.probeEndpoints(targets);
|
|
203
|
+
log(formatProbeReport(report));
|
|
204
|
+
return report.healthyCount > 0 ? 0 : 1;
|
|
205
|
+
}
|
|
206
|
+
const result = await diag.explainTransaction(createRpc(parsed.rpcUrl), {
|
|
207
|
+
signature: parsed.signature,
|
|
208
|
+
lastValidBlockHeight: parsed.lastValidBlockHeight,
|
|
209
|
+
});
|
|
210
|
+
log(formatDiagnosis(parsed.signature, result));
|
|
211
|
+
return result.status === "expired" ? 1 : 0;
|
|
212
|
+
}
|
|
213
|
+
/* v8 ignore start -- process bootstrap; only runs when invoked as the binary */
|
|
214
|
+
const entry = process.argv[1];
|
|
215
|
+
if (entry !== undefined && import.meta.url === pathToFileURL(entry).href) {
|
|
216
|
+
run(process.argv.slice(2)).then((code) => {
|
|
217
|
+
process.exitCode = code;
|
|
218
|
+
}, (err) => {
|
|
219
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/* v8 ignore stop */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-resilience-kit",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Vendor-neutral, client-side resilience and observability layer for Solana dApps, built on @solana/kit (web3.js v2).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"main": "./dist/index.js",
|
|
32
32
|
"module": "./dist/index.js",
|
|
33
33
|
"types": "./dist/index.d.ts",
|
|
34
|
+
"bin": {
|
|
35
|
+
"solana-resilience-diagnose": "./dist/cli/index.js"
|
|
36
|
+
},
|
|
34
37
|
"exports": {
|
|
35
38
|
".": {
|
|
36
39
|
"types": "./dist/index.d.ts",
|
|
@@ -54,6 +57,7 @@
|
|
|
54
57
|
"test:watch": "vitest",
|
|
55
58
|
"test:cov": "vitest run --coverage",
|
|
56
59
|
"typecheck": "tsc --noEmit",
|
|
60
|
+
"docs:api": "typedoc",
|
|
57
61
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
58
62
|
},
|
|
59
63
|
"dependencies": {
|
|
@@ -65,6 +69,7 @@
|
|
|
65
69
|
"@types/node": "^22.19.21",
|
|
66
70
|
"@vitest/coverage-v8": "^4.1.9",
|
|
67
71
|
"tsx": "^4.22.4",
|
|
72
|
+
"typedoc": "^0.28.19",
|
|
68
73
|
"typescript": "^5.6.0",
|
|
69
74
|
"vitest": "^4.1.9"
|
|
70
75
|
}
|