@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/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TransactionSignature, Connection, Finality, ConnectionConfig, Commitment, SendOptions } from '@solana/web3.js';
|
|
2
2
|
import { Tracer } from '@opentelemetry/api';
|
|
3
|
-
import {
|
|
3
|
+
import { IdlResolver, AnchorIdl } from '@thesight/core';
|
|
4
4
|
export { AnchorIdl, CpiTree, CuAttribution, DecodedError, FlamegraphItem, IdlResolver } from '@thesight/core';
|
|
5
5
|
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
6
6
|
import { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
@@ -108,14 +108,92 @@ declare class SightSpanExporter implements SpanExporter {
|
|
|
108
108
|
private convertSpan;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
interface TrackTransactionOptions {
|
|
112
|
+
/** The signature returned from a prior `sendRawTransaction` / `sendTransaction` call. */
|
|
113
|
+
signature: TransactionSignature;
|
|
114
|
+
/**
|
|
115
|
+
* Any `@solana/web3.js` Connection — plain or instrumented. Used to fetch
|
|
116
|
+
* the on-chain transaction via `getTransaction` for enrichment. Does
|
|
117
|
+
* **not** need to be an `InstrumentedConnection`; this helper intentionally
|
|
118
|
+
* never touches the transaction-send path.
|
|
119
|
+
*/
|
|
120
|
+
connection: Connection;
|
|
121
|
+
/**
|
|
122
|
+
* Service name used for the span. Falls back to the service name
|
|
123
|
+
* configured by `initSight`. Useful when you want a different service
|
|
124
|
+
* name for a specific subsystem's spans.
|
|
125
|
+
*/
|
|
126
|
+
serviceName?: string;
|
|
127
|
+
/** OTel tracer override. Defaults to `trace.getTracer('@thesight/sdk')`. */
|
|
128
|
+
tracer?: Tracer;
|
|
129
|
+
/** Optional IDL resolver for program/error decoding. A fresh one is built if omitted. */
|
|
130
|
+
idlResolver?: IdlResolver;
|
|
131
|
+
/**
|
|
132
|
+
* Pre-registered IDLs to hand to a freshly-built resolver. Ignored when
|
|
133
|
+
* `idlResolver` is provided.
|
|
134
|
+
*/
|
|
135
|
+
idls?: Record<string, AnchorIdl>;
|
|
136
|
+
/** Finality commitment used for the enrichment fetch. Default 'confirmed'. */
|
|
137
|
+
commitment?: Finality;
|
|
138
|
+
/** Max wall-time before the span is ended with status=timeout. Default 30000ms. */
|
|
139
|
+
timeoutMs?: number;
|
|
140
|
+
/** Base poll interval for retrying getTransaction. Default 500ms. */
|
|
141
|
+
pollIntervalMs?: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* **Non-wrapping observation helper.** Given a transaction signature that
|
|
145
|
+
* was already sent through any `Connection`, fetch the on-chain record,
|
|
146
|
+
* parse the logs, and emit a single OpenTelemetry span describing the
|
|
147
|
+
* transaction.
|
|
148
|
+
*
|
|
149
|
+
* Unlike `InstrumentedConnection`, this helper **never touches the send
|
|
150
|
+
* path** — it only observes after the fact via `getTransaction(signature)`.
|
|
151
|
+
* Use this when:
|
|
152
|
+
*
|
|
153
|
+
* - You want observability for wallet-adapter flows where the send path
|
|
154
|
+
* goes through the wallet and you can't easily override the Connection
|
|
155
|
+
* - You're security-conscious and don't want any SDK code on the critical
|
|
156
|
+
* transaction path
|
|
157
|
+
* - You want to instrument a handful of high-value transactions rather
|
|
158
|
+
* than every call a Connection makes
|
|
159
|
+
*
|
|
160
|
+
* Usage:
|
|
161
|
+
*
|
|
162
|
+
* import { trackSolanaTransaction } from '@thesight/sdk';
|
|
163
|
+
* import { Connection } from '@solana/web3.js';
|
|
164
|
+
*
|
|
165
|
+
* const connection = new Connection(rpcUrl);
|
|
166
|
+
* const signature = await wallet.sendTransaction(tx, connection);
|
|
167
|
+
*
|
|
168
|
+
* // Fire-and-forget. Span flushes on its own.
|
|
169
|
+
* void trackSolanaTransaction({
|
|
170
|
+
* signature,
|
|
171
|
+
* connection,
|
|
172
|
+
* serviceName: 'checkout',
|
|
173
|
+
* idls: { [programId]: idl },
|
|
174
|
+
* });
|
|
175
|
+
*
|
|
176
|
+
* Returns a `Promise<void>` — typically not awaited, but callers who want
|
|
177
|
+
* to wait for the span to export (e.g. short-lived scripts) can await it.
|
|
178
|
+
*/
|
|
179
|
+
declare function trackSolanaTransaction(opts: TrackTransactionOptions): Promise<void>;
|
|
180
|
+
|
|
111
181
|
interface SightConfig {
|
|
112
|
-
/** OTel tracer.
|
|
182
|
+
/** OTel tracer. Defaults to `trace.getTracer('@thesight/sdk')`. */
|
|
113
183
|
tracer?: Tracer;
|
|
114
184
|
/** Override RPC endpoint for IDL fetching (defaults to same as connection) */
|
|
115
185
|
idlRpcEndpoint?: string;
|
|
116
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Commitment used when fetching confirmed tx details for enrichment.
|
|
188
|
+
* Defaults to 'confirmed'.
|
|
189
|
+
*/
|
|
117
190
|
commitment?: Commitment;
|
|
118
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Skip IDL resolution entirely. Spans will still emit with signature and
|
|
193
|
+
* timing attributes, but program names, instruction names, CPI trees, and
|
|
194
|
+
* decoded errors will all be omitted. Useful when you want minimal
|
|
195
|
+
* overhead and don't care about the richer observability.
|
|
196
|
+
*/
|
|
119
197
|
skipIdlResolution?: boolean;
|
|
120
198
|
/**
|
|
121
199
|
* Pre-register IDLs at construction time. Keys are program IDs, values are
|
|
@@ -126,18 +204,35 @@ interface SightConfig {
|
|
|
126
204
|
/**
|
|
127
205
|
* Opt into reading Anchor IDL accounts from the on-chain PDA as a fallback
|
|
128
206
|
* when a program is not explicitly registered. Defaults to **false** —
|
|
129
|
-
* Sight's model is that IDLs are explicitly provided by the application
|
|
130
|
-
*
|
|
207
|
+
* Sight's model is that IDLs are explicitly provided by the application,
|
|
208
|
+
* not silently guessed from chain.
|
|
131
209
|
*/
|
|
132
210
|
allowOnChainIdlFetch?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Max wall-time the background enrichment task will wait for a
|
|
213
|
+
* transaction to show up in `getTransaction`. After this expires the
|
|
214
|
+
* span is ended with `solana.tx.status = 'timeout'`. Default 30000 (30s).
|
|
215
|
+
*/
|
|
216
|
+
enrichmentTimeoutMs?: number;
|
|
217
|
+
/**
|
|
218
|
+
* How often the enrichment task polls `getTransaction`. Each retry backs
|
|
219
|
+
* off by 1.5x up to a cap. Default 500ms base.
|
|
220
|
+
*/
|
|
221
|
+
enrichmentPollIntervalMs?: number;
|
|
222
|
+
/**
|
|
223
|
+
* Disable automatic span creation in the overridden `sendRawTransaction`.
|
|
224
|
+
* When true, InstrumentedConnection behaves like a plain Connection. You
|
|
225
|
+
* probably don't want this — it's here as an escape hatch for tests and
|
|
226
|
+
* rare cases where you need the class hierarchy but not the tracing.
|
|
227
|
+
*/
|
|
228
|
+
disableAutoSpan?: boolean;
|
|
133
229
|
}
|
|
134
230
|
interface SightSpanAttributes {
|
|
135
231
|
'solana.tx.signature': string;
|
|
136
|
-
'solana.tx.status': 'confirmed' | 'failed' | 'timeout';
|
|
232
|
+
'solana.tx.status': 'submitted' | 'confirmed' | 'failed' | 'timeout';
|
|
137
233
|
'solana.tx.slot'?: number;
|
|
138
234
|
'solana.tx.fee_lamports'?: number;
|
|
139
235
|
'solana.tx.submit_ms': number;
|
|
140
|
-
'solana.tx.confirmation_ms'?: number;
|
|
141
236
|
'solana.tx.enrichment_ms'?: number;
|
|
142
237
|
'solana.tx.cu_used'?: number;
|
|
143
238
|
'solana.tx.cu_budget'?: number;
|
|
@@ -150,17 +245,41 @@ interface SightSpanAttributes {
|
|
|
150
245
|
'solana.tx.error_msg'?: string;
|
|
151
246
|
}
|
|
152
247
|
/**
|
|
153
|
-
* Drop-in replacement for
|
|
154
|
-
* every
|
|
248
|
+
* Drop-in replacement for `@solana/web3.js`'s `Connection` that emits an
|
|
249
|
+
* OpenTelemetry span for **every** call to `sendRawTransaction` —
|
|
250
|
+
* regardless of whether the caller is your own code, an Anchor provider's
|
|
251
|
+
* `sendAndConfirm`, `@solana/web3.js`'s top-level `sendAndConfirmTransaction`,
|
|
252
|
+
* a wallet adapter, or anything else.
|
|
253
|
+
*
|
|
254
|
+
* Internally it overrides the parent class's `sendRawTransaction`, so the
|
|
255
|
+
* span covers the same surface web3.js already mediates. No mutation of
|
|
256
|
+
* the transaction bytes — we call `super.sendRawTransaction(rawTx, opts)`
|
|
257
|
+
* verbatim and observe the signature it returns.
|
|
258
|
+
*
|
|
259
|
+
* After the signature is returned (synchronously from the caller's point
|
|
260
|
+
* of view), a background task polls `getTransaction` until the on-chain
|
|
261
|
+
* record is available, parses the program logs via `@thesight/core`, and
|
|
262
|
+
* enriches the span with CU attribution, per-CPI events, program + instruction
|
|
263
|
+
* names, and decoded error details. The background task never blocks the
|
|
264
|
+
* caller — if it fails or times out, the span ends with status=timeout and
|
|
265
|
+
* whatever partial data was collected.
|
|
155
266
|
*
|
|
156
267
|
* Usage:
|
|
157
|
-
* const connection = new InstrumentedConnection(rpcUrl, {
|
|
158
|
-
* tracer: trace.getTracer('my-service'),
|
|
159
|
-
* });
|
|
160
268
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
269
|
+
* import { initSight, InstrumentedConnection } from '@thesight/sdk';
|
|
270
|
+
*
|
|
271
|
+
* initSight({ apiKey, serviceName: 'swap-bot' });
|
|
272
|
+
*
|
|
273
|
+
* // Anywhere you currently construct a Connection, construct this instead:
|
|
274
|
+
* const connection = new InstrumentedConnection(rpcUrl, {
|
|
275
|
+
* commitment: 'confirmed',
|
|
276
|
+
* });
|
|
277
|
+
*
|
|
278
|
+
* // All of the following now emit spans automatically:
|
|
279
|
+
* await connection.sendRawTransaction(tx.serialize());
|
|
280
|
+
* await sendAndConfirmTransaction(connection, tx, signers); // web3.js
|
|
281
|
+
* await program.methods.foo().rpc(); // Anchor
|
|
282
|
+
* await wallet.sendTransaction(tx, connection); // wallet adapter
|
|
164
283
|
*/
|
|
165
284
|
declare class InstrumentedConnection extends Connection {
|
|
166
285
|
private sightConfig;
|
|
@@ -179,16 +298,41 @@ declare class InstrumentedConnection extends Connection {
|
|
|
179
298
|
/** Bulk-register multiple IDLs. */
|
|
180
299
|
registerIdls(idls: Record<string, AnchorIdl>): void;
|
|
181
300
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
301
|
+
* Overrides the parent `Connection.sendRawTransaction` so every submit —
|
|
302
|
+
* including those made by Anchor's `provider.sendAndConfirm`, web3.js's
|
|
303
|
+
* top-level `sendAndConfirmTransaction`, and wallet adapter flows — is
|
|
304
|
+
* observed by an OTel span automatically.
|
|
305
|
+
*
|
|
306
|
+
* The span is a child of whatever OTel context is active when the call
|
|
307
|
+
* fires, so app-level parent spans (HTTP handlers, job runs, wallet flow
|
|
308
|
+
* spans from another SDK) nest the Sight span correctly.
|
|
309
|
+
*/
|
|
310
|
+
sendRawTransaction(rawTransaction: Buffer | Uint8Array | Array<number>, options?: SendOptions): Promise<TransactionSignature>;
|
|
311
|
+
/**
|
|
312
|
+
* Poll `getTransaction` until the on-chain record is available, then
|
|
313
|
+
* enrich the span with CU attribution, program names, decoded errors,
|
|
314
|
+
* and per-CPI events.
|
|
315
|
+
*
|
|
316
|
+
* This runs async and is never awaited by callers of `sendRawTransaction`.
|
|
317
|
+
* Failures inside this method attach to the span via recordException
|
|
318
|
+
* rather than throwing.
|
|
319
|
+
*/
|
|
320
|
+
private enrichSpanInBackground;
|
|
321
|
+
/**
|
|
322
|
+
* Poll `getTransaction(signature)` until either the on-chain record is
|
|
323
|
+
* returned or the deadline passes. Exponential backoff (1.5x) capped at
|
|
324
|
+
* 2 seconds to balance responsiveness against RPC load.
|
|
325
|
+
*/
|
|
326
|
+
private pollForTransaction;
|
|
327
|
+
/** Attach the flat fields (slot, fee, CU, status) from a tx response to a span. */
|
|
328
|
+
private attachTxDetailsToSpan;
|
|
329
|
+
/**
|
|
330
|
+
* Parse program logs into a CPI tree, enrich with registered IDLs, and
|
|
331
|
+
* emit one `cpi.invoke` event per invocation onto the span. Root
|
|
332
|
+
* program/instruction names are copied onto span attributes so dashboards
|
|
333
|
+
* can filter by them without walking events.
|
|
184
334
|
*/
|
|
185
|
-
|
|
186
|
-
commitment?: Commitment;
|
|
187
|
-
}): Promise<{
|
|
188
|
-
signature: string;
|
|
189
|
-
cpiTree?: CpiTree;
|
|
190
|
-
error?: DecodedError;
|
|
191
|
-
}>;
|
|
335
|
+
private attachParsedLogsToSpan;
|
|
192
336
|
}
|
|
193
337
|
|
|
194
|
-
export { type InitSightConfig, InstrumentedConnection, type SightConfig, type SightExporterConfig, type SightSpanAttributes, SightSpanExporter, type SightTracerHandle, initSight };
|
|
338
|
+
export { type InitSightConfig, InstrumentedConnection, type SightConfig, type SightExporterConfig, type SightSpanAttributes, SightSpanExporter, type SightTracerHandle, type TrackTransactionOptions, initSight, trackSolanaTransaction };
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
Connection
|
|
4
4
|
} from "@solana/web3.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {
|
|
6
|
+
trace as trace2,
|
|
7
|
+
context as context2,
|
|
8
|
+
SpanStatusCode as SpanStatusCode2
|
|
9
|
+
} from "@opentelemetry/api";
|
|
10
|
+
import { parseLogs as parseLogs2, IdlResolver as IdlResolver2, enrichTree as enrichTree2, flatAttributions as flatAttributions2 } from "@thesight/core";
|
|
11
|
+
import { IdlResolver as IdlResolver3 } from "@thesight/core";
|
|
8
12
|
|
|
9
13
|
// src/init.ts
|
|
10
14
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
@@ -175,6 +179,98 @@ function initSight(config) {
|
|
|
175
179
|
};
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// src/track.ts
|
|
183
|
+
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
|
|
184
|
+
import { parseLogs, IdlResolver, enrichTree, flatAttributions } from "@thesight/core";
|
|
185
|
+
async function trackSolanaTransaction(opts) {
|
|
186
|
+
const tracerName = opts.serviceName ?? "@thesight/sdk";
|
|
187
|
+
const tracer = opts.tracer ?? trace.getTracer(tracerName);
|
|
188
|
+
const span = tracer.startSpan("solana.trackTransaction", {}, context.active());
|
|
189
|
+
span.setAttribute("solana.tx.signature", opts.signature);
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
const deadline = start + (opts.timeoutMs ?? 3e4);
|
|
192
|
+
const commitment = opts.commitment ?? "confirmed";
|
|
193
|
+
const basePoll = opts.pollIntervalMs ?? 500;
|
|
194
|
+
const resolver = opts.idlResolver ?? buildResolverFromIdls(opts.idls);
|
|
195
|
+
try {
|
|
196
|
+
const txDetails = await pollGetTransaction(opts.connection, opts.signature, commitment, deadline, basePoll);
|
|
197
|
+
if (!txDetails) {
|
|
198
|
+
span.setAttribute("solana.tx.status", "timeout");
|
|
199
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
|
|
203
|
+
span.setAttribute("solana.tx.slot", txDetails.slot);
|
|
204
|
+
const fee = txDetails.meta?.fee;
|
|
205
|
+
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
206
|
+
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
207
|
+
if (cuUsed !== void 0 && cuUsed !== null) {
|
|
208
|
+
span.setAttribute("solana.tx.cu_used", Number(cuUsed));
|
|
209
|
+
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
210
|
+
span.setAttribute(
|
|
211
|
+
"solana.tx.cu_utilization",
|
|
212
|
+
parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const logs = txDetails.meta?.logMessages ?? [];
|
|
216
|
+
if (logs.length > 0) {
|
|
217
|
+
const { cpiTree } = parseLogs({ logs });
|
|
218
|
+
await enrichTree(cpiTree, resolver);
|
|
219
|
+
for (const attr of flatAttributions(cpiTree)) {
|
|
220
|
+
span.addEvent("cpi.invoke", {
|
|
221
|
+
"cpi.program": attr.programName ?? attr.programId,
|
|
222
|
+
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
223
|
+
"cpi.depth": attr.depth,
|
|
224
|
+
"cpi.cu_consumed": attr.cuConsumed,
|
|
225
|
+
"cpi.cu_self": attr.cuSelf,
|
|
226
|
+
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const root = cpiTree.roots[0];
|
|
230
|
+
if (root) {
|
|
231
|
+
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
232
|
+
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
|
|
236
|
+
if (txDetails.meta?.err) {
|
|
237
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
238
|
+
} else {
|
|
239
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
243
|
+
span.setStatus({
|
|
244
|
+
code: SpanStatusCode.ERROR,
|
|
245
|
+
message: err instanceof Error ? err.message : String(err)
|
|
246
|
+
});
|
|
247
|
+
} finally {
|
|
248
|
+
span.end();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function buildResolverFromIdls(idls) {
|
|
252
|
+
const resolver = new IdlResolver();
|
|
253
|
+
if (idls) resolver.registerMany(idls);
|
|
254
|
+
return resolver;
|
|
255
|
+
}
|
|
256
|
+
async function pollGetTransaction(connection, signature, commitment, deadline, basePollMs) {
|
|
257
|
+
let attempt = 0;
|
|
258
|
+
while (Date.now() < deadline) {
|
|
259
|
+
try {
|
|
260
|
+
const tx = await connection.getTransaction(signature, {
|
|
261
|
+
commitment,
|
|
262
|
+
maxSupportedTransactionVersion: 0
|
|
263
|
+
});
|
|
264
|
+
if (tx) return tx;
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
attempt++;
|
|
268
|
+
const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
178
274
|
// src/index.ts
|
|
179
275
|
var InstrumentedConnection = class extends Connection {
|
|
180
276
|
sightConfig;
|
|
@@ -187,16 +283,29 @@ var InstrumentedConnection = class extends Connection {
|
|
|
187
283
|
skipIdlResolution,
|
|
188
284
|
idls,
|
|
189
285
|
allowOnChainIdlFetch,
|
|
286
|
+
enrichmentTimeoutMs,
|
|
287
|
+
enrichmentPollIntervalMs,
|
|
288
|
+
disableAutoSpan,
|
|
289
|
+
commitment,
|
|
190
290
|
...connectionConfig
|
|
191
291
|
} = config ?? {};
|
|
192
292
|
super(endpoint, connectionConfig);
|
|
193
|
-
this.sightConfig = {
|
|
194
|
-
|
|
293
|
+
this.sightConfig = {
|
|
294
|
+
tracer,
|
|
295
|
+
idlRpcEndpoint,
|
|
296
|
+
skipIdlResolution,
|
|
297
|
+
allowOnChainIdlFetch,
|
|
298
|
+
enrichmentTimeoutMs: enrichmentTimeoutMs ?? 3e4,
|
|
299
|
+
enrichmentPollIntervalMs: enrichmentPollIntervalMs ?? 500,
|
|
300
|
+
disableAutoSpan,
|
|
301
|
+
commitment
|
|
302
|
+
};
|
|
303
|
+
this.idlResolver = new IdlResolver2({
|
|
195
304
|
rpcEndpoint: idlRpcEndpoint ?? endpoint,
|
|
196
305
|
allowOnChainFetch: allowOnChainIdlFetch ?? false
|
|
197
306
|
});
|
|
198
307
|
if (idls) this.idlResolver.registerMany(idls);
|
|
199
|
-
this.tracer = tracer ??
|
|
308
|
+
this.tracer = tracer ?? trace2.getTracer("@thesight/sdk");
|
|
200
309
|
}
|
|
201
310
|
/**
|
|
202
311
|
* Register an IDL for a single program. Convenience forwarder for the
|
|
@@ -213,131 +322,164 @@ var InstrumentedConnection = class extends Connection {
|
|
|
213
322
|
registerIdls(idls) {
|
|
214
323
|
this.idlResolver.registerMany(idls);
|
|
215
324
|
}
|
|
325
|
+
// ─── Overridden method ────────────────────────────────────────────────────
|
|
216
326
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
327
|
+
* Overrides the parent `Connection.sendRawTransaction` so every submit —
|
|
328
|
+
* including those made by Anchor's `provider.sendAndConfirm`, web3.js's
|
|
329
|
+
* top-level `sendAndConfirmTransaction`, and wallet adapter flows — is
|
|
330
|
+
* observed by an OTel span automatically.
|
|
331
|
+
*
|
|
332
|
+
* The span is a child of whatever OTel context is active when the call
|
|
333
|
+
* fires, so app-level parent spans (HTTP handlers, job runs, wallet flow
|
|
334
|
+
* spans from another SDK) nest the Sight span correctly.
|
|
219
335
|
*/
|
|
220
|
-
async
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
336
|
+
async sendRawTransaction(rawTransaction, options) {
|
|
337
|
+
if (this.sightConfig.disableAutoSpan) {
|
|
338
|
+
return super.sendRawTransaction(rawTransaction, options);
|
|
339
|
+
}
|
|
340
|
+
const span = this.tracer.startSpan("solana.sendRawTransaction", {}, context2.active());
|
|
224
341
|
const submitStart = Date.now();
|
|
342
|
+
let signature;
|
|
225
343
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
{ skipPreflight: false, ...options }
|
|
229
|
-
);
|
|
230
|
-
span.setAttribute("solana.tx.signature", signature);
|
|
344
|
+
signature = await super.sendRawTransaction(rawTransaction, options);
|
|
345
|
+
} catch (err) {
|
|
231
346
|
const submitMs = Date.now() - submitStart;
|
|
232
347
|
span.setAttribute("solana.tx.submit_ms", submitMs);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
span.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (decoded.errorMsg) {
|
|
269
|
-
span.setAttribute("solana.tx.error_msg", decoded.errorMsg);
|
|
270
|
-
}
|
|
271
|
-
span.end();
|
|
272
|
-
return { signature, error: decoded };
|
|
273
|
-
}
|
|
348
|
+
span.setAttribute("solana.tx.status", "failed");
|
|
349
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
350
|
+
span.setStatus({
|
|
351
|
+
code: SpanStatusCode2.ERROR,
|
|
352
|
+
message: err instanceof Error ? err.message : String(err)
|
|
353
|
+
});
|
|
354
|
+
span.end();
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
span.setAttribute("solana.tx.signature", signature);
|
|
358
|
+
span.setAttribute("solana.tx.submit_ms", Date.now() - submitStart);
|
|
359
|
+
span.setAttribute("solana.tx.status", "submitted");
|
|
360
|
+
void this.enrichSpanInBackground(span, signature, submitStart);
|
|
361
|
+
return signature;
|
|
362
|
+
}
|
|
363
|
+
// ─── Background enrichment ───────────────────────────────────────────────
|
|
364
|
+
/**
|
|
365
|
+
* Poll `getTransaction` until the on-chain record is available, then
|
|
366
|
+
* enrich the span with CU attribution, program names, decoded errors,
|
|
367
|
+
* and per-CPI events.
|
|
368
|
+
*
|
|
369
|
+
* This runs async and is never awaited by callers of `sendRawTransaction`.
|
|
370
|
+
* Failures inside this method attach to the span via recordException
|
|
371
|
+
* rather than throwing.
|
|
372
|
+
*/
|
|
373
|
+
async enrichSpanInBackground(span, signature, submitStart) {
|
|
374
|
+
const enrichStart = Date.now();
|
|
375
|
+
const deadline = enrichStart + (this.sightConfig.enrichmentTimeoutMs ?? 3e4);
|
|
376
|
+
const commitment = this.sightConfig.commitment ?? "confirmed";
|
|
377
|
+
const basePollMs = this.sightConfig.enrichmentPollIntervalMs ?? 500;
|
|
378
|
+
try {
|
|
379
|
+
const txDetails = await this.pollForTransaction(signature, commitment, deadline, basePollMs);
|
|
380
|
+
if (!txDetails) {
|
|
381
|
+
span.setAttribute("solana.tx.status", "timeout");
|
|
382
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
|
|
274
383
|
span.end();
|
|
275
|
-
return
|
|
384
|
+
return;
|
|
276
385
|
}
|
|
277
|
-
|
|
386
|
+
this.attachTxDetailsToSpan(span, txDetails);
|
|
278
387
|
if (!this.sightConfig.skipIdlResolution) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
maxSupportedTransactionVersion: 0
|
|
283
|
-
});
|
|
284
|
-
if (txDetails) {
|
|
285
|
-
const slot = txDetails.slot;
|
|
286
|
-
const fee = txDetails.meta?.fee;
|
|
287
|
-
const logs = txDetails.meta?.logMessages ?? [];
|
|
288
|
-
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
289
|
-
span.setAttribute("solana.tx.slot", slot);
|
|
290
|
-
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
291
|
-
if (cuUsed !== void 0) {
|
|
292
|
-
span.setAttribute("solana.tx.cu_used", cuUsed);
|
|
293
|
-
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
294
|
-
span.setAttribute("solana.tx.cu_utilization", parseFloat((cuUsed / 2e5 * 100).toFixed(1)));
|
|
295
|
-
}
|
|
296
|
-
const { cpiTree } = parseLogs({ logs });
|
|
297
|
-
if (!this.sightConfig.skipIdlResolution) {
|
|
298
|
-
await enrichTree(cpiTree, this.idlResolver);
|
|
299
|
-
}
|
|
300
|
-
const attributions = flatAttributions(cpiTree);
|
|
301
|
-
for (const attr of attributions) {
|
|
302
|
-
span.addEvent("cpi.invoke", {
|
|
303
|
-
"cpi.program": attr.programName ?? attr.programId,
|
|
304
|
-
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
305
|
-
"cpi.depth": attr.depth,
|
|
306
|
-
"cpi.cu_consumed": attr.cuConsumed,
|
|
307
|
-
"cpi.cu_self": attr.cuSelf,
|
|
308
|
-
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
const root = cpiTree.roots[0];
|
|
312
|
-
if (root) {
|
|
313
|
-
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
314
|
-
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
315
|
-
}
|
|
316
|
-
span.setAttribute("solana.tx.enrichment_ms", Date.now() - enrichStart);
|
|
317
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
318
|
-
span.end();
|
|
319
|
-
return { signature, cpiTree };
|
|
388
|
+
const logs = txDetails.meta?.logMessages ?? [];
|
|
389
|
+
if (logs.length > 0) {
|
|
390
|
+
await this.attachParsedLogsToSpan(span, logs);
|
|
320
391
|
}
|
|
321
392
|
}
|
|
322
|
-
span.
|
|
323
|
-
|
|
324
|
-
|
|
393
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
|
|
394
|
+
if (txDetails.meta?.err) {
|
|
395
|
+
span.setStatus({ code: SpanStatusCode2.ERROR });
|
|
396
|
+
} else {
|
|
397
|
+
span.setStatus({ code: SpanStatusCode2.OK });
|
|
398
|
+
}
|
|
325
399
|
} catch (err) {
|
|
326
|
-
span.
|
|
327
|
-
span.setAttribute(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
400
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
401
|
+
span.setAttribute(
|
|
402
|
+
"solana.tx.enrichment_error",
|
|
403
|
+
err instanceof Error ? err.message : String(err)
|
|
404
|
+
);
|
|
405
|
+
} finally {
|
|
332
406
|
span.end();
|
|
333
|
-
throw err;
|
|
334
407
|
}
|
|
335
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Poll `getTransaction(signature)` until either the on-chain record is
|
|
411
|
+
* returned or the deadline passes. Exponential backoff (1.5x) capped at
|
|
412
|
+
* 2 seconds to balance responsiveness against RPC load.
|
|
413
|
+
*/
|
|
414
|
+
async pollForTransaction(signature, commitment, deadline, basePollMs) {
|
|
415
|
+
let attempt = 0;
|
|
416
|
+
while (Date.now() < deadline) {
|
|
417
|
+
try {
|
|
418
|
+
const tx = await super.getTransaction(signature, {
|
|
419
|
+
commitment,
|
|
420
|
+
maxSupportedTransactionVersion: 0
|
|
421
|
+
});
|
|
422
|
+
if (tx) return tx;
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
attempt++;
|
|
426
|
+
const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
|
|
427
|
+
await sleep(waitMs);
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
/** Attach the flat fields (slot, fee, CU, status) from a tx response to a span. */
|
|
432
|
+
attachTxDetailsToSpan(span, txDetails) {
|
|
433
|
+
span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
|
|
434
|
+
span.setAttribute("solana.tx.slot", txDetails.slot);
|
|
435
|
+
const fee = txDetails.meta?.fee;
|
|
436
|
+
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
437
|
+
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
438
|
+
if (cuUsed !== void 0 && cuUsed !== null) {
|
|
439
|
+
span.setAttribute("solana.tx.cu_used", Number(cuUsed));
|
|
440
|
+
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
441
|
+
span.setAttribute(
|
|
442
|
+
"solana.tx.cu_utilization",
|
|
443
|
+
parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Parse program logs into a CPI tree, enrich with registered IDLs, and
|
|
449
|
+
* emit one `cpi.invoke` event per invocation onto the span. Root
|
|
450
|
+
* program/instruction names are copied onto span attributes so dashboards
|
|
451
|
+
* can filter by them without walking events.
|
|
452
|
+
*/
|
|
453
|
+
async attachParsedLogsToSpan(span, logs) {
|
|
454
|
+
const { cpiTree } = parseLogs2({ logs });
|
|
455
|
+
await enrichTree2(cpiTree, this.idlResolver);
|
|
456
|
+
const attributions = flatAttributions2(cpiTree);
|
|
457
|
+
for (const attr of attributions) {
|
|
458
|
+
span.addEvent("cpi.invoke", {
|
|
459
|
+
"cpi.program": attr.programName ?? attr.programId,
|
|
460
|
+
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
461
|
+
"cpi.depth": attr.depth,
|
|
462
|
+
"cpi.cu_consumed": attr.cuConsumed,
|
|
463
|
+
"cpi.cu_self": attr.cuSelf,
|
|
464
|
+
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const root = cpiTree.roots[0];
|
|
468
|
+
if (root) {
|
|
469
|
+
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
470
|
+
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
471
|
+
}
|
|
472
|
+
return cpiTree;
|
|
473
|
+
}
|
|
336
474
|
};
|
|
475
|
+
function sleep(ms) {
|
|
476
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
477
|
+
}
|
|
337
478
|
export {
|
|
338
|
-
|
|
479
|
+
IdlResolver3 as IdlResolver,
|
|
339
480
|
InstrumentedConnection,
|
|
340
481
|
SightSpanExporter,
|
|
341
|
-
initSight
|
|
482
|
+
initSight,
|
|
483
|
+
trackSolanaTransaction
|
|
342
484
|
};
|
|
343
485
|
//# sourceMappingURL=index.js.map
|