@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.
- package/dist/cjs/events/geyser.js +295 -0
- package/dist/cjs/events/geyser.js.map +1 -0
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/postgres/sync.js +72 -4
- package/dist/cjs/postgres/sync.js.map +1 -1
- package/dist/esm/events/geyser.js +258 -0
- package/dist/esm/events/geyser.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/postgres/sync.js +72 -4
- package/dist/esm/postgres/sync.js.map +1 -1
- package/dist/types/events/geyser.d.ts +150 -0
- package/dist/types/events/geyser.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/postgres/sync.d.ts +26 -2
- package/dist/types/postgres/sync.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/events/geyser.ts +384 -0
- package/src/events/yellowstone.d.ts +7 -0
- package/src/index.ts +4 -0
- package/src/postgres/sync.ts +90 -4
|
@@ -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;
|
|
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.
|
|
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
|
+
}
|
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,
|
package/src/postgres/sync.ts
CHANGED
|
@@ -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("
|
|
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
|
// ═════════════════════════════════════════════
|