@thesight/sdk 0.1.1 → 0.2.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/README.md +135 -18
- package/dist/index.cjs +252 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -27
- package/dist/index.d.ts +171 -27
- package/dist/index.js +255 -113
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,12 +13,24 @@ correlation — consumable by the Sight dashboard or any APM backend
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
pnpm add @thesight/sdk
|
|
16
|
+
# or npm install @thesight/sdk
|
|
17
|
+
# or yarn add @thesight/sdk
|
|
16
18
|
```
|
|
17
19
|
|
|
18
20
|
All OpenTelemetry dependencies are bundled transitively — you do **not**
|
|
19
21
|
need to `pnpm add @opentelemetry/*` separately.
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Ships both ESM and CJS builds. Works from modern TypeScript projects,
|
|
24
|
+
Node ESM, legacy CJS via `require()`, bundlers, and serverless runtimes.
|
|
25
|
+
|
|
26
|
+
## Quickstart — `InstrumentedConnection` (automatic spans)
|
|
27
|
+
|
|
28
|
+
The simplest pattern: swap your `Connection` for `InstrumentedConnection`
|
|
29
|
+
and every transaction you send through it gets a span automatically —
|
|
30
|
+
no matter whether you're calling it directly, going through an Anchor
|
|
31
|
+
`provider.sendAndConfirm`, using web3.js's top-level
|
|
32
|
+
`sendAndConfirmTransaction`, or letting a wallet adapter drive the
|
|
33
|
+
flow.
|
|
22
34
|
|
|
23
35
|
```ts
|
|
24
36
|
import { initSight, InstrumentedConnection } from '@thesight/sdk';
|
|
@@ -33,17 +45,73 @@ initSight({
|
|
|
33
45
|
// Drop-in replacement for @solana/web3.js Connection
|
|
34
46
|
const connection = new InstrumentedConnection(clusterApiUrl('mainnet'));
|
|
35
47
|
|
|
36
|
-
//
|
|
37
|
-
|
|
48
|
+
// Anywhere a Solana tx goes out, a span goes with it:
|
|
49
|
+
await connection.sendRawTransaction(tx.serialize());
|
|
50
|
+
// or...
|
|
51
|
+
await sendAndConfirmTransaction(connection, tx, signers);
|
|
52
|
+
// or...
|
|
53
|
+
const program = new Program(idl, programId, new AnchorProvider(connection, wallet, {}));
|
|
54
|
+
await program.methods.foo().rpc();
|
|
55
|
+
// or...
|
|
56
|
+
await wallet.sendTransaction(tx, connection);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Under the hood, `InstrumentedConnection` overrides the single method
|
|
60
|
+
every Solana send-path eventually calls — `sendRawTransaction` — so you
|
|
61
|
+
get spans regardless of which layer is driving the transaction.
|
|
62
|
+
|
|
63
|
+
### What the span does, step by step
|
|
64
|
+
|
|
65
|
+
1. On the synchronous submit path, a span named `solana.sendRawTransaction`
|
|
66
|
+
starts as a child of whatever OTel context is active.
|
|
67
|
+
2. `super.sendRawTransaction(rawTx, options)` is called verbatim — **no
|
|
68
|
+
byte mutation, no interception of signing, no private key access**.
|
|
69
|
+
3. The returned signature is attached to the span and the caller gets
|
|
70
|
+
it back immediately.
|
|
71
|
+
4. In the background (never blocking the caller), a task polls
|
|
72
|
+
`getTransaction(signature)` with exponential backoff until the
|
|
73
|
+
on-chain record is available, parses the program logs into a CPI
|
|
74
|
+
tree via `@thesight/core`, enriches with registered Anchor IDLs,
|
|
75
|
+
and adds `cpi.invoke` events, per-program CU attribution, and
|
|
76
|
+
decoded error details to the span.
|
|
77
|
+
5. The span ends and flushes via the Sight exporter configured by
|
|
78
|
+
`initSight`.
|
|
79
|
+
|
|
80
|
+
## Alternative — `trackSolanaTransaction` (non-wrapping)
|
|
81
|
+
|
|
82
|
+
If you don't want to wrap your `Connection` at all, `trackSolanaTransaction`
|
|
83
|
+
observes by signature alone after you've already sent the transaction
|
|
84
|
+
through *any* connection:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { Connection } from '@solana/web3.js';
|
|
88
|
+
import { initSight, trackSolanaTransaction } from '@thesight/sdk';
|
|
89
|
+
|
|
90
|
+
initSight({ apiKey, serviceName: 'checkout' });
|
|
91
|
+
|
|
92
|
+
// Use your normal Connection, unchanged.
|
|
93
|
+
const connection = new Connection(rpcUrl);
|
|
94
|
+
const signature = await wallet.sendTransaction(tx, connection);
|
|
95
|
+
|
|
96
|
+
// Fire-and-forget — span flushes on its own in the background.
|
|
97
|
+
void trackSolanaTransaction({
|
|
98
|
+
signature,
|
|
99
|
+
connection,
|
|
100
|
+
serviceName: 'checkout',
|
|
101
|
+
idls: { [swapProgramId]: swapIdl },
|
|
102
|
+
});
|
|
38
103
|
```
|
|
39
104
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
105
|
+
This helper **never touches the send path**. It only observes the
|
|
106
|
+
transaction after the fact by calling `getTransaction(signature)`.
|
|
107
|
+
Recommended when:
|
|
108
|
+
|
|
109
|
+
- You're security-conscious and don't want any SDK code on the
|
|
110
|
+
critical transaction path
|
|
111
|
+
- Your send path goes through a wallet adapter or a wrapper SDK you
|
|
112
|
+
don't control
|
|
113
|
+
- You want to instrument a specific high-value transaction rather
|
|
114
|
+
than everything a connection sends
|
|
47
115
|
|
|
48
116
|
## Anchor IDL registration
|
|
49
117
|
|
|
@@ -73,7 +141,8 @@ being fabricated.
|
|
|
73
141
|
## Self-host the ingest
|
|
74
142
|
|
|
75
143
|
By default spans export to `https://ingest.thesight.dev/ingest`.
|
|
76
|
-
Override via `ingestUrl` to point at a self-hosted Sight ingest
|
|
144
|
+
Override via `ingestUrl` to point at a self-hosted Sight ingest (or a
|
|
145
|
+
local Docker stack for development):
|
|
77
146
|
|
|
78
147
|
```ts
|
|
79
148
|
initSight({
|
|
@@ -85,24 +154,45 @@ initSight({
|
|
|
85
154
|
|
|
86
155
|
## What's on the span
|
|
87
156
|
|
|
88
|
-
Every
|
|
157
|
+
Every instrumented transaction produces an OTel span with the
|
|
89
158
|
following attributes when they're available:
|
|
90
159
|
|
|
91
160
|
- `solana.tx.signature` — base58 signature
|
|
92
|
-
- `solana.tx.status` — `confirmed` / `failed` / `timeout`
|
|
161
|
+
- `solana.tx.status` — `submitted` → `confirmed` / `failed` / `timeout`
|
|
93
162
|
- `solana.tx.slot`
|
|
94
163
|
- `solana.tx.cu_used` / `solana.tx.cu_budget` / `solana.tx.cu_utilization`
|
|
95
164
|
- `solana.tx.program` — root program name (from registered IDL or
|
|
96
|
-
curated known-programs list)
|
|
165
|
+
the curated known-programs list)
|
|
97
166
|
- `solana.tx.instruction` — extracted from `Program log: Instruction: …`
|
|
98
167
|
- `solana.tx.error` / `solana.tx.error_code` / `solana.tx.error_program`
|
|
99
168
|
— decoded from on-chain return data via the registered IDL's error
|
|
100
169
|
table or the hardcoded native-error tables
|
|
101
170
|
- `solana.tx.fee_lamports`
|
|
102
|
-
- `solana.tx.submit_ms` / `solana.tx.
|
|
103
|
-
|
|
104
|
-
- Per-CPI `cpi.invoke` events on the span: program, instruction,
|
|
105
|
-
CU consumed, CU self, percentage
|
|
171
|
+
- `solana.tx.submit_ms` / `solana.tx.enrichment_ms` — client-observed
|
|
172
|
+
latencies
|
|
173
|
+
- Per-CPI `cpi.invoke` events on the span: program, instruction,
|
|
174
|
+
depth, CU consumed, CU self, percentage
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
`InstrumentedConnection` accepts these extra options alongside the
|
|
179
|
+
standard web3.js `ConnectionConfig`:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
new InstrumentedConnection(rpcUrl, {
|
|
183
|
+
// Standard ConnectionConfig fields work as before
|
|
184
|
+
commitment: 'confirmed',
|
|
185
|
+
|
|
186
|
+
// Sight-specific
|
|
187
|
+
tracer: customTracer,
|
|
188
|
+
skipIdlResolution: false, // skip log parsing + IDL decoding
|
|
189
|
+
idls: { [programId]: idl },
|
|
190
|
+
allowOnChainIdlFetch: false, // off by default — never guess
|
|
191
|
+
enrichmentTimeoutMs: 30_000, // background enrichment wall time
|
|
192
|
+
enrichmentPollIntervalMs: 500, // base poll interval with 1.5x backoff
|
|
193
|
+
disableAutoSpan: false, // escape hatch for tests
|
|
194
|
+
});
|
|
195
|
+
```
|
|
106
196
|
|
|
107
197
|
## Shutdown
|
|
108
198
|
|
|
@@ -118,6 +208,33 @@ process.on('SIGTERM', async () => {
|
|
|
118
208
|
});
|
|
119
209
|
```
|
|
120
210
|
|
|
211
|
+
## Migration from 0.1.x
|
|
212
|
+
|
|
213
|
+
Version 0.2.0 removes the `sendAndConfirmInstrumented()` method that
|
|
214
|
+
earlier versions required you to call explicitly. Spans now fire
|
|
215
|
+
automatically from every call to `sendRawTransaction()` — including
|
|
216
|
+
those made by Anchor's provider, web3.js's top-level helpers, and
|
|
217
|
+
wallet adapters — so explicit wrapping is no longer necessary.
|
|
218
|
+
|
|
219
|
+
**If your 0.1.x code looked like this:**
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const { signature, cpiTree } = await connection.sendAndConfirmInstrumented(tx);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Replace it with either:**
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// 1. Standard web3.js — span fires automatically
|
|
229
|
+
const signature = await sendAndConfirmTransaction(connection, tx, signers);
|
|
230
|
+
|
|
231
|
+
// 2. Or inspect the span attributes via your OTel processor
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The `cpiTree` is no longer returned from a direct call. It lives on the
|
|
235
|
+
span as `cpi.invoke` events, accessible via any OTel processor hooked
|
|
236
|
+
into the same tracer provider, or visible in the Sight dashboard.
|
|
237
|
+
|
|
121
238
|
## License
|
|
122
239
|
|
|
123
240
|
MIT — see `LICENSE`.
|
package/dist/index.cjs
CHANGED
|
@@ -20,16 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
IdlResolver: () =>
|
|
23
|
+
IdlResolver: () => import_core4.IdlResolver,
|
|
24
24
|
InstrumentedConnection: () => InstrumentedConnection,
|
|
25
25
|
SightSpanExporter: () => SightSpanExporter,
|
|
26
|
-
initSight: () => initSight
|
|
26
|
+
initSight: () => initSight,
|
|
27
|
+
trackSolanaTransaction: () => trackSolanaTransaction
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(index_exports);
|
|
29
30
|
var import_web3 = require("@solana/web3.js");
|
|
30
|
-
var
|
|
31
|
-
var import_core2 = require("@thesight/core");
|
|
31
|
+
var import_api2 = require("@opentelemetry/api");
|
|
32
32
|
var import_core3 = require("@thesight/core");
|
|
33
|
+
var import_core4 = require("@thesight/core");
|
|
33
34
|
|
|
34
35
|
// src/init.ts
|
|
35
36
|
var import_sdk_trace_node = require("@opentelemetry/sdk-trace-node");
|
|
@@ -197,6 +198,98 @@ function initSight(config) {
|
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
// src/track.ts
|
|
202
|
+
var import_api = require("@opentelemetry/api");
|
|
203
|
+
var import_core2 = require("@thesight/core");
|
|
204
|
+
async function trackSolanaTransaction(opts) {
|
|
205
|
+
const tracerName = opts.serviceName ?? "@thesight/sdk";
|
|
206
|
+
const tracer = opts.tracer ?? import_api.trace.getTracer(tracerName);
|
|
207
|
+
const span = tracer.startSpan("solana.trackTransaction", {}, import_api.context.active());
|
|
208
|
+
span.setAttribute("solana.tx.signature", opts.signature);
|
|
209
|
+
const start = Date.now();
|
|
210
|
+
const deadline = start + (opts.timeoutMs ?? 3e4);
|
|
211
|
+
const commitment = opts.commitment ?? "confirmed";
|
|
212
|
+
const basePoll = opts.pollIntervalMs ?? 500;
|
|
213
|
+
const resolver = opts.idlResolver ?? buildResolverFromIdls(opts.idls);
|
|
214
|
+
try {
|
|
215
|
+
const txDetails = await pollGetTransaction(opts.connection, opts.signature, commitment, deadline, basePoll);
|
|
216
|
+
if (!txDetails) {
|
|
217
|
+
span.setAttribute("solana.tx.status", "timeout");
|
|
218
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
|
|
222
|
+
span.setAttribute("solana.tx.slot", txDetails.slot);
|
|
223
|
+
const fee = txDetails.meta?.fee;
|
|
224
|
+
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
225
|
+
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
226
|
+
if (cuUsed !== void 0 && cuUsed !== null) {
|
|
227
|
+
span.setAttribute("solana.tx.cu_used", Number(cuUsed));
|
|
228
|
+
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
229
|
+
span.setAttribute(
|
|
230
|
+
"solana.tx.cu_utilization",
|
|
231
|
+
parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const logs = txDetails.meta?.logMessages ?? [];
|
|
235
|
+
if (logs.length > 0) {
|
|
236
|
+
const { cpiTree } = (0, import_core2.parseLogs)({ logs });
|
|
237
|
+
await (0, import_core2.enrichTree)(cpiTree, resolver);
|
|
238
|
+
for (const attr of (0, import_core2.flatAttributions)(cpiTree)) {
|
|
239
|
+
span.addEvent("cpi.invoke", {
|
|
240
|
+
"cpi.program": attr.programName ?? attr.programId,
|
|
241
|
+
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
242
|
+
"cpi.depth": attr.depth,
|
|
243
|
+
"cpi.cu_consumed": attr.cuConsumed,
|
|
244
|
+
"cpi.cu_self": attr.cuSelf,
|
|
245
|
+
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const root = cpiTree.roots[0];
|
|
249
|
+
if (root) {
|
|
250
|
+
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
251
|
+
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
|
|
255
|
+
if (txDetails.meta?.err) {
|
|
256
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR });
|
|
257
|
+
} else {
|
|
258
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
262
|
+
span.setStatus({
|
|
263
|
+
code: import_api.SpanStatusCode.ERROR,
|
|
264
|
+
message: err instanceof Error ? err.message : String(err)
|
|
265
|
+
});
|
|
266
|
+
} finally {
|
|
267
|
+
span.end();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function buildResolverFromIdls(idls) {
|
|
271
|
+
const resolver = new import_core2.IdlResolver();
|
|
272
|
+
if (idls) resolver.registerMany(idls);
|
|
273
|
+
return resolver;
|
|
274
|
+
}
|
|
275
|
+
async function pollGetTransaction(connection, signature, commitment, deadline, basePollMs) {
|
|
276
|
+
let attempt = 0;
|
|
277
|
+
while (Date.now() < deadline) {
|
|
278
|
+
try {
|
|
279
|
+
const tx = await connection.getTransaction(signature, {
|
|
280
|
+
commitment,
|
|
281
|
+
maxSupportedTransactionVersion: 0
|
|
282
|
+
});
|
|
283
|
+
if (tx) return tx;
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
attempt++;
|
|
287
|
+
const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
200
293
|
// src/index.ts
|
|
201
294
|
var InstrumentedConnection = class extends import_web3.Connection {
|
|
202
295
|
sightConfig;
|
|
@@ -209,16 +302,29 @@ var InstrumentedConnection = class extends import_web3.Connection {
|
|
|
209
302
|
skipIdlResolution,
|
|
210
303
|
idls,
|
|
211
304
|
allowOnChainIdlFetch,
|
|
305
|
+
enrichmentTimeoutMs,
|
|
306
|
+
enrichmentPollIntervalMs,
|
|
307
|
+
disableAutoSpan,
|
|
308
|
+
commitment,
|
|
212
309
|
...connectionConfig
|
|
213
310
|
} = config ?? {};
|
|
214
311
|
super(endpoint, connectionConfig);
|
|
215
|
-
this.sightConfig = {
|
|
216
|
-
|
|
312
|
+
this.sightConfig = {
|
|
313
|
+
tracer,
|
|
314
|
+
idlRpcEndpoint,
|
|
315
|
+
skipIdlResolution,
|
|
316
|
+
allowOnChainIdlFetch,
|
|
317
|
+
enrichmentTimeoutMs: enrichmentTimeoutMs ?? 3e4,
|
|
318
|
+
enrichmentPollIntervalMs: enrichmentPollIntervalMs ?? 500,
|
|
319
|
+
disableAutoSpan,
|
|
320
|
+
commitment
|
|
321
|
+
};
|
|
322
|
+
this.idlResolver = new import_core3.IdlResolver({
|
|
217
323
|
rpcEndpoint: idlRpcEndpoint ?? endpoint,
|
|
218
324
|
allowOnChainFetch: allowOnChainIdlFetch ?? false
|
|
219
325
|
});
|
|
220
326
|
if (idls) this.idlResolver.registerMany(idls);
|
|
221
|
-
this.tracer = tracer ??
|
|
327
|
+
this.tracer = tracer ?? import_api2.trace.getTracer("@thesight/sdk");
|
|
222
328
|
}
|
|
223
329
|
/**
|
|
224
330
|
* Register an IDL for a single program. Convenience forwarder for the
|
|
@@ -235,132 +341,165 @@ var InstrumentedConnection = class extends import_web3.Connection {
|
|
|
235
341
|
registerIdls(idls) {
|
|
236
342
|
this.idlResolver.registerMany(idls);
|
|
237
343
|
}
|
|
344
|
+
// ─── Overridden method ────────────────────────────────────────────────────
|
|
238
345
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
346
|
+
* Overrides the parent `Connection.sendRawTransaction` so every submit —
|
|
347
|
+
* including those made by Anchor's `provider.sendAndConfirm`, web3.js's
|
|
348
|
+
* top-level `sendAndConfirmTransaction`, and wallet adapter flows — is
|
|
349
|
+
* observed by an OTel span automatically.
|
|
350
|
+
*
|
|
351
|
+
* The span is a child of whatever OTel context is active when the call
|
|
352
|
+
* fires, so app-level parent spans (HTTP handlers, job runs, wallet flow
|
|
353
|
+
* spans from another SDK) nest the Sight span correctly.
|
|
241
354
|
*/
|
|
242
|
-
async
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
355
|
+
async sendRawTransaction(rawTransaction, options) {
|
|
356
|
+
if (this.sightConfig.disableAutoSpan) {
|
|
357
|
+
return super.sendRawTransaction(rawTransaction, options);
|
|
358
|
+
}
|
|
359
|
+
const span = this.tracer.startSpan("solana.sendRawTransaction", {}, import_api2.context.active());
|
|
246
360
|
const submitStart = Date.now();
|
|
361
|
+
let signature;
|
|
247
362
|
try {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{ skipPreflight: false, ...options }
|
|
251
|
-
);
|
|
252
|
-
span.setAttribute("solana.tx.signature", signature);
|
|
363
|
+
signature = await super.sendRawTransaction(rawTransaction, options);
|
|
364
|
+
} catch (err) {
|
|
253
365
|
const submitMs = Date.now() - submitStart;
|
|
254
366
|
span.setAttribute("solana.tx.submit_ms", submitMs);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
span.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (decoded.errorMsg) {
|
|
291
|
-
span.setAttribute("solana.tx.error_msg", decoded.errorMsg);
|
|
292
|
-
}
|
|
293
|
-
span.end();
|
|
294
|
-
return { signature, error: decoded };
|
|
295
|
-
}
|
|
367
|
+
span.setAttribute("solana.tx.status", "failed");
|
|
368
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
369
|
+
span.setStatus({
|
|
370
|
+
code: import_api2.SpanStatusCode.ERROR,
|
|
371
|
+
message: err instanceof Error ? err.message : String(err)
|
|
372
|
+
});
|
|
373
|
+
span.end();
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
span.setAttribute("solana.tx.signature", signature);
|
|
377
|
+
span.setAttribute("solana.tx.submit_ms", Date.now() - submitStart);
|
|
378
|
+
span.setAttribute("solana.tx.status", "submitted");
|
|
379
|
+
void this.enrichSpanInBackground(span, signature, submitStart);
|
|
380
|
+
return signature;
|
|
381
|
+
}
|
|
382
|
+
// ─── Background enrichment ───────────────────────────────────────────────
|
|
383
|
+
/**
|
|
384
|
+
* Poll `getTransaction` until the on-chain record is available, then
|
|
385
|
+
* enrich the span with CU attribution, program names, decoded errors,
|
|
386
|
+
* and per-CPI events.
|
|
387
|
+
*
|
|
388
|
+
* This runs async and is never awaited by callers of `sendRawTransaction`.
|
|
389
|
+
* Failures inside this method attach to the span via recordException
|
|
390
|
+
* rather than throwing.
|
|
391
|
+
*/
|
|
392
|
+
async enrichSpanInBackground(span, signature, submitStart) {
|
|
393
|
+
const enrichStart = Date.now();
|
|
394
|
+
const deadline = enrichStart + (this.sightConfig.enrichmentTimeoutMs ?? 3e4);
|
|
395
|
+
const commitment = this.sightConfig.commitment ?? "confirmed";
|
|
396
|
+
const basePollMs = this.sightConfig.enrichmentPollIntervalMs ?? 500;
|
|
397
|
+
try {
|
|
398
|
+
const txDetails = await this.pollForTransaction(signature, commitment, deadline, basePollMs);
|
|
399
|
+
if (!txDetails) {
|
|
400
|
+
span.setAttribute("solana.tx.status", "timeout");
|
|
401
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
|
|
296
402
|
span.end();
|
|
297
|
-
return
|
|
403
|
+
return;
|
|
298
404
|
}
|
|
299
|
-
|
|
405
|
+
this.attachTxDetailsToSpan(span, txDetails);
|
|
300
406
|
if (!this.sightConfig.skipIdlResolution) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
maxSupportedTransactionVersion: 0
|
|
305
|
-
});
|
|
306
|
-
if (txDetails) {
|
|
307
|
-
const slot = txDetails.slot;
|
|
308
|
-
const fee = txDetails.meta?.fee;
|
|
309
|
-
const logs = txDetails.meta?.logMessages ?? [];
|
|
310
|
-
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
311
|
-
span.setAttribute("solana.tx.slot", slot);
|
|
312
|
-
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
313
|
-
if (cuUsed !== void 0) {
|
|
314
|
-
span.setAttribute("solana.tx.cu_used", cuUsed);
|
|
315
|
-
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
316
|
-
span.setAttribute("solana.tx.cu_utilization", parseFloat((cuUsed / 2e5 * 100).toFixed(1)));
|
|
317
|
-
}
|
|
318
|
-
const { cpiTree } = (0, import_core2.parseLogs)({ logs });
|
|
319
|
-
if (!this.sightConfig.skipIdlResolution) {
|
|
320
|
-
await (0, import_core2.enrichTree)(cpiTree, this.idlResolver);
|
|
321
|
-
}
|
|
322
|
-
const attributions = (0, import_core2.flatAttributions)(cpiTree);
|
|
323
|
-
for (const attr of attributions) {
|
|
324
|
-
span.addEvent("cpi.invoke", {
|
|
325
|
-
"cpi.program": attr.programName ?? attr.programId,
|
|
326
|
-
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
327
|
-
"cpi.depth": attr.depth,
|
|
328
|
-
"cpi.cu_consumed": attr.cuConsumed,
|
|
329
|
-
"cpi.cu_self": attr.cuSelf,
|
|
330
|
-
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
const root = cpiTree.roots[0];
|
|
334
|
-
if (root) {
|
|
335
|
-
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
336
|
-
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
337
|
-
}
|
|
338
|
-
span.setAttribute("solana.tx.enrichment_ms", Date.now() - enrichStart);
|
|
339
|
-
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
340
|
-
span.end();
|
|
341
|
-
return { signature, cpiTree };
|
|
407
|
+
const logs = txDetails.meta?.logMessages ?? [];
|
|
408
|
+
if (logs.length > 0) {
|
|
409
|
+
await this.attachParsedLogsToSpan(span, logs);
|
|
342
410
|
}
|
|
343
411
|
}
|
|
344
|
-
span.
|
|
345
|
-
|
|
346
|
-
|
|
412
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
|
|
413
|
+
if (txDetails.meta?.err) {
|
|
414
|
+
span.setStatus({ code: import_api2.SpanStatusCode.ERROR });
|
|
415
|
+
} else {
|
|
416
|
+
span.setStatus({ code: import_api2.SpanStatusCode.OK });
|
|
417
|
+
}
|
|
347
418
|
} catch (err) {
|
|
348
|
-
span.
|
|
349
|
-
span.setAttribute(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
419
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
420
|
+
span.setAttribute(
|
|
421
|
+
"solana.tx.enrichment_error",
|
|
422
|
+
err instanceof Error ? err.message : String(err)
|
|
423
|
+
);
|
|
424
|
+
} finally {
|
|
354
425
|
span.end();
|
|
355
|
-
throw err;
|
|
356
426
|
}
|
|
357
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Poll `getTransaction(signature)` until either the on-chain record is
|
|
430
|
+
* returned or the deadline passes. Exponential backoff (1.5x) capped at
|
|
431
|
+
* 2 seconds to balance responsiveness against RPC load.
|
|
432
|
+
*/
|
|
433
|
+
async pollForTransaction(signature, commitment, deadline, basePollMs) {
|
|
434
|
+
let attempt = 0;
|
|
435
|
+
while (Date.now() < deadline) {
|
|
436
|
+
try {
|
|
437
|
+
const tx = await super.getTransaction(signature, {
|
|
438
|
+
commitment,
|
|
439
|
+
maxSupportedTransactionVersion: 0
|
|
440
|
+
});
|
|
441
|
+
if (tx) return tx;
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
attempt++;
|
|
445
|
+
const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
|
|
446
|
+
await sleep(waitMs);
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
/** Attach the flat fields (slot, fee, CU, status) from a tx response to a span. */
|
|
451
|
+
attachTxDetailsToSpan(span, txDetails) {
|
|
452
|
+
span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
|
|
453
|
+
span.setAttribute("solana.tx.slot", txDetails.slot);
|
|
454
|
+
const fee = txDetails.meta?.fee;
|
|
455
|
+
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
456
|
+
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
457
|
+
if (cuUsed !== void 0 && cuUsed !== null) {
|
|
458
|
+
span.setAttribute("solana.tx.cu_used", Number(cuUsed));
|
|
459
|
+
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
460
|
+
span.setAttribute(
|
|
461
|
+
"solana.tx.cu_utilization",
|
|
462
|
+
parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Parse program logs into a CPI tree, enrich with registered IDLs, and
|
|
468
|
+
* emit one `cpi.invoke` event per invocation onto the span. Root
|
|
469
|
+
* program/instruction names are copied onto span attributes so dashboards
|
|
470
|
+
* can filter by them without walking events.
|
|
471
|
+
*/
|
|
472
|
+
async attachParsedLogsToSpan(span, logs) {
|
|
473
|
+
const { cpiTree } = (0, import_core3.parseLogs)({ logs });
|
|
474
|
+
await (0, import_core3.enrichTree)(cpiTree, this.idlResolver);
|
|
475
|
+
const attributions = (0, import_core3.flatAttributions)(cpiTree);
|
|
476
|
+
for (const attr of attributions) {
|
|
477
|
+
span.addEvent("cpi.invoke", {
|
|
478
|
+
"cpi.program": attr.programName ?? attr.programId,
|
|
479
|
+
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
480
|
+
"cpi.depth": attr.depth,
|
|
481
|
+
"cpi.cu_consumed": attr.cuConsumed,
|
|
482
|
+
"cpi.cu_self": attr.cuSelf,
|
|
483
|
+
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
const root = cpiTree.roots[0];
|
|
487
|
+
if (root) {
|
|
488
|
+
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
489
|
+
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
490
|
+
}
|
|
491
|
+
return cpiTree;
|
|
492
|
+
}
|
|
358
493
|
};
|
|
494
|
+
function sleep(ms) {
|
|
495
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
496
|
+
}
|
|
359
497
|
// Annotate the CommonJS export names for ESM import in node:
|
|
360
498
|
0 && (module.exports = {
|
|
361
499
|
IdlResolver,
|
|
362
500
|
InstrumentedConnection,
|
|
363
501
|
SightSpanExporter,
|
|
364
|
-
initSight
|
|
502
|
+
initSight,
|
|
503
|
+
trackSolanaTransaction
|
|
365
504
|
});
|
|
366
505
|
//# sourceMappingURL=index.cjs.map
|