@oobe-protocol-labs/synapse-sap-sdk 0.6.2 → 0.6.3

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.
@@ -33,6 +33,7 @@
33
33
  * ```
34
34
  */
35
35
  import type { SapClient } from "../core/client";
36
+ import { type GeyserConfig } from "../events/geyser";
36
37
  import type { SapPostgres, SyncAllResult } from "./adapter";
37
38
  import type { SyncOptions } from "./types";
38
39
  /**
@@ -59,6 +60,7 @@ export declare class SapSyncEngine {
59
60
  private readonly debug;
60
61
  private intervalId;
61
62
  private logSubId;
63
+ private geyserStream;
62
64
  private running;
63
65
  constructor(pg: SapPostgres, client: SapClient, debug?: boolean);
64
66
  /**
@@ -111,10 +113,32 @@ export declare class SapSyncEngine {
111
113
  startEventStream(): Promise<void>;
112
114
  /**
113
115
  * @name stopEventStream
114
- * @description Unsubscribe from the program log stream.
116
+ * @description Unsubscribe from the program log stream (WebSocket or Geyser).
115
117
  * @since v0.1.0
116
118
  */
117
119
  stopEventStream(): Promise<void>;
120
+ /**
121
+ * @name startGeyserStream
122
+ * @description Subscribe to SAP program transactions via Yellowstone gRPC
123
+ * and insert parsed events into the `sap_events` table in real-time.
124
+ *
125
+ * Drop-in replacement for {@link startEventStream} with lower latency,
126
+ * no missed events, and automatic reconnection.
127
+ *
128
+ * Requires `@triton-one/yellowstone-grpc` to be installed.
129
+ *
130
+ * @param geyserConfig - Yellowstone gRPC connection config.
131
+ * @since v0.6.3
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * await sync.startGeyserStream({
136
+ * endpoint: "https://grpc.triton.one",
137
+ * token: process.env.GEYSER_TOKEN!,
138
+ * });
139
+ * ```
140
+ */
141
+ startGeyserStream(geyserConfig: GeyserConfig): Promise<void>;
118
142
  /**
119
143
  * @name isRunning
120
144
  * @description Check whether the periodic sync is active.
@@ -123,7 +147,7 @@ export declare class SapSyncEngine {
123
147
  isRunning(): boolean;
124
148
  /**
125
149
  * @name isStreaming
126
- * @description Check whether the event stream is active.
150
+ * @description Check whether the event stream is active (WebSocket or Geyser).
127
151
  * @since v0.1.0
128
152
  */
129
153
  isStreaming(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../src/postgres/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3C;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAEhC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,OAAO,CAAS;gBAEZ,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,UAAQ;IAU7D;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAWxD;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,UAAU,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IAcvD;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,CAAC,YAAY;IAWpB;;;;;;;;;;;;;;;OAeG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAyCvC;;;;OAIG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAatC;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;OAIG;IACH,WAAW,IAAI,OAAO;IAQtB,OAAO,CAAC,GAAG;CAKZ"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../src/postgres/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3C;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAEhC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,OAAO,CAAS;gBAEZ,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,UAAQ;IAU7D;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAWxD;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,UAAU,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IAcvD;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,CAAC,YAAY;IAWpB;;;;;;;;;;;;;;;OAeG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAyCvC;;;;OAIG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBtC;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA0DlE;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;OAIG;IACH,WAAW,IAAI,OAAO;IAQtB,OAAO,CAAC,GAAG;CAKZ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oobe-protocol-labs/synapse-sap-sdk",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "TypeScript SDK for the Synapse Agent Protocol (SAP v2) on Solana",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",
@@ -166,6 +166,7 @@
166
166
  "peerDependencies": {
167
167
  "@coral-xyz/anchor": ">=0.30.0",
168
168
  "@solana/web3.js": ">=1.90.0",
169
+ "@triton-one/yellowstone-grpc": ">=1.0.0",
169
170
  "zod": ">=3.20.0",
170
171
  "pg": ">=8.0.0"
171
172
  },
@@ -175,6 +176,9 @@
175
176
  },
176
177
  "pg": {
177
178
  "optional": true
179
+ },
180
+ "@triton-one/yellowstone-grpc": {
181
+ "optional": true
178
182
  }
179
183
  },
180
184
  "engines": {
@@ -0,0 +1,384 @@
1
+ /**
2
+ * @module events/geyser
3
+ * @description Yellowstone gRPC (Geyser) event stream for SAP v2.
4
+ *
5
+ * Drop-in alternative to the WebSocket `connection.onLogs()` pipeline.
6
+ * Uses Triton / Helius / any Yellowstone-compatible gRPC endpoint to
7
+ * receive program transaction updates with sub-second latency and
8
+ * automatic reconnection.
9
+ *
10
+ * @category Events
11
+ * @since v0.6.3
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { GeyserEventStream } from "@oobe-protocol-labs/synapse-sap-sdk";
16
+ * import { EventParser } from "@oobe-protocol-labs/synapse-sap-sdk";
17
+ *
18
+ * const stream = new GeyserEventStream({
19
+ * endpoint: "https://grpc.triton.one",
20
+ * token: process.env.GEYSER_TOKEN!,
21
+ * programId: "SAPpUhsWLJG1FfkGRcXagEDMrMsWGjbky7AyhGpFETZ",
22
+ * });
23
+ *
24
+ * const parser = new EventParser(program);
25
+ *
26
+ * stream.on("logs", (logs, signature, slot) => {
27
+ * const events = parser.parseLogs(logs);
28
+ * for (const event of events) {
29
+ * console.log(event.name, event.data);
30
+ * }
31
+ * });
32
+ *
33
+ * stream.on("error", (err) => console.error("gRPC error:", err));
34
+ *
35
+ * await stream.connect();
36
+ * // ... later
37
+ * await stream.disconnect();
38
+ * ```
39
+ */
40
+
41
+ import { EventEmitter } from "events";
42
+
43
+ // ═══════════════════════════════════════════════════════════════════
44
+ // Types
45
+ // ═══════════════════════════════════════════════════════════════════
46
+
47
+ /**
48
+ * Configuration for the Yellowstone gRPC event stream.
49
+ *
50
+ * @interface GeyserConfig
51
+ * @since v0.6.3
52
+ */
53
+ export interface GeyserConfig {
54
+ /** Yellowstone gRPC endpoint URL (e.g. "https://grpc.triton.one") */
55
+ endpoint: string;
56
+
57
+ /** Authentication token for the gRPC endpoint */
58
+ token: string;
59
+
60
+ /** SAP program ID to filter. Defaults to SAP v2 program. */
61
+ programId?: string;
62
+
63
+ /**
64
+ * Commitment level for the subscription.
65
+ * @default "confirmed"
66
+ */
67
+ commitment?: "processed" | "confirmed" | "finalized";
68
+
69
+ /**
70
+ * Automatically reconnect on disconnect.
71
+ * @default true
72
+ */
73
+ autoReconnect?: boolean;
74
+
75
+ /**
76
+ * Delay between reconnection attempts in ms.
77
+ * @default 3000
78
+ */
79
+ reconnectDelayMs?: number;
80
+
81
+ /**
82
+ * Maximum number of reconnection attempts. 0 = unlimited.
83
+ * @default 0
84
+ */
85
+ maxReconnectAttempts?: number;
86
+
87
+ /**
88
+ * Include failed transactions in the stream.
89
+ * @default false
90
+ */
91
+ includeFailedTxs?: boolean;
92
+ }
93
+
94
+ /**
95
+ * Events emitted by {@link GeyserEventStream}.
96
+ *
97
+ * @interface GeyserStreamEvents
98
+ * @since v0.6.3
99
+ */
100
+ export interface GeyserStreamEvents {
101
+ /**
102
+ * Emitted for each transaction's log messages.
103
+ * Same shape as `connection.onLogs()` callback — plug into `EventParser.parseLogs()`.
104
+ */
105
+ logs: (logs: string[], signature: string, slot: number) => void;
106
+
107
+ /** Emitted when the gRPC stream connects or reconnects. */
108
+ connected: () => void;
109
+
110
+ /** Emitted when the stream disconnects. */
111
+ disconnected: (reason: string) => void;
112
+
113
+ /** Emitted on transport or parsing errors. */
114
+ error: (err: Error) => void;
115
+
116
+ /** Emitted on reconnection attempt. */
117
+ reconnecting: (attempt: number) => void;
118
+ }
119
+
120
+ // ═══════════════════════════════════════════════════════════════════
121
+ // Constants
122
+ // ═══════════════════════════════════════════════════════════════════
123
+
124
+ const SAP_PROGRAM_ID = "SAPpUhsWLJG1FfkGRcXagEDMrMsWGjbky7AyhGpFETZ";
125
+
126
+ const COMMITMENT_MAP: Record<string, number> = {
127
+ processed: 0,
128
+ confirmed: 1,
129
+ finalized: 2,
130
+ };
131
+
132
+ // ═══════════════════════════════════════════════════════════════════
133
+ // GeyserEventStream
134
+ // ═══════════════════════════════════════════════════════════════════
135
+
136
+ /**
137
+ * Yellowstone gRPC event stream for SAP v2 programs.
138
+ *
139
+ * Wraps `@triton-one/yellowstone-grpc` and emits parsed log lines
140
+ * compatible with the existing {@link EventParser}.
141
+ *
142
+ * @name GeyserEventStream
143
+ * @category Events
144
+ * @since v0.6.3
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * const stream = new GeyserEventStream({
149
+ * endpoint: "https://grpc.triton.one",
150
+ * token: process.env.GEYSER_TOKEN!,
151
+ * });
152
+ *
153
+ * stream.on("logs", (logs, sig, slot) => {
154
+ * const events = parser.parseLogs(logs);
155
+ * events.forEach(e => db.insertEvent(e));
156
+ * });
157
+ *
158
+ * await stream.connect();
159
+ * ```
160
+ */
161
+ export class GeyserEventStream extends EventEmitter {
162
+ private readonly config: Required<GeyserConfig>;
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ private client: any = null;
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
+ private stream: any = null;
167
+ private reconnectAttempts = 0;
168
+ private _connected = false;
169
+ private _stopped = false;
170
+
171
+ constructor(config: GeyserConfig) {
172
+ super();
173
+ this.config = {
174
+ endpoint: config.endpoint,
175
+ token: config.token,
176
+ programId: config.programId ?? SAP_PROGRAM_ID,
177
+ commitment: config.commitment ?? "confirmed",
178
+ autoReconnect: config.autoReconnect ?? true,
179
+ reconnectDelayMs: config.reconnectDelayMs ?? 3_000,
180
+ maxReconnectAttempts: config.maxReconnectAttempts ?? 0,
181
+ includeFailedTxs: config.includeFailedTxs ?? false,
182
+ };
183
+ }
184
+
185
+ /** Whether the gRPC stream is currently connected. */
186
+ get connected(): boolean {
187
+ return this._connected;
188
+ }
189
+
190
+ /**
191
+ * Connect to the Yellowstone gRPC endpoint and start streaming.
192
+ *
193
+ * @throws If `@triton-one/yellowstone-grpc` is not installed.
194
+ */
195
+ async connect(): Promise<void> {
196
+ this._stopped = false;
197
+ this.reconnectAttempts = 0;
198
+
199
+ // Dynamic import — yellowstone is an optional peer dependency
200
+ let YellowstoneClient: new (...args: unknown[]) => unknown;
201
+ try {
202
+ const mod = await import("@triton-one/yellowstone-grpc");
203
+ YellowstoneClient = mod.default ?? mod.Client;
204
+ } catch {
205
+ throw new Error(
206
+ "Missing dependency: @triton-one/yellowstone-grpc\n" +
207
+ "Install it with: npm i @triton-one/yellowstone-grpc",
208
+ );
209
+ }
210
+
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ this.client = new (YellowstoneClient as any)(
213
+ this.config.endpoint,
214
+ this.config.token,
215
+ undefined, // TLS options — use defaults
216
+ );
217
+
218
+ await this.subscribe();
219
+ }
220
+
221
+ /**
222
+ * Disconnect from the gRPC stream and stop reconnection.
223
+ */
224
+ async disconnect(): Promise<void> {
225
+ this._stopped = true;
226
+ this._connected = false;
227
+
228
+ if (this.stream) {
229
+ try {
230
+ this.stream.cancel?.();
231
+ this.stream = null;
232
+ } catch {
233
+ // ignore cancel errors
234
+ }
235
+ }
236
+
237
+ this.emit("disconnected", "manual");
238
+ }
239
+
240
+ // ─── Internal ──────────────────────────────────────
241
+
242
+ private async subscribe(): Promise<void> {
243
+ if (!this.client || this._stopped) return;
244
+
245
+ try {
246
+ this.stream = await this.client.subscribe();
247
+ } catch (err) {
248
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
249
+ await this.maybeReconnect();
250
+ return;
251
+ }
252
+
253
+ // Build the subscription request
254
+ const request = {
255
+ accounts: {},
256
+ slots: {},
257
+ transactions: {
258
+ sap: {
259
+ vote: false,
260
+ failed: this.config.includeFailedTxs,
261
+ signature: undefined,
262
+ accountInclude: [this.config.programId],
263
+ accountExclude: [],
264
+ accountRequired: [],
265
+ },
266
+ },
267
+ transactionsStatus: {},
268
+ blocks: {},
269
+ blocksMeta: {},
270
+ entry: {},
271
+ commitment: COMMITMENT_MAP[this.config.commitment] ?? 1,
272
+ accountsDataSlice: [],
273
+ ping: { id: 1 },
274
+ };
275
+
276
+ // Send subscription
277
+ try {
278
+ await new Promise<void>((resolve, reject) => {
279
+ this.stream.write(request, (err: Error | null) => {
280
+ if (err) reject(err);
281
+ else resolve();
282
+ });
283
+ });
284
+ } catch (err) {
285
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
286
+ await this.maybeReconnect();
287
+ return;
288
+ }
289
+
290
+ this._connected = true;
291
+ this.reconnectAttempts = 0;
292
+ this.emit("connected");
293
+
294
+ // Listen for data
295
+ this.stream.on("data", (data: GeyserUpdateMessage) => {
296
+ try {
297
+ this.handleMessage(data);
298
+ } catch (err) {
299
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
300
+ }
301
+ });
302
+
303
+ this.stream.on("error", (err: Error) => {
304
+ this.emit("error", err);
305
+ });
306
+
307
+ this.stream.on("end", () => {
308
+ this._connected = false;
309
+ this.emit("disconnected", "stream-end");
310
+ this.maybeReconnect();
311
+ });
312
+
313
+ this.stream.on("close", () => {
314
+ this._connected = false;
315
+ this.emit("disconnected", "stream-close");
316
+ this.maybeReconnect();
317
+ });
318
+ }
319
+
320
+ private handleMessage(data: GeyserUpdateMessage): void {
321
+ // Respond to pings to keep the stream alive
322
+ if (data.ping) {
323
+ this.stream?.write({ ping: { id: data.ping.id } }, () => {});
324
+ return;
325
+ }
326
+
327
+ // Extract transaction data
328
+ const tx = data.transaction;
329
+ if (!tx?.transaction?.transaction) return;
330
+
331
+ const meta = tx.transaction.meta;
332
+ if (!meta) return;
333
+
334
+ // Extract log messages from the transaction meta
335
+ const logs: string[] = meta.logMessages ?? [];
336
+ if (logs.length === 0) return;
337
+
338
+ const signature = tx.transaction.signature
339
+ ? Buffer.from(tx.transaction.signature).toString("base64")
340
+ : "unknown";
341
+
342
+ const slot = Number(tx.slot ?? 0);
343
+
344
+ this.emit("logs", logs, signature, slot);
345
+ }
346
+
347
+ private async maybeReconnect(): Promise<void> {
348
+ if (this._stopped || !this.config.autoReconnect) return;
349
+
350
+ const max = this.config.maxReconnectAttempts;
351
+ if (max > 0 && this.reconnectAttempts >= max) {
352
+ this.emit(
353
+ "error",
354
+ new Error(`Max reconnect attempts (${max}) exceeded`),
355
+ );
356
+ return;
357
+ }
358
+
359
+ this.reconnectAttempts++;
360
+ this.emit("reconnecting", this.reconnectAttempts);
361
+
362
+ await new Promise((r) => setTimeout(r, this.config.reconnectDelayMs));
363
+
364
+ if (!this._stopped) {
365
+ await this.subscribe();
366
+ }
367
+ }
368
+ }
369
+
370
+ // ─── Internal Yellowstone message types ─────────────
371
+
372
+ interface GeyserUpdateMessage {
373
+ ping?: { id: number };
374
+ transaction?: {
375
+ slot?: string | number;
376
+ transaction?: {
377
+ signature?: Uint8Array;
378
+ transaction?: unknown;
379
+ meta?: {
380
+ logMessages?: string[];
381
+ };
382
+ };
383
+ };
384
+ }
@@ -0,0 +1,7 @@
1
+ // Optional peer dependency — only needed when using GeyserEventStream
2
+ declare module "@triton-one/yellowstone-grpc" {
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ const Client: any;
5
+ export default Client;
6
+ export { Client };
7
+ }
package/src/index.ts CHANGED
@@ -240,6 +240,10 @@ export type {
240
240
  LedgerEntryEventData,
241
241
  } from "./events";
242
242
 
243
+ // ── Geyser (Yellowstone gRPC) ────────────────────────
244
+ export { GeyserEventStream } from "./events/geyser";
245
+ export type { GeyserConfig, GeyserStreamEvents } from "./events/geyser";
246
+
243
247
  // ── Modules (for advanced usage / tree-shaking) ──────
244
248
  export {
245
249
  AgentModule,
@@ -35,6 +35,7 @@
35
35
 
36
36
  import type { SapClient } from "../core/client";
37
37
  import { EventParser } from "../events";
38
+ import { GeyserEventStream, type GeyserConfig } from "../events/geyser";
38
39
  import type { SapPostgres, SyncAllResult } from "./adapter";
39
40
  import type { SyncOptions } from "./types";
40
41
 
@@ -67,6 +68,7 @@ export class SapSyncEngine {
67
68
 
68
69
  private intervalId: ReturnType<typeof setInterval> | null = null;
69
70
  private logSubId: number | null = null;
71
+ private geyserStream: GeyserEventStream | null = null;
70
72
  private running = false;
71
73
 
72
74
  constructor(pg: SapPostgres, client: SapClient, debug = false) {
@@ -208,7 +210,7 @@ export class SapSyncEngine {
208
210
 
209
211
  /**
210
212
  * @name stopEventStream
211
- * @description Unsubscribe from the program log stream.
213
+ * @description Unsubscribe from the program log stream (WebSocket or Geyser).
212
214
  * @since v0.1.0
213
215
  */
214
216
  async stopEventStream(): Promise<void> {
@@ -216,8 +218,92 @@ export class SapSyncEngine {
216
218
  const connection = this.client.program.provider.connection;
217
219
  await connection.removeOnLogsListener(this.logSubId);
218
220
  this.logSubId = null;
219
- this.log("Event stream stopped");
221
+ this.log("WebSocket event stream stopped");
220
222
  }
223
+ if (this.geyserStream) {
224
+ await this.geyserStream.disconnect();
225
+ this.geyserStream = null;
226
+ this.log("Geyser event stream stopped");
227
+ }
228
+ }
229
+
230
+ // ═════════════════════════════════════════════
231
+ // Geyser gRPC Event Stream
232
+ // ═════════════════════════════════════════════
233
+
234
+ /**
235
+ * @name startGeyserStream
236
+ * @description Subscribe to SAP program transactions via Yellowstone gRPC
237
+ * and insert parsed events into the `sap_events` table in real-time.
238
+ *
239
+ * Drop-in replacement for {@link startEventStream} with lower latency,
240
+ * no missed events, and automatic reconnection.
241
+ *
242
+ * Requires `@triton-one/yellowstone-grpc` to be installed.
243
+ *
244
+ * @param geyserConfig - Yellowstone gRPC connection config.
245
+ * @since v0.6.3
246
+ *
247
+ * @example
248
+ * ```ts
249
+ * await sync.startGeyserStream({
250
+ * endpoint: "https://grpc.triton.one",
251
+ * token: process.env.GEYSER_TOKEN!,
252
+ * });
253
+ * ```
254
+ */
255
+ async startGeyserStream(geyserConfig: GeyserConfig): Promise<void> {
256
+ if (this.geyserStream) {
257
+ this.log("Geyser stream already running");
258
+ return;
259
+ }
260
+
261
+ const eventParser = new EventParser(this.client.program);
262
+ const stream = new GeyserEventStream(geyserConfig);
263
+
264
+ stream.on("logs", async (logs: string[], signature: string, slot: number) => {
265
+ try {
266
+ const events = eventParser.parseLogs(logs);
267
+ for (const event of events) {
268
+ const data = event.data as Record<string, unknown>;
269
+ const agentPda =
270
+ (data.agent as string) ?? (data.agentPda as string) ?? undefined;
271
+ const wallet =
272
+ (data.wallet as string) ?? (data.owner as string) ?? undefined;
273
+
274
+ await this.pg.syncEvent(
275
+ event.name,
276
+ signature,
277
+ slot,
278
+ data,
279
+ agentPda,
280
+ wallet,
281
+ );
282
+ }
283
+ } catch (err) {
284
+ this.log(`Geyser event parse error: ${err}`);
285
+ }
286
+ });
287
+
288
+ stream.on("connected", () => {
289
+ this.log("Geyser gRPC connected");
290
+ });
291
+
292
+ stream.on("disconnected", (reason: string) => {
293
+ this.log(`Geyser gRPC disconnected: ${reason}`);
294
+ });
295
+
296
+ stream.on("error", (err: Error) => {
297
+ this.log(`Geyser error: ${err.message}`);
298
+ });
299
+
300
+ stream.on("reconnecting", (attempt: number) => {
301
+ this.log(`Geyser reconnecting (attempt ${attempt})...`);
302
+ });
303
+
304
+ await stream.connect();
305
+ this.geyserStream = stream;
306
+ this.log("Geyser event stream started");
221
307
  }
222
308
 
223
309
  // ═════════════════════════════════════════════
@@ -235,11 +321,11 @@ export class SapSyncEngine {
235
321
 
236
322
  /**
237
323
  * @name isStreaming
238
- * @description Check whether the event stream is active.
324
+ * @description Check whether the event stream is active (WebSocket or Geyser).
239
325
  * @since v0.1.0
240
326
  */
241
327
  isStreaming(): boolean {
242
- return this.logSubId !== null;
328
+ return this.logSubId !== null || this.geyserStream?.connected === true;
243
329
  }
244
330
 
245
331
  // ═════════════════════════════════════════════