@drift-labs/sdk 2.96.0-beta.1 → 2.96.0-beta.2

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.96.0-beta.1
1
+ 2.96.0-beta.2
@@ -15,12 +15,19 @@ export declare class EventSubscriber {
15
15
  private awaitTxPromises;
16
16
  private awaitTxResolver;
17
17
  private logProvider;
18
+ private currentProviderType;
18
19
  eventEmitter: StrictEventEmitter<EventEmitter, EventSubscriberEvents>;
19
20
  private lastSeenSlot;
20
21
  private lastSeenBlockTime;
21
22
  lastSeenTxSig: string;
22
23
  constructor(connection: Connection, program: Program, options?: EventSubscriptionOptions);
24
+ private initializeLogProvider;
23
25
  private populateInitialEventListMap;
26
+ /**
27
+ * Implements fallback logic for reconnecting to LogProvider. Currently terminates at polling,
28
+ * could be improved to try the original type again after some cooldown.
29
+ */
30
+ private updateFallbackProviderType;
24
31
  subscribe(): Promise<boolean>;
25
32
  private handleTxLogs;
26
33
  fetchPreviousTx(fetchMax?: boolean): Promise<void>;
@@ -10,6 +10,7 @@ const webSocketLogProvider_1 = require("./webSocketLogProvider");
10
10
  const events_1 = require("events");
11
11
  const sort_1 = require("./sort");
12
12
  const parse_1 = require("./parse");
13
+ const eventsServerLogProvider_1 = require("./eventsServerLogProvider");
13
14
  class EventSubscriber {
14
15
  constructor(connection, program, options = types_1.DefaultEventSubscriptionOptions) {
15
16
  var _a;
@@ -23,15 +24,36 @@ class EventSubscriber {
23
24
  this.txEventCache = new txEventCache_1.TxEventCache(this.options.maxTx);
24
25
  this.eventListMap = new Map();
25
26
  this.eventEmitter = new events_1.EventEmitter();
26
- if (this.options.logProviderConfig.type === 'websocket') {
27
+ this.currentProviderType = this.options.logProviderConfig.type;
28
+ this.initializeLogProvider();
29
+ }
30
+ initializeLogProvider(subscribe = false) {
31
+ if (this.currentProviderType === 'websocket') {
32
+ const logProviderConfig = this.options
33
+ .logProviderConfig;
27
34
  this.logProvider = new webSocketLogProvider_1.WebSocketLogProvider(
28
35
  // @ts-ignore
29
- this.connection, this.address, this.options.commitment, this.options.logProviderConfig.resubTimeoutMs);
36
+ this.connection, this.address, this.options.commitment, logProviderConfig.resubTimeoutMs);
30
37
  }
31
- else {
38
+ else if (this.currentProviderType === 'polling') {
39
+ const logProviderConfig = this.options
40
+ .logProviderConfig;
32
41
  this.logProvider = new pollingLogProvider_1.PollingLogProvider(
33
42
  // @ts-ignore
34
- this.connection, this.address, options.commitment, this.options.logProviderConfig.frequency, this.options.logProviderConfig.batchSize);
43
+ this.connection, this.address, this.options.commitment, logProviderConfig.frequency, logProviderConfig.batchSize);
44
+ }
45
+ else if (this.currentProviderType === 'events-server') {
46
+ const logProviderConfig = this.options
47
+ .logProviderConfig;
48
+ this.logProvider = new eventsServerLogProvider_1.EventsServerLogProvider(logProviderConfig.url, this.options.eventTypes, this.options.address ? this.options.address.toString() : undefined);
49
+ }
50
+ else {
51
+ throw new Error(`Invalid log provider type: ${this.currentProviderType}`);
52
+ }
53
+ if (subscribe) {
54
+ this.logProvider.subscribe((txSig, slot, logs, mostRecentBlockTime, txSigIndex) => {
55
+ this.handleTxLogs(txSig, slot, logs, mostRecentBlockTime, this.currentProviderType === 'events-server', txSigIndex);
56
+ }, true);
35
57
  }
36
58
  }
37
59
  populateInitialEventListMap() {
@@ -39,37 +61,51 @@ class EventSubscriber {
39
61
  this.eventListMap.set(eventType, new eventList_1.EventList(eventType, this.options.maxEventsPerType, (0, sort_1.getSortFn)(this.options.orderBy, this.options.orderDir), this.options.orderDir));
40
62
  }
41
63
  }
64
+ /**
65
+ * Implements fallback logic for reconnecting to LogProvider. Currently terminates at polling,
66
+ * could be improved to try the original type again after some cooldown.
67
+ */
68
+ updateFallbackProviderType(reconnectAttempts, maxReconnectAttempts) {
69
+ if (reconnectAttempts < maxReconnectAttempts) {
70
+ return;
71
+ }
72
+ let nextProviderType = this.currentProviderType;
73
+ if (this.currentProviderType === 'events-server') {
74
+ nextProviderType = 'websocket';
75
+ }
76
+ else if (this.currentProviderType === 'websocket') {
77
+ nextProviderType = 'polling';
78
+ }
79
+ else if (this.currentProviderType === 'polling') {
80
+ nextProviderType = 'polling';
81
+ }
82
+ console.log(`EventSubscriber: Failing over providerType ${this.currentProviderType} to ${nextProviderType}`);
83
+ this.currentProviderType = nextProviderType;
84
+ }
42
85
  async subscribe() {
43
86
  try {
44
87
  if (this.logProvider.isSubscribed()) {
45
88
  return true;
46
89
  }
47
90
  this.populateInitialEventListMap();
48
- if (this.options.logProviderConfig.type === 'websocket') {
49
- if (this.options.logProviderConfig.resubTimeoutMs) {
50
- if (this.options.logProviderConfig.maxReconnectAttempts &&
51
- this.options.logProviderConfig.maxReconnectAttempts > 0) {
52
- const logProviderConfig = this.options
53
- .logProviderConfig;
54
- this.logProvider.eventEmitter.on('reconnect', (reconnectAttempts) => {
55
- if (reconnectAttempts > logProviderConfig.maxReconnectAttempts) {
56
- console.log('Failing over to polling');
57
- this.logProvider.eventEmitter.removeAllListeners('reconnect');
58
- this.unsubscribe().then(() => {
59
- this.logProvider = new pollingLogProvider_1.PollingLogProvider(
60
- // @ts-ignore
61
- this.connection, this.address, this.options.commitment, logProviderConfig.fallbackFrequency, logProviderConfig.fallbackBatchSize);
62
- this.logProvider.subscribe((txSig, slot, logs, mostRecentBlockTime) => {
63
- this.handleTxLogs(txSig, slot, logs, mostRecentBlockTime);
64
- }, true);
65
- });
66
- }
67
- });
68
- }
91
+ if (this.options.logProviderConfig.type === 'websocket' ||
92
+ this.options.logProviderConfig.type === 'events-server') {
93
+ const logProviderConfig = this.options
94
+ .logProviderConfig;
95
+ if (this.logProvider.eventEmitter) {
96
+ this.logProvider.eventEmitter.on('reconnect', async (reconnectAttempts) => {
97
+ if (reconnectAttempts > logProviderConfig.maxReconnectAttempts) {
98
+ console.log(`EventSubscriber: Reconnect attempts ${reconnectAttempts}/${logProviderConfig.maxReconnectAttempts}, reconnecting...`);
99
+ this.logProvider.eventEmitter.removeAllListeners('reconnect');
100
+ await this.unsubscribe();
101
+ this.updateFallbackProviderType(reconnectAttempts, logProviderConfig.maxReconnectAttempts);
102
+ this.initializeLogProvider(true);
103
+ }
104
+ });
69
105
  }
70
106
  }
71
- this.logProvider.subscribe((txSig, slot, logs, mostRecentBlockTime) => {
72
- this.handleTxLogs(txSig, slot, logs, mostRecentBlockTime);
107
+ this.logProvider.subscribe((txSig, slot, logs, mostRecentBlockTime, txSigIndex) => {
108
+ this.handleTxLogs(txSig, slot, logs, mostRecentBlockTime, this.currentProviderType === 'events-server', txSigIndex);
73
109
  }, true);
74
110
  return true;
75
111
  }
@@ -79,11 +115,11 @@ class EventSubscriber {
79
115
  return false;
80
116
  }
81
117
  }
82
- handleTxLogs(txSig, slot, logs, mostRecentBlockTime) {
83
- if (this.txEventCache.has(txSig)) {
118
+ handleTxLogs(txSig, slot, logs, mostRecentBlockTime, fromEventsServer = false, txSigIndex = undefined) {
119
+ if (!fromEventsServer && this.txEventCache.has(txSig)) {
84
120
  return;
85
121
  }
86
- const wrappedEvents = this.parseEventsFromLogs(txSig, slot, logs);
122
+ const wrappedEvents = this.parseEventsFromLogs(txSig, slot, logs, txSigIndex);
87
123
  for (const wrappedEvent of wrappedEvents) {
88
124
  this.eventListMap.get(wrappedEvent.eventType).insert(wrappedEvent);
89
125
  }
@@ -134,7 +170,7 @@ class EventSubscriber {
134
170
  this.awaitTxResolver.clear();
135
171
  return await this.logProvider.unsubscribe(true);
136
172
  }
137
- parseEventsFromLogs(txSig, slot, logs) {
173
+ parseEventsFromLogs(txSig, slot, logs, txSigIndex) {
138
174
  const records = [];
139
175
  // @ts-ignore
140
176
  const events = (0, parse_1.parseLogs)(this.program, logs);
@@ -146,7 +182,8 @@ class EventSubscriber {
146
182
  event.data.txSig = txSig;
147
183
  event.data.slot = slot;
148
184
  event.data.eventType = event.name;
149
- event.data.txSigIndex = runningEventIndex;
185
+ event.data.txSigIndex =
186
+ txSigIndex !== undefined ? txSigIndex : runningEventIndex;
150
187
  records.push(event.data);
151
188
  }
152
189
  runningEventIndex++;
@@ -0,0 +1,21 @@
1
+ /// <reference types="node" />
2
+ import { logProviderCallback, EventType, LogProvider } from './types';
3
+ import { EventEmitter } from 'events';
4
+ export declare class EventsServerLogProvider implements LogProvider {
5
+ private readonly url;
6
+ private readonly eventTypes;
7
+ private readonly userAccount?;
8
+ private ws?;
9
+ private callback?;
10
+ private isUnsubscribing;
11
+ private externalUnsubscribe;
12
+ private lastHeartbeat;
13
+ private timeoutId?;
14
+ private reconnectAttempts;
15
+ eventEmitter?: EventEmitter;
16
+ constructor(url: string, eventTypes: EventType[], userAccount?: string);
17
+ isSubscribed(): boolean;
18
+ subscribe(callback: logProviderCallback): Promise<boolean>;
19
+ unsubscribe(external?: boolean): Promise<boolean>;
20
+ private setTimeout;
21
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsServerLogProvider = void 0;
4
+ const events_1 = require("events");
5
+ // browser support
6
+ let WebSocketImpl;
7
+ if (typeof window !== 'undefined' && window.WebSocket) {
8
+ WebSocketImpl = window.WebSocket;
9
+ }
10
+ else {
11
+ WebSocketImpl = require('ws');
12
+ }
13
+ const EVENT_SERVER_HEARTBEAT_INTERVAL_MS = 5000;
14
+ const ALLOWED_MISSED_HEARTBEATS = 3;
15
+ class EventsServerLogProvider {
16
+ constructor(url, eventTypes, userAccount) {
17
+ this.url = url;
18
+ this.eventTypes = eventTypes;
19
+ this.userAccount = userAccount;
20
+ this.isUnsubscribing = false;
21
+ this.externalUnsubscribe = false;
22
+ this.lastHeartbeat = 0;
23
+ this.reconnectAttempts = 0;
24
+ this.eventEmitter = new events_1.EventEmitter();
25
+ }
26
+ isSubscribed() {
27
+ return this.ws !== undefined;
28
+ }
29
+ async subscribe(callback) {
30
+ if (this.ws !== undefined) {
31
+ return true;
32
+ }
33
+ this.ws = new WebSocketImpl(this.url);
34
+ this.callback = callback;
35
+ this.ws.addEventListener('open', () => {
36
+ for (const channel of this.eventTypes) {
37
+ const subscribeMessage = {
38
+ type: 'subscribe',
39
+ channel: channel,
40
+ };
41
+ if (this.userAccount) {
42
+ subscribeMessage['user'] = this.userAccount;
43
+ }
44
+ this.ws.send(JSON.stringify(subscribeMessage));
45
+ }
46
+ this.reconnectAttempts = 0;
47
+ });
48
+ this.ws.addEventListener('message', (data) => {
49
+ try {
50
+ if (!this.isUnsubscribing) {
51
+ clearTimeout(this.timeoutId);
52
+ this.setTimeout();
53
+ if (this.reconnectAttempts > 0) {
54
+ console.log('eventsServerLogProvider: Resetting reconnect attempts to 0');
55
+ }
56
+ this.reconnectAttempts = 0;
57
+ }
58
+ const parsedData = JSON.parse(data.data.toString());
59
+ if (parsedData.channel === 'heartbeat') {
60
+ this.lastHeartbeat = Date.now();
61
+ return;
62
+ }
63
+ if (parsedData.message !== undefined) {
64
+ return;
65
+ }
66
+ const event = JSON.parse(parsedData.data);
67
+ this.callback(event.txSig, event.slot, [
68
+ 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]',
69
+ event.rawLog,
70
+ 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success',
71
+ ], undefined, event.txSigIndex);
72
+ }
73
+ catch (error) {
74
+ console.error('Error parsing message:', error);
75
+ }
76
+ });
77
+ this.ws.addEventListener('close', () => {
78
+ console.log('eventsServerLogProvider: WebSocket closed');
79
+ });
80
+ this.ws.addEventListener('error', (error) => {
81
+ console.error('eventsServerLogProvider: WebSocket error:', error);
82
+ });
83
+ this.setTimeout();
84
+ return true;
85
+ }
86
+ async unsubscribe(external = false) {
87
+ this.isUnsubscribing = true;
88
+ this.externalUnsubscribe = external;
89
+ if (this.timeoutId) {
90
+ clearInterval(this.timeoutId);
91
+ this.timeoutId = undefined;
92
+ }
93
+ if (this.ws !== undefined) {
94
+ this.ws.close();
95
+ this.ws = undefined;
96
+ return true;
97
+ }
98
+ else {
99
+ this.isUnsubscribing = false;
100
+ return true;
101
+ }
102
+ }
103
+ setTimeout() {
104
+ this.timeoutId = setTimeout(async () => {
105
+ if (this.isUnsubscribing || this.externalUnsubscribe) {
106
+ // If we are in the process of unsubscribing, do not attempt to resubscribe
107
+ return;
108
+ }
109
+ const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeat;
110
+ if (timeSinceLastHeartbeat >
111
+ EVENT_SERVER_HEARTBEAT_INTERVAL_MS * ALLOWED_MISSED_HEARTBEATS) {
112
+ console.log(`eventServerLogProvider: No heartbeat in ${timeSinceLastHeartbeat}ms, resubscribing on attempt ${this.reconnectAttempts + 1}`);
113
+ await this.unsubscribe();
114
+ this.reconnectAttempts++;
115
+ this.eventEmitter.emit('reconnect', this.reconnectAttempts);
116
+ this.subscribe(this.callback);
117
+ }
118
+ }, EVENT_SERVER_HEARTBEAT_INTERVAL_MS * 2);
119
+ }
120
+ }
121
+ exports.EventsServerLogProvider = EventsServerLogProvider;
@@ -30,7 +30,7 @@ class PollingLogProvider {
30
30
  this.firstFetch = false;
31
31
  const { mostRecentTx, transactionLogs } = response;
32
32
  for (const { txSig, slot, logs } of transactionLogs) {
33
- callback(txSig, slot, logs, response.mostRecentBlockTime);
33
+ callback(txSig, slot, logs, response.mostRecentBlockTime, undefined);
34
34
  }
35
35
  this.mostRecentSeenTx = mostRecentTx;
36
36
  }
@@ -48,23 +48,30 @@ export interface EventSubscriberEvents {
48
48
  newEvent: (event: WrappedEvent<EventType>) => void;
49
49
  }
50
50
  export type SortFn = (currentRecord: EventMap[EventType], newRecord: EventMap[EventType]) => 'less than' | 'greater than';
51
- export type logProviderCallback = (txSig: TransactionSignature, slot: number, logs: string[], mostRecentBlockTime: number | undefined) => void;
51
+ export type logProviderCallback = (txSig: TransactionSignature, slot: number, logs: string[], mostRecentBlockTime: number | undefined, txSigIndex: number | undefined) => void;
52
52
  export interface LogProvider {
53
53
  isSubscribed(): boolean;
54
54
  subscribe(callback: logProviderCallback, skipHistory?: boolean): Promise<boolean>;
55
55
  unsubscribe(external?: boolean): Promise<boolean>;
56
56
  eventEmitter?: EventEmitter;
57
57
  }
58
- export type WebSocketLogProviderConfig = {
59
- type: 'websocket';
60
- resubTimeoutMs?: number;
58
+ export type LogProviderType = 'websocket' | 'polling' | 'events-server';
59
+ export type StreamingLogProviderConfig = {
61
60
  maxReconnectAttempts?: number;
62
61
  fallbackFrequency?: number;
63
62
  fallbackBatchSize?: number;
64
63
  };
64
+ export type WebSocketLogProviderConfig = StreamingLogProviderConfig & {
65
+ type: 'websocket';
66
+ resubTimeoutMs?: number;
67
+ };
65
68
  export type PollingLogProviderConfig = {
66
69
  type: 'polling';
67
70
  frequency: number;
68
71
  batchSize?: number;
69
72
  };
70
- export type LogProviderConfig = WebSocketLogProviderConfig | PollingLogProviderConfig;
73
+ export type EventsServerLogProviderConfig = StreamingLogProviderConfig & {
74
+ type: 'events-server';
75
+ url: string;
76
+ };
77
+ export type LogProviderConfig = WebSocketLogProviderConfig | PollingLogProviderConfig | EventsServerLogProviderConfig;
@@ -25,6 +25,10 @@ exports.DefaultEventSubscriptionOptions = {
25
25
  commitment: 'confirmed',
26
26
  maxTx: 4096,
27
27
  logProviderConfig: {
28
- type: 'websocket',
28
+ type: 'events-server',
29
+ url: 'wss://events.drift.trade/ws',
30
+ maxReconnectAttempts: 5,
31
+ fallbackFrequency: 1000,
32
+ fallbackBatchSize: 100,
29
33
  },
30
34
  };
@@ -47,7 +47,7 @@ class WebSocketLogProvider {
47
47
  if (logs.err !== null) {
48
48
  return;
49
49
  }
50
- callback(logs.signature, ctx.slot, logs.logs, undefined);
50
+ callback(logs.signature, ctx.slot, logs.logs, undefined, undefined);
51
51
  }, this.commitment);
52
52
  }
53
53
  isSubscribed() {
@@ -83,7 +83,7 @@ class WebSocketLogProvider {
83
83
  return;
84
84
  }
85
85
  if (this.receivingData) {
86
- console.log(`No log data in ${this.resubTimeoutMs}ms, resubscribing on attempt ${this.reconnectAttempts + 1}`);
86
+ console.log(`webSocketLogProvider: No log data in ${this.resubTimeoutMs}ms, resubscribing on attempt ${this.reconnectAttempts + 1}`);
87
87
  await this.unsubscribe();
88
88
  this.receivingData = false;
89
89
  this.reconnectAttempts++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drift-labs/sdk",
3
- "version": "2.96.0-beta.1",
3
+ "version": "2.96.0-beta.2",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "author": "crispheaney",
@@ -9,6 +9,10 @@ import {
9
9
  LogProvider,
10
10
  EventSubscriberEvents,
11
11
  WebSocketLogProviderConfig,
12
+ PollingLogProviderConfig,
13
+ EventsServerLogProviderConfig,
14
+ LogProviderType,
15
+ StreamingLogProviderConfig,
12
16
  } from './types';
13
17
  import { TxEventCache } from './txEventCache';
14
18
  import { EventList } from './eventList';
@@ -19,6 +23,7 @@ import { EventEmitter } from 'events';
19
23
  import StrictEventEmitter from 'strict-event-emitter-types';
20
24
  import { getSortFn } from './sort';
21
25
  import { parseLogs } from './parse';
26
+ import { EventsServerLogProvider } from './eventsServerLogProvider';
22
27
 
23
28
  export class EventSubscriber {
24
29
  private address: PublicKey;
@@ -27,6 +32,7 @@ export class EventSubscriber {
27
32
  private awaitTxPromises = new Map<string, Promise<void>>();
28
33
  private awaitTxResolver = new Map<string, () => void>();
29
34
  private logProvider: LogProvider;
35
+ private currentProviderType: LogProviderType;
30
36
  public eventEmitter: StrictEventEmitter<EventEmitter, EventSubscriberEvents>;
31
37
  private lastSeenSlot: number;
32
38
  private lastSeenBlockTime: number | undefined;
@@ -43,22 +49,57 @@ export class EventSubscriber {
43
49
  this.eventListMap = new Map<EventType, EventList<EventType>>();
44
50
  this.eventEmitter = new EventEmitter();
45
51
 
46
- if (this.options.logProviderConfig.type === 'websocket') {
52
+ this.currentProviderType = this.options.logProviderConfig.type;
53
+ this.initializeLogProvider();
54
+ }
55
+
56
+ private initializeLogProvider(subscribe = false) {
57
+ if (this.currentProviderType === 'websocket') {
58
+ const logProviderConfig = this.options
59
+ .logProviderConfig as WebSocketLogProviderConfig;
47
60
  this.logProvider = new WebSocketLogProvider(
48
61
  // @ts-ignore
49
62
  this.connection,
50
63
  this.address,
51
64
  this.options.commitment,
52
- this.options.logProviderConfig.resubTimeoutMs
65
+ logProviderConfig.resubTimeoutMs
53
66
  );
54
- } else {
67
+ } else if (this.currentProviderType === 'polling') {
68
+ const logProviderConfig = this.options
69
+ .logProviderConfig as PollingLogProviderConfig;
55
70
  this.logProvider = new PollingLogProvider(
56
71
  // @ts-ignore
57
72
  this.connection,
58
73
  this.address,
59
- options.commitment,
60
- this.options.logProviderConfig.frequency,
61
- this.options.logProviderConfig.batchSize
74
+ this.options.commitment,
75
+ logProviderConfig.frequency,
76
+ logProviderConfig.batchSize
77
+ );
78
+ } else if (this.currentProviderType === 'events-server') {
79
+ const logProviderConfig = this.options
80
+ .logProviderConfig as EventsServerLogProviderConfig;
81
+ this.logProvider = new EventsServerLogProvider(
82
+ logProviderConfig.url,
83
+ this.options.eventTypes,
84
+ this.options.address ? this.options.address.toString() : undefined
85
+ );
86
+ } else {
87
+ throw new Error(`Invalid log provider type: ${this.currentProviderType}`);
88
+ }
89
+
90
+ if (subscribe) {
91
+ this.logProvider.subscribe(
92
+ (txSig, slot, logs, mostRecentBlockTime, txSigIndex) => {
93
+ this.handleTxLogs(
94
+ txSig,
95
+ slot,
96
+ logs,
97
+ mostRecentBlockTime,
98
+ this.currentProviderType === 'events-server',
99
+ txSigIndex
100
+ );
101
+ },
102
+ true
62
103
  );
63
104
  }
64
105
  }
@@ -77,6 +118,33 @@ export class EventSubscriber {
77
118
  }
78
119
  }
79
120
 
121
+ /**
122
+ * Implements fallback logic for reconnecting to LogProvider. Currently terminates at polling,
123
+ * could be improved to try the original type again after some cooldown.
124
+ */
125
+ private updateFallbackProviderType(
126
+ reconnectAttempts: number,
127
+ maxReconnectAttempts: number
128
+ ) {
129
+ if (reconnectAttempts < maxReconnectAttempts) {
130
+ return;
131
+ }
132
+
133
+ let nextProviderType = this.currentProviderType;
134
+ if (this.currentProviderType === 'events-server') {
135
+ nextProviderType = 'websocket';
136
+ } else if (this.currentProviderType === 'websocket') {
137
+ nextProviderType = 'polling';
138
+ } else if (this.currentProviderType === 'polling') {
139
+ nextProviderType = 'polling';
140
+ }
141
+
142
+ console.log(
143
+ `EventSubscriber: Failing over providerType ${this.currentProviderType} to ${nextProviderType}`
144
+ );
145
+ this.currentProviderType = nextProviderType;
146
+ }
147
+
80
148
  public async subscribe(): Promise<boolean> {
81
149
  try {
82
150
  if (this.logProvider.isSubscribed()) {
@@ -85,52 +153,46 @@ export class EventSubscriber {
85
153
 
86
154
  this.populateInitialEventListMap();
87
155
 
88
- if (this.options.logProviderConfig.type === 'websocket') {
89
- if (this.options.logProviderConfig.resubTimeoutMs) {
90
- if (
91
- this.options.logProviderConfig.maxReconnectAttempts &&
92
- this.options.logProviderConfig.maxReconnectAttempts > 0
93
- ) {
94
- const logProviderConfig = this.options
95
- .logProviderConfig as WebSocketLogProviderConfig;
96
- this.logProvider.eventEmitter.on(
97
- 'reconnect',
98
- (reconnectAttempts) => {
99
- if (
100
- reconnectAttempts > logProviderConfig.maxReconnectAttempts
101
- ) {
102
- console.log('Failing over to polling');
103
- this.logProvider.eventEmitter.removeAllListeners('reconnect');
104
- this.unsubscribe().then(() => {
105
- this.logProvider = new PollingLogProvider(
106
- // @ts-ignore
107
- this.connection,
108
- this.address,
109
- this.options.commitment,
110
- logProviderConfig.fallbackFrequency,
111
- logProviderConfig.fallbackBatchSize
112
- );
113
- this.logProvider.subscribe(
114
- (txSig, slot, logs, mostRecentBlockTime) => {
115
- this.handleTxLogs(
116
- txSig,
117
- slot,
118
- logs,
119
- mostRecentBlockTime
120
- );
121
- },
122
- true
123
- );
124
- });
125
- }
156
+ if (
157
+ this.options.logProviderConfig.type === 'websocket' ||
158
+ this.options.logProviderConfig.type === 'events-server'
159
+ ) {
160
+ const logProviderConfig = this.options
161
+ .logProviderConfig as StreamingLogProviderConfig;
162
+
163
+ if (this.logProvider.eventEmitter) {
164
+ this.logProvider.eventEmitter.on(
165
+ 'reconnect',
166
+ async (reconnectAttempts) => {
167
+ if (reconnectAttempts > logProviderConfig.maxReconnectAttempts) {
168
+ console.log(
169
+ `EventSubscriber: Reconnect attempts ${reconnectAttempts}/${logProviderConfig.maxReconnectAttempts}, reconnecting...`
170
+ );
171
+ this.logProvider.eventEmitter.removeAllListeners('reconnect');
172
+ await this.unsubscribe();
173
+ this.updateFallbackProviderType(
174
+ reconnectAttempts,
175
+ logProviderConfig.maxReconnectAttempts
176
+ );
177
+ this.initializeLogProvider(true);
126
178
  }
127
- );
128
- }
179
+ }
180
+ );
129
181
  }
130
182
  }
131
- this.logProvider.subscribe((txSig, slot, logs, mostRecentBlockTime) => {
132
- this.handleTxLogs(txSig, slot, logs, mostRecentBlockTime);
133
- }, true);
183
+ this.logProvider.subscribe(
184
+ (txSig, slot, logs, mostRecentBlockTime, txSigIndex) => {
185
+ this.handleTxLogs(
186
+ txSig,
187
+ slot,
188
+ logs,
189
+ mostRecentBlockTime,
190
+ this.currentProviderType === 'events-server',
191
+ txSigIndex
192
+ );
193
+ },
194
+ true
195
+ );
134
196
 
135
197
  return true;
136
198
  } catch (e) {
@@ -144,13 +206,20 @@ export class EventSubscriber {
144
206
  txSig: TransactionSignature,
145
207
  slot: number,
146
208
  logs: string[],
147
- mostRecentBlockTime: number | undefined
209
+ mostRecentBlockTime: number | undefined,
210
+ fromEventsServer = false,
211
+ txSigIndex: number | undefined = undefined
148
212
  ): void {
149
- if (this.txEventCache.has(txSig)) {
213
+ if (!fromEventsServer && this.txEventCache.has(txSig)) {
150
214
  return;
151
215
  }
152
216
 
153
- const wrappedEvents = this.parseEventsFromLogs(txSig, slot, logs);
217
+ const wrappedEvents = this.parseEventsFromLogs(
218
+ txSig,
219
+ slot,
220
+ logs,
221
+ txSigIndex
222
+ );
154
223
 
155
224
  for (const wrappedEvent of wrappedEvents) {
156
225
  this.eventListMap.get(wrappedEvent.eventType).insert(wrappedEvent);
@@ -225,7 +294,8 @@ export class EventSubscriber {
225
294
  private parseEventsFromLogs(
226
295
  txSig: TransactionSignature,
227
296
  slot: number,
228
- logs: string[]
297
+ logs: string[],
298
+ txSigIndex: number | undefined
229
299
  ): WrappedEvents {
230
300
  const records = [];
231
301
  // @ts-ignore
@@ -238,7 +308,8 @@ export class EventSubscriber {
238
308
  event.data.txSig = txSig;
239
309
  event.data.slot = slot;
240
310
  event.data.eventType = event.name;
241
- event.data.txSigIndex = runningEventIndex;
311
+ event.data.txSigIndex =
312
+ txSigIndex !== undefined ? txSigIndex : runningEventIndex;
242
313
  records.push(event.data);
243
314
  }
244
315
  runningEventIndex++;
@@ -0,0 +1,152 @@
1
+ // import WebSocket from 'ws';
2
+ import { logProviderCallback, EventType, LogProvider } from './types';
3
+ import { EventEmitter } from 'events';
4
+
5
+ // browser support
6
+ let WebSocketImpl: typeof WebSocket;
7
+ if (typeof window !== 'undefined' && window.WebSocket) {
8
+ WebSocketImpl = window.WebSocket;
9
+ } else {
10
+ WebSocketImpl = require('ws');
11
+ }
12
+
13
+ const EVENT_SERVER_HEARTBEAT_INTERVAL_MS = 5000;
14
+ const ALLOWED_MISSED_HEARTBEATS = 3;
15
+
16
+ export class EventsServerLogProvider implements LogProvider {
17
+ private ws?: WebSocket;
18
+ private callback?: logProviderCallback;
19
+ private isUnsubscribing = false;
20
+ private externalUnsubscribe = false;
21
+ private lastHeartbeat = 0;
22
+ private timeoutId?: NodeJS.Timeout;
23
+ private reconnectAttempts = 0;
24
+ eventEmitter?: EventEmitter;
25
+
26
+ public constructor(
27
+ private readonly url: string,
28
+ private readonly eventTypes: EventType[],
29
+ private readonly userAccount?: string
30
+ ) {
31
+ this.eventEmitter = new EventEmitter();
32
+ }
33
+
34
+ public isSubscribed(): boolean {
35
+ return this.ws !== undefined;
36
+ }
37
+
38
+ public async subscribe(callback: logProviderCallback): Promise<boolean> {
39
+ if (this.ws !== undefined) {
40
+ return true;
41
+ }
42
+ this.ws = new WebSocketImpl(this.url);
43
+
44
+ this.callback = callback;
45
+ this.ws.addEventListener('open', () => {
46
+ for (const channel of this.eventTypes) {
47
+ const subscribeMessage = {
48
+ type: 'subscribe',
49
+ channel: channel,
50
+ };
51
+ if (this.userAccount) {
52
+ subscribeMessage['user'] = this.userAccount;
53
+ }
54
+ this.ws.send(JSON.stringify(subscribeMessage));
55
+ }
56
+ this.reconnectAttempts = 0;
57
+ });
58
+
59
+ this.ws.addEventListener('message', (data) => {
60
+ try {
61
+ if (!this.isUnsubscribing) {
62
+ clearTimeout(this.timeoutId);
63
+ this.setTimeout();
64
+ if (this.reconnectAttempts > 0) {
65
+ console.log(
66
+ 'eventsServerLogProvider: Resetting reconnect attempts to 0'
67
+ );
68
+ }
69
+ this.reconnectAttempts = 0;
70
+ }
71
+
72
+ const parsedData = JSON.parse(data.data.toString());
73
+ if (parsedData.channel === 'heartbeat') {
74
+ this.lastHeartbeat = Date.now();
75
+ return;
76
+ }
77
+ if (parsedData.message !== undefined) {
78
+ return;
79
+ }
80
+ const event = JSON.parse(parsedData.data);
81
+ this.callback(
82
+ event.txSig,
83
+ event.slot,
84
+ [
85
+ 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]',
86
+ event.rawLog,
87
+ 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success',
88
+ ],
89
+ undefined,
90
+ event.txSigIndex
91
+ );
92
+ } catch (error) {
93
+ console.error('Error parsing message:', error);
94
+ }
95
+ });
96
+
97
+ this.ws.addEventListener('close', () => {
98
+ console.log('eventsServerLogProvider: WebSocket closed');
99
+ });
100
+
101
+ this.ws.addEventListener('error', (error) => {
102
+ console.error('eventsServerLogProvider: WebSocket error:', error);
103
+ });
104
+
105
+ this.setTimeout();
106
+
107
+ return true;
108
+ }
109
+
110
+ public async unsubscribe(external = false): Promise<boolean> {
111
+ this.isUnsubscribing = true;
112
+ this.externalUnsubscribe = external;
113
+ if (this.timeoutId) {
114
+ clearInterval(this.timeoutId);
115
+ this.timeoutId = undefined;
116
+ }
117
+
118
+ if (this.ws !== undefined) {
119
+ this.ws.close();
120
+ this.ws = undefined;
121
+ return true;
122
+ } else {
123
+ this.isUnsubscribing = false;
124
+ return true;
125
+ }
126
+ }
127
+
128
+ private setTimeout(): void {
129
+ this.timeoutId = setTimeout(async () => {
130
+ if (this.isUnsubscribing || this.externalUnsubscribe) {
131
+ // If we are in the process of unsubscribing, do not attempt to resubscribe
132
+ return;
133
+ }
134
+
135
+ const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeat;
136
+ if (
137
+ timeSinceLastHeartbeat >
138
+ EVENT_SERVER_HEARTBEAT_INTERVAL_MS * ALLOWED_MISSED_HEARTBEATS
139
+ ) {
140
+ console.log(
141
+ `eventServerLogProvider: No heartbeat in ${timeSinceLastHeartbeat}ms, resubscribing on attempt ${
142
+ this.reconnectAttempts + 1
143
+ }`
144
+ );
145
+ await this.unsubscribe();
146
+ this.reconnectAttempts++;
147
+ this.eventEmitter.emit('reconnect', this.reconnectAttempts);
148
+ this.subscribe(this.callback);
149
+ }
150
+ }, EVENT_SERVER_HEARTBEAT_INTERVAL_MS * 2);
151
+ }
152
+ }
@@ -60,7 +60,7 @@ export class PollingLogProvider implements LogProvider {
60
60
  const { mostRecentTx, transactionLogs } = response;
61
61
 
62
62
  for (const { txSig, slot, logs } of transactionLogs) {
63
- callback(txSig, slot, logs, response.mostRecentBlockTime);
63
+ callback(txSig, slot, logs, response.mostRecentBlockTime, undefined);
64
64
  }
65
65
 
66
66
  this.mostRecentSeenTx = mostRecentTx;
@@ -56,7 +56,11 @@ export const DefaultEventSubscriptionOptions: EventSubscriptionOptions = {
56
56
  commitment: 'confirmed',
57
57
  maxTx: 4096,
58
58
  logProviderConfig: {
59
- type: 'websocket',
59
+ type: 'events-server',
60
+ url: 'wss://events.drift.trade/ws',
61
+ maxReconnectAttempts: 5,
62
+ fallbackFrequency: 1000,
63
+ fallbackBatchSize: 100,
60
64
  },
61
65
  };
62
66
 
@@ -126,7 +130,8 @@ export type logProviderCallback = (
126
130
  txSig: TransactionSignature,
127
131
  slot: number,
128
132
  logs: string[],
129
- mostRecentBlockTime: number | undefined
133
+ mostRecentBlockTime: number | undefined,
134
+ txSigIndex: number | undefined
130
135
  ) => void;
131
136
 
132
137
  export interface LogProvider {
@@ -139,20 +144,38 @@ export interface LogProvider {
139
144
  eventEmitter?: EventEmitter;
140
145
  }
141
146
 
142
- export type WebSocketLogProviderConfig = {
143
- type: 'websocket';
144
- resubTimeoutMs?: number;
147
+ export type LogProviderType = 'websocket' | 'polling' | 'events-server';
148
+
149
+ export type StreamingLogProviderConfig = {
150
+ /// Max number of times to try reconnecting before failing over to fallback provider
145
151
  maxReconnectAttempts?: number;
152
+ /// used for PollingLogProviderConfig on fallback
146
153
  fallbackFrequency?: number;
154
+ /// used for PollingLogProviderConfig on fallback
147
155
  fallbackBatchSize?: number;
148
156
  };
149
157
 
158
+ export type WebSocketLogProviderConfig = StreamingLogProviderConfig & {
159
+ type: 'websocket';
160
+ /// Max time to wait before resubscribing
161
+ resubTimeoutMs?: number;
162
+ };
163
+
150
164
  export type PollingLogProviderConfig = {
151
165
  type: 'polling';
166
+ /// frequency to poll for new events
152
167
  frequency: number;
168
+ /// max number of events to fetch per poll
153
169
  batchSize?: number;
154
170
  };
155
171
 
172
+ export type EventsServerLogProviderConfig = StreamingLogProviderConfig & {
173
+ type: 'events-server';
174
+ /// url of the events server
175
+ url: string;
176
+ };
177
+
156
178
  export type LogProviderConfig =
157
179
  | WebSocketLogProviderConfig
158
- | PollingLogProviderConfig;
180
+ | PollingLogProviderConfig
181
+ | EventsServerLogProviderConfig;
@@ -64,7 +64,7 @@ export class WebSocketLogProvider implements LogProvider {
64
64
  if (logs.err !== null) {
65
65
  return;
66
66
  }
67
- callback(logs.signature, ctx.slot, logs.logs, undefined);
67
+ callback(logs.signature, ctx.slot, logs.logs, undefined, undefined);
68
68
  },
69
69
  this.commitment
70
70
  );
@@ -106,9 +106,9 @@ export class WebSocketLogProvider implements LogProvider {
106
106
 
107
107
  if (this.receivingData) {
108
108
  console.log(
109
- `No log data in ${this.resubTimeoutMs}ms, resubscribing on attempt ${
110
- this.reconnectAttempts + 1
111
- }`
109
+ `webSocketLogProvider: No log data in ${
110
+ this.resubTimeoutMs
111
+ }ms, resubscribing on attempt ${this.reconnectAttempts + 1}`
112
112
  );
113
113
  await this.unsubscribe();
114
114
  this.receivingData = false;