@drift-labs/sdk 2.145.0 → 2.146.0-alpha.13
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/.env +4 -0
- package/VERSION +1 -1
- package/lib/browser/accounts/grpcMultiUserAccountSubscriber.js +8 -1
- package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.d.ts +99 -7
- package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.js +435 -144
- package/lib/browser/adminClient.d.ts +5 -1
- package/lib/browser/adminClient.js +57 -23
- package/lib/browser/constants/numericConstants.d.ts +2 -0
- package/lib/browser/constants/numericConstants.js +5 -1
- package/lib/browser/constants/perpMarkets.js +0 -2
- package/lib/browser/decode/user.js +4 -0
- package/lib/browser/driftClient.d.ts +25 -10
- package/lib/browser/driftClient.js +238 -41
- package/lib/browser/driftClientConfig.d.ts +7 -2
- package/lib/browser/idl/drift.json +245 -22
- package/lib/browser/index.d.ts +4 -0
- package/lib/browser/index.js +9 -1
- package/lib/browser/marginCalculation.d.ts +86 -0
- package/lib/browser/marginCalculation.js +209 -0
- package/lib/browser/math/margin.d.ts +1 -1
- package/lib/browser/math/margin.js +8 -1
- package/lib/browser/math/position.d.ts +1 -0
- package/lib/browser/math/position.js +10 -2
- package/lib/browser/math/spotPosition.d.ts +1 -1
- package/lib/browser/math/spotPosition.js +3 -2
- package/lib/browser/math/superStake.d.ts +3 -2
- package/lib/browser/types.d.ts +13 -0
- package/lib/browser/types.js +12 -1
- package/lib/browser/user.d.ts +59 -11
- package/lib/browser/user.js +348 -43
- package/lib/node/accounts/grpcMultiUserAccountSubscriber.d.ts.map +1 -1
- package/lib/node/accounts/grpcMultiUserAccountSubscriber.js +8 -1
- package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts +99 -7
- package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts.map +1 -1
- package/lib/node/accounts/webSocketProgramAccountSubscriberV2.js +435 -144
- package/lib/node/adminClient.d.ts +5 -1
- package/lib/node/adminClient.d.ts.map +1 -1
- package/lib/node/adminClient.js +57 -23
- package/lib/node/constants/numericConstants.d.ts +2 -0
- package/lib/node/constants/numericConstants.d.ts.map +1 -1
- package/lib/node/constants/numericConstants.js +5 -1
- package/lib/node/constants/perpMarkets.d.ts.map +1 -1
- package/lib/node/constants/perpMarkets.js +0 -2
- package/lib/node/decode/user.d.ts.map +1 -1
- package/lib/node/decode/user.js +4 -0
- package/lib/node/driftClient.d.ts +25 -10
- package/lib/node/driftClient.d.ts.map +1 -1
- package/lib/node/driftClient.js +238 -41
- package/lib/node/driftClientConfig.d.ts +7 -2
- package/lib/node/driftClientConfig.d.ts.map +1 -1
- package/lib/node/idl/drift.json +245 -22
- package/lib/node/index.d.ts +4 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +9 -1
- package/lib/node/marginCalculation.d.ts +87 -0
- package/lib/node/marginCalculation.d.ts.map +1 -0
- package/lib/node/marginCalculation.js +209 -0
- package/lib/node/math/margin.d.ts +1 -1
- package/lib/node/math/margin.d.ts.map +1 -1
- package/lib/node/math/margin.js +8 -1
- package/lib/node/math/position.d.ts +1 -0
- package/lib/node/math/position.d.ts.map +1 -1
- package/lib/node/math/position.js +10 -2
- package/lib/node/math/spotPosition.d.ts +1 -1
- package/lib/node/math/spotPosition.d.ts.map +1 -1
- package/lib/node/math/spotPosition.js +3 -2
- package/lib/node/math/superStake.d.ts +3 -2
- package/lib/node/math/superStake.d.ts.map +1 -1
- package/lib/node/types.d.ts +13 -0
- package/lib/node/types.d.ts.map +1 -1
- package/lib/node/types.js +12 -1
- package/lib/node/user.d.ts +59 -11
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +348 -43
- package/package.json +1 -1
- package/scripts/deposit-isolated-positions.ts +110 -0
- package/scripts/single-grpc-client-test.ts +71 -21
- package/scripts/withdraw-isolated-positions.ts +174 -0
- package/src/accounts/grpcMultiUserAccountSubscriber.ts +8 -1
- package/src/accounts/webSocketProgramAccountSubscriberV2.ts +566 -167
- package/src/adminClient.ts +74 -25
- package/src/constants/numericConstants.ts +5 -0
- package/src/constants/perpMarkets.ts +0 -3
- package/src/decode/user.ts +7 -1
- package/src/driftClient.ts +465 -52
- package/src/driftClientConfig.ts +15 -8
- package/src/idl/drift.json +246 -23
- package/src/index.ts +4 -0
- package/src/margin/README.md +143 -0
- package/src/marginCalculation.ts +306 -0
- package/src/math/margin.ts +13 -1
- package/src/math/position.ts +12 -2
- package/src/math/spotPosition.ts +6 -2
- package/src/types.ts +16 -0
- package/src/user.ts +623 -81
- package/tests/amm/test.ts +1 -1
- package/tests/dlob/helpers.ts +6 -3
- package/tests/user/getMarginCalculation.ts +405 -0
- package/tests/user/test.ts +0 -7
|
@@ -7,17 +7,96 @@ import {
|
|
|
7
7
|
AccountInfoWithBase64EncodedData,
|
|
8
8
|
createSolanaClient,
|
|
9
9
|
isAddress,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Lamports,
|
|
11
|
+
Slot,
|
|
12
|
+
Address,
|
|
13
|
+
Commitment as GillCommitment,
|
|
12
14
|
} from 'gill';
|
|
13
15
|
import bs58 from 'bs58';
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
type ProgramAccountSubscriptionAsyncIterable = AsyncIterable<
|
|
18
|
+
Readonly<{
|
|
19
|
+
context: Readonly<{
|
|
20
|
+
slot: Slot;
|
|
21
|
+
}>;
|
|
22
|
+
value: Readonly<{
|
|
23
|
+
account: Readonly<{
|
|
24
|
+
executable: boolean;
|
|
25
|
+
lamports: Lamports;
|
|
26
|
+
owner: Address;
|
|
27
|
+
rentEpoch: bigint;
|
|
28
|
+
space: bigint;
|
|
29
|
+
}> &
|
|
30
|
+
Readonly<any>;
|
|
31
|
+
pubkey: Address;
|
|
32
|
+
}>;
|
|
33
|
+
}>
|
|
34
|
+
>;
|
|
35
|
+
/**
|
|
36
|
+
* WebSocketProgramAccountsSubscriberV2
|
|
37
|
+
*
|
|
38
|
+
* High-level overview
|
|
39
|
+
* - WebSocket-first subscriber for Solana program accounts that also layers in
|
|
40
|
+
* targeted polling to detect missed updates reliably.
|
|
41
|
+
* - Emits decoded account updates via the provided `onChange` callback.
|
|
42
|
+
* - Designed to focus extra work on the specific accounts the consumer cares
|
|
43
|
+
* about ("monitored accounts") while keeping baseline WS behavior for the
|
|
44
|
+
* full program subscription.
|
|
45
|
+
*
|
|
46
|
+
* Why polling if this is a WebSocket subscriber?
|
|
47
|
+
* - WS infra can stall, drop, or reorder notifications under network stress or
|
|
48
|
+
* provider hiccups. When that happens, critical account changes can be missed.
|
|
49
|
+
* - To mitigate this, the class accepts a set of accounts (provided via constructor) to monitor
|
|
50
|
+
* and uses light polling to verify whether a WS change was missed.
|
|
51
|
+
* - If polling detects a newer slot with different data than the last seen
|
|
52
|
+
* buffer, a centralized resubscription is triggered to restore a clean stream.
|
|
53
|
+
*
|
|
54
|
+
* Initial fetch (on subscribe)
|
|
55
|
+
* - On `subscribe()`, we first perform a single batched fetch of all monitored
|
|
56
|
+
* accounts ("initial monitor fetch").
|
|
57
|
+
* - Purpose: seed the internal `bufferAndSlotMap` and emit the latest state so
|
|
58
|
+
* consumers have up-to-date data immediately, even before WS events arrive.
|
|
59
|
+
* - This step does not decide resubscription; it only establishes ground truth.
|
|
60
|
+
*
|
|
61
|
+
* Continuous polling (only for monitored accounts)
|
|
62
|
+
* - After seeding, each monitored account is put into a monitoring cycle:
|
|
63
|
+
* 1) If no WS notification for an account is observed for `pollingIntervalMs`,
|
|
64
|
+
* we enqueue it for a batched fetch (buffered for a short window).
|
|
65
|
+
* 2) Once an account enters the "currently polling" set, a shared batch poll
|
|
66
|
+
* runs every `pollingIntervalMs` across all such accounts.
|
|
67
|
+
* 3) If WS notifications resume for an account, that account is removed from
|
|
68
|
+
* the polling set and returns to passive monitoring.
|
|
69
|
+
* - Polling compares the newly fetched buffer with the last stored buffer at a
|
|
70
|
+
* later slot. A difference indicates a missed update; we schedule a single
|
|
71
|
+
* resubscription (coalesced across accounts) to re-sync.
|
|
72
|
+
*
|
|
73
|
+
* Accounts the consumer cares about
|
|
74
|
+
* - Provide accounts up-front via the constructor `accountsToMonitor`, or add
|
|
75
|
+
* them dynamically with `addAccountToMonitor()` and remove with
|
|
76
|
+
* `removeAccountFromMonitor()`.
|
|
77
|
+
* - Only these accounts incur additional polling safeguards; other accounts are
|
|
78
|
+
* still processed from the WS stream normally.
|
|
79
|
+
*
|
|
80
|
+
* Resubscription strategy
|
|
81
|
+
* - Missed updates from any monitored account are coalesced and trigger a single
|
|
82
|
+
* resubscription after a short delay. This avoids rapid churn.
|
|
83
|
+
* - If `resubOpts.resubTimeoutMs` is set, an inactivity timer also performs a
|
|
84
|
+
* batch check of monitored accounts. If a missed update is found, the same
|
|
85
|
+
* centralized resubscription flow is used.
|
|
86
|
+
*
|
|
87
|
+
* Tuning knobs
|
|
88
|
+
* - `setPollingInterval(ms)`: adjust how often monitoring/polling runs
|
|
89
|
+
* (default 30s). Shorter = faster detection, higher RPC load.
|
|
90
|
+
* - Debounced immediate poll (~100ms): batches accounts added to polling right after inactivity.
|
|
91
|
+
* - Batch size for `getMultipleAccounts` is limited to 100, requests are chunked
|
|
92
|
+
* and processed concurrently.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
export class WebSocketProgramAccountsSubscriberV2<T>
|
|
16
96
|
implements ProgramAccountSubscriber<T>
|
|
17
97
|
{
|
|
18
98
|
subscriptionName: string;
|
|
19
99
|
accountDiscriminator: string;
|
|
20
|
-
bufferAndSlot?: BufferAndSlot;
|
|
21
100
|
bufferAndSlotMap: Map<string, BufferAndSlot> = new Map();
|
|
22
101
|
program: Program;
|
|
23
102
|
decodeBuffer: (accountName: string, ix: Buffer) => T;
|
|
@@ -28,7 +107,7 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
28
107
|
buffer: Buffer
|
|
29
108
|
) => void;
|
|
30
109
|
listenerId?: number;
|
|
31
|
-
resubOpts
|
|
110
|
+
resubOpts: ResubOpts;
|
|
32
111
|
isUnsubscribing = false;
|
|
33
112
|
timeoutId?: ReturnType<typeof setTimeout>;
|
|
34
113
|
options: { filters: MemcmpFilter[]; commitment?: Commitment };
|
|
@@ -51,6 +130,15 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
51
130
|
private accountsCurrentlyPolling: Set<string> = new Set(); // Track which accounts are being polled
|
|
52
131
|
private batchPollingTimeout?: ReturnType<typeof setTimeout>; // Single timeout for batch polling
|
|
53
132
|
|
|
133
|
+
// Debounced immediate poll to batch multiple additions within a short window
|
|
134
|
+
private debouncedImmediatePollTimeout?: ReturnType<typeof setTimeout>;
|
|
135
|
+
private debouncedImmediatePollMs: number = 100; // configurable short window
|
|
136
|
+
|
|
137
|
+
// Centralized resubscription handling
|
|
138
|
+
private missedChangeDetected = false; // Flag to track if any missed change was detected
|
|
139
|
+
private resubscriptionTimeout?: ReturnType<typeof setTimeout>; // Timeout for delayed resubscription
|
|
140
|
+
private accountsWithMissedUpdates: Set<string> = new Set(); // Track which accounts had missed updates
|
|
141
|
+
|
|
54
142
|
public constructor(
|
|
55
143
|
subscriptionName: string,
|
|
56
144
|
accountDiscriminator: string,
|
|
@@ -66,7 +154,11 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
66
154
|
this.accountDiscriminator = accountDiscriminator;
|
|
67
155
|
this.program = program;
|
|
68
156
|
this.decodeBuffer = decodeBufferFn;
|
|
69
|
-
this.resubOpts = resubOpts
|
|
157
|
+
this.resubOpts = resubOpts ?? {
|
|
158
|
+
resubTimeoutMs: 30000,
|
|
159
|
+
usePollingInsteadOfResub: true,
|
|
160
|
+
logResubMessages: false,
|
|
161
|
+
};
|
|
70
162
|
if (this.resubOpts?.resubTimeoutMs < 1000) {
|
|
71
163
|
console.log(
|
|
72
164
|
'resubTimeoutMs should be at least 1000ms to avoid spamming resub'
|
|
@@ -92,6 +184,44 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
92
184
|
this.rpcSubscriptions = rpcSubscriptions;
|
|
93
185
|
}
|
|
94
186
|
|
|
187
|
+
private async handleNotificationLoop(
|
|
188
|
+
notificationPromise: Promise<ProgramAccountSubscriptionAsyncIterable>
|
|
189
|
+
) {
|
|
190
|
+
try {
|
|
191
|
+
const subscriptionIterable = await notificationPromise;
|
|
192
|
+
for await (const notification of subscriptionIterable) {
|
|
193
|
+
try {
|
|
194
|
+
if (this.resubOpts?.resubTimeoutMs) {
|
|
195
|
+
this.receivingData = true;
|
|
196
|
+
clearTimeout(this.timeoutId);
|
|
197
|
+
this.handleRpcResponse(
|
|
198
|
+
notification.context,
|
|
199
|
+
notification.value.pubkey,
|
|
200
|
+
notification.value.account.data
|
|
201
|
+
);
|
|
202
|
+
this.setTimeout();
|
|
203
|
+
} else {
|
|
204
|
+
this.handleRpcResponse(
|
|
205
|
+
notification.context,
|
|
206
|
+
notification.value.pubkey,
|
|
207
|
+
notification.value.account.data
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(
|
|
212
|
+
`Error handling RPC response for pubkey ${notification.value.pubkey}:`,
|
|
213
|
+
error
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(
|
|
219
|
+
`[${this.subscriptionName}] Error in notification loop:`,
|
|
220
|
+
error
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
95
225
|
async subscribe(
|
|
96
226
|
onChange: (
|
|
97
227
|
accountId: PublicKey,
|
|
@@ -100,62 +230,91 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
100
230
|
buffer: Buffer
|
|
101
231
|
) => void
|
|
102
232
|
): Promise<void> {
|
|
233
|
+
/**
|
|
234
|
+
* Start the WebSocket subscription and initialize polling safeguards.
|
|
235
|
+
*
|
|
236
|
+
* Flow
|
|
237
|
+
* - Seeds all monitored accounts with a single batched RPC fetch and emits
|
|
238
|
+
* their current state.
|
|
239
|
+
* - Subscribes to program notifications via WS using gill.
|
|
240
|
+
* - If `resubOpts.resubTimeoutMs` is set, starts an inactivity timer that
|
|
241
|
+
* batch-checks monitored accounts when WS goes quiet.
|
|
242
|
+
* - Begins monitoring for accounts that may need polling when WS
|
|
243
|
+
* notifications are not observed within `pollingIntervalMs`.
|
|
244
|
+
*
|
|
245
|
+
* @param onChange Callback invoked with decoded account data when an update
|
|
246
|
+
* is detected (via WS or batch RPC fetch).
|
|
247
|
+
*/
|
|
248
|
+
const startTime = performance.now();
|
|
103
249
|
if (this.listenerId != null || this.isUnsubscribing) {
|
|
104
250
|
return;
|
|
105
251
|
}
|
|
106
252
|
|
|
253
|
+
if (this.resubOpts?.logResubMessages) {
|
|
254
|
+
console.log(
|
|
255
|
+
`[${this.subscriptionName}] initializing subscription. This many monitored accounts: ${this.accountsToMonitor.size}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
107
259
|
this.onChange = onChange;
|
|
108
260
|
|
|
261
|
+
// initial fetch of monitored data - only fetch and populate, don't check for missed changes
|
|
262
|
+
await this.fetchAndPopulateAllMonitoredAccounts();
|
|
263
|
+
|
|
109
264
|
// Create abort controller for proper cleanup
|
|
110
265
|
const abortController = new AbortController();
|
|
111
266
|
this.abortController = abortController;
|
|
112
267
|
|
|
268
|
+
this.listenerId = Math.random(); // Unique ID for logging purposes
|
|
269
|
+
|
|
270
|
+
if (this.resubOpts?.resubTimeoutMs) {
|
|
271
|
+
this.receivingData = true;
|
|
272
|
+
this.setTimeout();
|
|
273
|
+
}
|
|
274
|
+
|
|
113
275
|
// Subscribe to program account changes using gill's rpcSubscriptions
|
|
114
276
|
const programId = this.program.programId.toBase58();
|
|
115
277
|
if (isAddress(programId)) {
|
|
116
|
-
const
|
|
278
|
+
const subscriptionPromise = this.rpcSubscriptions
|
|
117
279
|
.programNotifications(programId, {
|
|
118
280
|
commitment: this.options.commitment as GillCommitment,
|
|
119
281
|
encoding: 'base64',
|
|
120
|
-
filters: this.options.filters.map((filter) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
282
|
+
filters: this.options.filters.map((filter) => {
|
|
283
|
+
// Convert filter bytes from base58 to base64 if needed
|
|
284
|
+
let bytes = filter.memcmp.bytes;
|
|
285
|
+
if (
|
|
286
|
+
typeof bytes === 'string' &&
|
|
287
|
+
/^[1-9A-HJ-NP-Za-km-z]+$/.test(bytes)
|
|
288
|
+
) {
|
|
289
|
+
// Looks like base58 - convert to base64
|
|
290
|
+
const decoded = bs58.decode(bytes);
|
|
291
|
+
bytes = Buffer.from(decoded).toString('base64');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
memcmp: {
|
|
296
|
+
offset: BigInt(filter.memcmp.offset),
|
|
297
|
+
bytes: bytes as any,
|
|
298
|
+
encoding: 'base64' as const,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}),
|
|
127
302
|
})
|
|
128
303
|
.subscribe({
|
|
129
304
|
abortSignal: abortController.signal,
|
|
130
305
|
});
|
|
131
306
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.handleRpcResponse(
|
|
137
|
-
notification.context,
|
|
138
|
-
notification.value.account
|
|
139
|
-
);
|
|
140
|
-
this.setTimeout();
|
|
141
|
-
} else {
|
|
142
|
-
this.handleRpcResponse(
|
|
143
|
-
notification.context,
|
|
144
|
-
notification.value.account
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.listenerId = Math.random(); // Unique ID for logging purposes
|
|
151
|
-
|
|
152
|
-
if (this.resubOpts?.resubTimeoutMs) {
|
|
153
|
-
this.receivingData = true;
|
|
154
|
-
this.setTimeout();
|
|
307
|
+
// Start notification loop without awaiting
|
|
308
|
+
this.handleNotificationLoop(subscriptionPromise);
|
|
309
|
+
// Start monitoring for accounts that may need polling if no WS event is received
|
|
310
|
+
this.startMonitoringForAccounts();
|
|
155
311
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
312
|
+
const endTime = performance.now();
|
|
313
|
+
console.log(
|
|
314
|
+
`[PROFILING] ${this.subscriptionName}.subscribe() completed in ${
|
|
315
|
+
endTime - startTime
|
|
316
|
+
}ms`
|
|
317
|
+
);
|
|
159
318
|
}
|
|
160
319
|
|
|
161
320
|
protected setTimeout(): void {
|
|
@@ -172,12 +331,21 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
172
331
|
if (this.receivingData) {
|
|
173
332
|
if (this.resubOpts?.logResubMessages) {
|
|
174
333
|
console.log(
|
|
175
|
-
`No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms,
|
|
334
|
+
`No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, checking for missed changes`
|
|
176
335
|
);
|
|
177
336
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await this.
|
|
337
|
+
|
|
338
|
+
// Check for missed changes in monitored accounts
|
|
339
|
+
const missedChangeDetected = await this.fetchAllMonitoredAccounts();
|
|
340
|
+
|
|
341
|
+
if (missedChangeDetected) {
|
|
342
|
+
// Signal missed change with a generic identifier since we don't have specific account IDs from this context
|
|
343
|
+
this.signalMissedChange('timeout-check');
|
|
344
|
+
} else {
|
|
345
|
+
// No missed changes, continue monitoring
|
|
346
|
+
this.receivingData = false;
|
|
347
|
+
this.setTimeout();
|
|
348
|
+
}
|
|
181
349
|
}
|
|
182
350
|
},
|
|
183
351
|
this.resubOpts?.resubTimeoutMs
|
|
@@ -186,43 +354,42 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
186
354
|
|
|
187
355
|
handleRpcResponse(
|
|
188
356
|
context: { slot: bigint },
|
|
357
|
+
accountId: Address,
|
|
189
358
|
accountInfo?: AccountInfoBase &
|
|
190
|
-
(
|
|
359
|
+
(
|
|
360
|
+
| AccountInfoWithBase58EncodedData
|
|
361
|
+
| AccountInfoWithBase64EncodedData
|
|
362
|
+
)['data']
|
|
191
363
|
): void {
|
|
192
364
|
const newSlot = Number(context.slot);
|
|
193
365
|
let newBuffer: Buffer | undefined = undefined;
|
|
194
366
|
|
|
195
367
|
if (accountInfo) {
|
|
196
|
-
//
|
|
197
|
-
if (accountInfo
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
} else {
|
|
207
|
-
newBuffer = Buffer.from(data, 'base64');
|
|
208
|
-
}
|
|
368
|
+
// Handle different data formats from gill
|
|
369
|
+
if (Array.isArray(accountInfo)) {
|
|
370
|
+
// If it's a tuple [data, encoding]
|
|
371
|
+
const [data, encoding] = accountInfo;
|
|
372
|
+
|
|
373
|
+
if (encoding === ('base58' as any)) {
|
|
374
|
+
// Convert base58 to buffer using bs58
|
|
375
|
+
newBuffer = Buffer.from(bs58.decode(data));
|
|
376
|
+
} else {
|
|
377
|
+
newBuffer = Buffer.from(data, 'base64');
|
|
209
378
|
}
|
|
210
379
|
}
|
|
211
380
|
}
|
|
212
381
|
|
|
213
|
-
|
|
214
|
-
// Note: accountInfo doesn't have a key property, we need to get it from the notification
|
|
215
|
-
// For now, we'll use a placeholder - this needs to be fixed based on the actual gill API
|
|
216
|
-
const accountId = new PublicKey('11111111111111111111111111111111'); // Placeholder
|
|
217
|
-
const accountIdString = accountId.toBase58();
|
|
218
|
-
|
|
382
|
+
const accountIdString = accountId.toString();
|
|
219
383
|
const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
|
|
220
384
|
|
|
221
385
|
// Track WebSocket notification time for this account
|
|
222
386
|
this.lastWsNotificationTime.set(accountIdString, Date.now());
|
|
223
387
|
|
|
224
|
-
// If this account was being polled, stop polling it
|
|
225
|
-
if (
|
|
388
|
+
// If this account was being polled, stop polling it if the buffer has changed
|
|
389
|
+
if (
|
|
390
|
+
this.accountsCurrentlyPolling.has(accountIdString) &&
|
|
391
|
+
!existingBufferAndSlot?.buffer.equals(newBuffer)
|
|
392
|
+
) {
|
|
226
393
|
this.accountsCurrentlyPolling.delete(accountIdString);
|
|
227
394
|
|
|
228
395
|
// If no more accounts are being polled, stop batch polling
|
|
@@ -237,12 +404,7 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
237
404
|
|
|
238
405
|
if (!existingBufferAndSlot) {
|
|
239
406
|
if (newBuffer) {
|
|
240
|
-
this.
|
|
241
|
-
buffer: newBuffer,
|
|
242
|
-
slot: newSlot,
|
|
243
|
-
});
|
|
244
|
-
const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
|
|
245
|
-
this.onChange(accountId, account, { slot: newSlot }, newBuffer);
|
|
407
|
+
this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
|
|
246
408
|
}
|
|
247
409
|
return;
|
|
248
410
|
}
|
|
@@ -253,12 +415,7 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
253
415
|
|
|
254
416
|
const oldBuffer = existingBufferAndSlot.buffer;
|
|
255
417
|
if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) {
|
|
256
|
-
this.
|
|
257
|
-
buffer: newBuffer,
|
|
258
|
-
slot: newSlot,
|
|
259
|
-
});
|
|
260
|
-
const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
|
|
261
|
-
this.onChange(accountId, account, { slot: newSlot }, newBuffer);
|
|
418
|
+
this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
|
|
262
419
|
}
|
|
263
420
|
}
|
|
264
421
|
|
|
@@ -283,17 +440,21 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
283
440
|
const timeoutId = setTimeout(async () => {
|
|
284
441
|
// Check if we've received a WS notification for this account recently
|
|
285
442
|
const lastNotificationTime =
|
|
286
|
-
this.lastWsNotificationTime.get(accountIdString);
|
|
443
|
+
this.lastWsNotificationTime.get(accountIdString) || 0;
|
|
287
444
|
const currentTime = Date.now();
|
|
288
445
|
|
|
289
446
|
if (
|
|
290
447
|
!lastNotificationTime ||
|
|
291
448
|
currentTime - lastNotificationTime >= this.pollingIntervalMs
|
|
292
449
|
) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
450
|
+
if (this.resubOpts?.logResubMessages) {
|
|
451
|
+
console.debug(
|
|
452
|
+
`[${this.subscriptionName}] No recent WS notification for ${accountIdString}, adding to polling set`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
// No recent WS notification: add to polling and schedule debounced poll
|
|
456
|
+
this.accountsCurrentlyPolling.add(accountIdString);
|
|
457
|
+
this.scheduleDebouncedImmediatePoll();
|
|
297
458
|
} else {
|
|
298
459
|
// We received a WS notification recently, continue monitoring
|
|
299
460
|
this.startMonitoringForAccount(accountIdString);
|
|
@@ -303,17 +464,35 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
303
464
|
this.pollingTimeouts.set(accountIdString, timeoutId);
|
|
304
465
|
}
|
|
305
466
|
|
|
306
|
-
private
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// If this is the first account being polled, start batch polling
|
|
311
|
-
if (this.accountsCurrentlyPolling.size === 1) {
|
|
312
|
-
this.startBatchPolling();
|
|
467
|
+
private scheduleDebouncedImmediatePoll(): void {
|
|
468
|
+
if (this.debouncedImmediatePollTimeout) {
|
|
469
|
+
clearTimeout(this.debouncedImmediatePollTimeout);
|
|
313
470
|
}
|
|
471
|
+
this.debouncedImmediatePollTimeout = setTimeout(async () => {
|
|
472
|
+
try {
|
|
473
|
+
await this.pollAllAccounts();
|
|
474
|
+
// After the immediate poll, ensure continuous batch polling is active
|
|
475
|
+
if (
|
|
476
|
+
!this.batchPollingTimeout &&
|
|
477
|
+
this.accountsCurrentlyPolling.size > 0
|
|
478
|
+
) {
|
|
479
|
+
this.startBatchPolling();
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
if (this.resubOpts?.logResubMessages) {
|
|
483
|
+
console.log(
|
|
484
|
+
`[${this.subscriptionName}] Error during debounced immediate poll:`,
|
|
485
|
+
e
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}, this.debouncedImmediatePollMs);
|
|
314
490
|
}
|
|
315
491
|
|
|
316
492
|
private startBatchPolling(): void {
|
|
493
|
+
if (this.resubOpts?.logResubMessages) {
|
|
494
|
+
console.debug(`[${this.subscriptionName}] Scheduling batch polling`);
|
|
495
|
+
}
|
|
317
496
|
// Clear existing batch polling timeout
|
|
318
497
|
if (this.batchPollingTimeout) {
|
|
319
498
|
clearTimeout(this.batchPollingTimeout);
|
|
@@ -335,8 +514,40 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
335
514
|
return;
|
|
336
515
|
}
|
|
337
516
|
|
|
517
|
+
if (this.resubOpts?.logResubMessages) {
|
|
518
|
+
console.debug(
|
|
519
|
+
`[${this.subscriptionName}] Polling all accounts`,
|
|
520
|
+
accountsToPoll.length,
|
|
521
|
+
'accounts'
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Use the shared batch fetch method
|
|
526
|
+
await this.fetchAccountsBatch(accountsToPoll);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (this.resubOpts?.logResubMessages) {
|
|
529
|
+
console.log(
|
|
530
|
+
`[${this.subscriptionName}] Error batch polling accounts:`,
|
|
531
|
+
error
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Fetches and populates all monitored accounts data without checking for missed changes
|
|
539
|
+
* This is used during initial subscription to populate data
|
|
540
|
+
*/
|
|
541
|
+
private async fetchAndPopulateAllMonitoredAccounts(): Promise<void> {
|
|
542
|
+
try {
|
|
543
|
+
// Get all accounts currently being polled
|
|
544
|
+
const accountsToMonitor = Array.from(this.accountsToMonitor);
|
|
545
|
+
if (accountsToMonitor.length === 0) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
338
549
|
// Fetch all accounts in a single batch request
|
|
339
|
-
const accountAddresses =
|
|
550
|
+
const accountAddresses = accountsToMonitor.map(
|
|
340
551
|
(accountId) => accountId as Address
|
|
341
552
|
);
|
|
342
553
|
const rpcResponse = await this.rpc
|
|
@@ -349,8 +560,8 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
349
560
|
const currentSlot = Number(rpcResponse.context.slot);
|
|
350
561
|
|
|
351
562
|
// Process each account response
|
|
352
|
-
for (let i = 0; i <
|
|
353
|
-
const accountIdString =
|
|
563
|
+
for (let i = 0; i < accountsToMonitor.length; i++) {
|
|
564
|
+
const accountIdString = accountsToMonitor[i];
|
|
354
565
|
const accountInfo = rpcResponse.value[i];
|
|
355
566
|
|
|
356
567
|
if (!accountInfo) {
|
|
@@ -363,7 +574,7 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
363
574
|
if (!existingBufferAndSlot) {
|
|
364
575
|
// Account not in our map yet, add it
|
|
365
576
|
let newBuffer: Buffer | undefined = undefined;
|
|
366
|
-
if (accountInfo
|
|
577
|
+
if (accountInfo) {
|
|
367
578
|
if (Array.isArray(accountInfo.data)) {
|
|
368
579
|
const [data, encoding] = accountInfo.data;
|
|
369
580
|
newBuffer = Buffer.from(data, encoding);
|
|
@@ -371,21 +582,16 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
371
582
|
}
|
|
372
583
|
|
|
373
584
|
if (newBuffer) {
|
|
374
|
-
this.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const account = this.decodeBuffer(
|
|
379
|
-
this.accountDiscriminator,
|
|
380
|
-
newBuffer
|
|
585
|
+
this.updateBufferAndHandleChange(
|
|
586
|
+
newBuffer,
|
|
587
|
+
currentSlot,
|
|
588
|
+
accountIdString
|
|
381
589
|
);
|
|
382
|
-
const accountId = new PublicKey(accountIdString);
|
|
383
|
-
this.onChange(accountId, account, { slot: currentSlot }, newBuffer);
|
|
384
590
|
}
|
|
385
591
|
continue;
|
|
386
592
|
}
|
|
387
593
|
|
|
388
|
-
//
|
|
594
|
+
// For initial population, just update the slot if we have newer data
|
|
389
595
|
if (currentSlot > existingBufferAndSlot.slot) {
|
|
390
596
|
let newBuffer: Buffer | undefined = undefined;
|
|
391
597
|
if (accountInfo.data) {
|
|
@@ -399,83 +605,89 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
399
605
|
}
|
|
400
606
|
}
|
|
401
607
|
|
|
402
|
-
//
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
console.log(
|
|
410
|
-
`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
// We missed an update, resubscribe
|
|
414
|
-
await this.unsubscribe(true);
|
|
415
|
-
this.receivingData = false;
|
|
416
|
-
await this.subscribe(this.onChange);
|
|
417
|
-
return;
|
|
608
|
+
// Update with newer data if available
|
|
609
|
+
if (newBuffer) {
|
|
610
|
+
this.updateBufferAndHandleChange(
|
|
611
|
+
newBuffer,
|
|
612
|
+
currentSlot,
|
|
613
|
+
accountIdString
|
|
614
|
+
);
|
|
418
615
|
}
|
|
419
616
|
}
|
|
420
617
|
}
|
|
421
618
|
} catch (error) {
|
|
422
619
|
if (this.resubOpts?.logResubMessages) {
|
|
423
620
|
console.log(
|
|
424
|
-
`[${this.subscriptionName}] Error
|
|
621
|
+
`[${this.subscriptionName}] Error fetching and populating monitored accounts:`,
|
|
425
622
|
error
|
|
426
623
|
);
|
|
427
624
|
}
|
|
428
625
|
}
|
|
429
626
|
}
|
|
430
627
|
|
|
431
|
-
|
|
628
|
+
/**
|
|
629
|
+
* Fetches all monitored accounts and checks for missed changes
|
|
630
|
+
* Returns true if a missed change was detected and resubscription is needed
|
|
631
|
+
*/
|
|
632
|
+
private async fetchAllMonitoredAccounts(): Promise<boolean> {
|
|
432
633
|
try {
|
|
433
|
-
//
|
|
434
|
-
const
|
|
634
|
+
// Get all accounts currently being polled
|
|
635
|
+
const accountsToMonitor = Array.from(this.accountsToMonitor);
|
|
636
|
+
if (accountsToMonitor.length === 0) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Fetch all accounts in a single batch request
|
|
641
|
+
const accountAddresses = accountsToMonitor.map(
|
|
642
|
+
(accountId) => accountId as Address
|
|
643
|
+
);
|
|
435
644
|
const rpcResponse = await this.rpc
|
|
436
|
-
.
|
|
645
|
+
.getMultipleAccounts(accountAddresses, {
|
|
437
646
|
commitment: this.options.commitment as GillCommitment,
|
|
438
647
|
encoding: 'base64',
|
|
439
648
|
})
|
|
440
649
|
.send();
|
|
441
650
|
|
|
442
651
|
const currentSlot = Number(rpcResponse.context.slot);
|
|
443
|
-
const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
|
|
444
652
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
653
|
+
// Process each account response
|
|
654
|
+
for (let i = 0; i < accountsToMonitor.length; i++) {
|
|
655
|
+
const accountIdString = accountsToMonitor[i];
|
|
656
|
+
const accountInfo = rpcResponse.value[i];
|
|
657
|
+
|
|
658
|
+
if (!accountInfo) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const existingBufferAndSlot =
|
|
663
|
+
this.bufferAndSlotMap.get(accountIdString);
|
|
664
|
+
|
|
665
|
+
if (!existingBufferAndSlot) {
|
|
666
|
+
// Account not in our map yet, add it
|
|
448
667
|
let newBuffer: Buffer | undefined = undefined;
|
|
449
|
-
if (
|
|
450
|
-
if (Array.isArray(
|
|
451
|
-
const [data, encoding] =
|
|
668
|
+
if (accountInfo.data) {
|
|
669
|
+
if (Array.isArray(accountInfo.data)) {
|
|
670
|
+
const [data, encoding] = accountInfo.data;
|
|
452
671
|
newBuffer = Buffer.from(data, encoding);
|
|
453
672
|
}
|
|
454
673
|
}
|
|
455
674
|
|
|
456
675
|
if (newBuffer) {
|
|
457
|
-
this.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const account = this.decodeBuffer(
|
|
462
|
-
this.accountDiscriminator,
|
|
463
|
-
newBuffer
|
|
676
|
+
this.updateBufferAndHandleChange(
|
|
677
|
+
newBuffer,
|
|
678
|
+
currentSlot,
|
|
679
|
+
accountIdString
|
|
464
680
|
);
|
|
465
|
-
const accountId = new PublicKey(accountIdString);
|
|
466
|
-
this.onChange(accountId, account, { slot: currentSlot }, newBuffer);
|
|
467
681
|
}
|
|
682
|
+
continue;
|
|
468
683
|
}
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
684
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const [data, encoding] = rpcResponse.value.data;
|
|
685
|
+
// Check if we missed an update
|
|
686
|
+
if (currentSlot > existingBufferAndSlot.slot) {
|
|
687
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
688
|
+
if (accountInfo.data) {
|
|
689
|
+
if (Array.isArray(accountInfo.data)) {
|
|
690
|
+
const [data, encoding] = accountInfo.data;
|
|
479
691
|
if (encoding === ('base58' as any)) {
|
|
480
692
|
newBuffer = Buffer.from(bs58.decode(data));
|
|
481
693
|
} else {
|
|
@@ -483,30 +695,130 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
483
695
|
}
|
|
484
696
|
}
|
|
485
697
|
}
|
|
486
|
-
}
|
|
487
698
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
699
|
+
// Check if buffer has changed
|
|
700
|
+
if (
|
|
701
|
+
newBuffer &&
|
|
702
|
+
(!existingBufferAndSlot.buffer ||
|
|
703
|
+
!newBuffer.equals(existingBufferAndSlot.buffer))
|
|
704
|
+
) {
|
|
705
|
+
if (this.resubOpts?.logResubMessages) {
|
|
706
|
+
console.log(
|
|
707
|
+
`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
// We missed an update, return true to indicate resubscription is needed
|
|
711
|
+
return true;
|
|
498
712
|
}
|
|
499
|
-
// We missed an update, resubscribe
|
|
500
|
-
await this.unsubscribe(true);
|
|
501
|
-
this.receivingData = false;
|
|
502
|
-
await this.subscribe(this.onChange);
|
|
503
|
-
return;
|
|
504
713
|
}
|
|
505
714
|
}
|
|
715
|
+
|
|
716
|
+
// No missed changes detected
|
|
717
|
+
return false;
|
|
718
|
+
} catch (error) {
|
|
719
|
+
if (this.resubOpts?.logResubMessages) {
|
|
720
|
+
console.log(
|
|
721
|
+
`[${this.subscriptionName}] Error batch polling accounts:`,
|
|
722
|
+
error
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private async fetchAccountsBatch(accountIds: string[]): Promise<void> {
|
|
730
|
+
try {
|
|
731
|
+
// Chunk account IDs into groups of 100 (getMultipleAccounts limit)
|
|
732
|
+
const chunkSize = 100;
|
|
733
|
+
const chunks: string[][] = [];
|
|
734
|
+
for (let i = 0; i < accountIds.length; i += chunkSize) {
|
|
735
|
+
chunks.push(accountIds.slice(i, i + chunkSize));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Process all chunks concurrently
|
|
739
|
+
await Promise.all(
|
|
740
|
+
chunks.map(async (chunk) => {
|
|
741
|
+
const accountAddresses = chunk.map(
|
|
742
|
+
(accountId) => accountId as Address
|
|
743
|
+
);
|
|
744
|
+
const rpcResponse = await this.rpc
|
|
745
|
+
.getMultipleAccounts(accountAddresses, {
|
|
746
|
+
commitment: this.options.commitment as GillCommitment,
|
|
747
|
+
encoding: 'base64',
|
|
748
|
+
})
|
|
749
|
+
.send();
|
|
750
|
+
|
|
751
|
+
const currentSlot = Number(rpcResponse.context.slot);
|
|
752
|
+
|
|
753
|
+
// Process each account response in this chunk
|
|
754
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
755
|
+
const accountIdString = chunk[i];
|
|
756
|
+
const accountInfo = rpcResponse.value[i];
|
|
757
|
+
|
|
758
|
+
if (!accountInfo) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const existingBufferAndSlot =
|
|
763
|
+
this.bufferAndSlotMap.get(accountIdString);
|
|
764
|
+
|
|
765
|
+
if (!existingBufferAndSlot) {
|
|
766
|
+
// Account not in our map yet, add it
|
|
767
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
768
|
+
if (accountInfo.data) {
|
|
769
|
+
if (Array.isArray(accountInfo.data)) {
|
|
770
|
+
const [data, encoding] = accountInfo.data;
|
|
771
|
+
newBuffer = Buffer.from(data, encoding);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (newBuffer) {
|
|
776
|
+
this.updateBufferAndHandleChange(
|
|
777
|
+
newBuffer,
|
|
778
|
+
currentSlot,
|
|
779
|
+
accountIdString
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Check if we missed an update
|
|
786
|
+
if (currentSlot > existingBufferAndSlot.slot) {
|
|
787
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
788
|
+
if (accountInfo.data) {
|
|
789
|
+
if (Array.isArray(accountInfo.data)) {
|
|
790
|
+
const [data, encoding] = accountInfo.data;
|
|
791
|
+
if (encoding === ('base58' as any)) {
|
|
792
|
+
newBuffer = Buffer.from(bs58.decode(data));
|
|
793
|
+
} else {
|
|
794
|
+
newBuffer = Buffer.from(data, 'base64');
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Check if buffer has changed
|
|
800
|
+
if (
|
|
801
|
+
newBuffer &&
|
|
802
|
+
(!existingBufferAndSlot.buffer ||
|
|
803
|
+
!newBuffer.equals(existingBufferAndSlot.buffer))
|
|
804
|
+
) {
|
|
805
|
+
if (this.resubOpts?.logResubMessages) {
|
|
806
|
+
console.log(
|
|
807
|
+
`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, signaling resubscription`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
// Signal missed change instead of immediately resubscribing
|
|
811
|
+
this.signalMissedChange(accountIdString);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
);
|
|
506
818
|
} catch (error) {
|
|
507
819
|
if (this.resubOpts?.logResubMessages) {
|
|
508
820
|
console.log(
|
|
509
|
-
`[${this.subscriptionName}] Error
|
|
821
|
+
`[${this.subscriptionName}] Error fetching accounts batch:`,
|
|
510
822
|
error
|
|
511
823
|
);
|
|
512
824
|
}
|
|
@@ -525,8 +837,72 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
525
837
|
this.batchPollingTimeout = undefined;
|
|
526
838
|
}
|
|
527
839
|
|
|
840
|
+
// Clear initial fetch timeout
|
|
841
|
+
// if (this.initialFetchTimeout) {
|
|
842
|
+
// clearTimeout(this.initialFetchTimeout);
|
|
843
|
+
// this.initialFetchTimeout = undefined;
|
|
844
|
+
// }
|
|
845
|
+
|
|
846
|
+
// Clear resubscription timeout
|
|
847
|
+
if (this.resubscriptionTimeout) {
|
|
848
|
+
clearTimeout(this.resubscriptionTimeout);
|
|
849
|
+
this.resubscriptionTimeout = undefined;
|
|
850
|
+
}
|
|
851
|
+
|
|
528
852
|
// Clear accounts currently polling
|
|
529
853
|
this.accountsCurrentlyPolling.clear();
|
|
854
|
+
|
|
855
|
+
// Clear accounts pending initial monitor fetch
|
|
856
|
+
// this.accountsPendingInitialMonitorFetch.clear();
|
|
857
|
+
|
|
858
|
+
// Reset missed change flag and clear accounts with missed updates
|
|
859
|
+
this.missedChangeDetected = false;
|
|
860
|
+
this.accountsWithMissedUpdates.clear();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Centralized resubscription handler that only resubscribes once after checking all accounts
|
|
865
|
+
*/
|
|
866
|
+
private async handleResubscription(): Promise<void> {
|
|
867
|
+
if (this.missedChangeDetected) {
|
|
868
|
+
if (this.resubOpts?.logResubMessages) {
|
|
869
|
+
console.log(
|
|
870
|
+
`[${this.subscriptionName}] Missed change detected for ${
|
|
871
|
+
this.accountsWithMissedUpdates.size
|
|
872
|
+
} accounts: ${Array.from(this.accountsWithMissedUpdates).join(
|
|
873
|
+
', '
|
|
874
|
+
)}, resubscribing`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
await this.unsubscribe(true);
|
|
878
|
+
this.receivingData = false;
|
|
879
|
+
await this.subscribe(this.onChange);
|
|
880
|
+
this.missedChangeDetected = false;
|
|
881
|
+
this.accountsWithMissedUpdates.clear();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Signal that a missed change was detected and schedule resubscription
|
|
887
|
+
*/
|
|
888
|
+
private signalMissedChange(accountIdString: string): void {
|
|
889
|
+
if (!this.missedChangeDetected) {
|
|
890
|
+
this.missedChangeDetected = true;
|
|
891
|
+
this.accountsWithMissedUpdates.add(accountIdString);
|
|
892
|
+
|
|
893
|
+
// Clear any existing resubscription timeout
|
|
894
|
+
if (this.resubscriptionTimeout) {
|
|
895
|
+
clearTimeout(this.resubscriptionTimeout);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Schedule resubscription after a short delay to allow for batch processing
|
|
899
|
+
this.resubscriptionTimeout = setTimeout(async () => {
|
|
900
|
+
await this.handleResubscription();
|
|
901
|
+
}, 100); // 100ms delay to allow for batch processing
|
|
902
|
+
} else {
|
|
903
|
+
// If already detected, just add the account to the set
|
|
904
|
+
this.accountsWithMissedUpdates.add(accountIdString);
|
|
905
|
+
}
|
|
530
906
|
}
|
|
531
907
|
|
|
532
908
|
unsubscribe(onResub = false): Promise<void> {
|
|
@@ -553,6 +929,11 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
553
929
|
}
|
|
554
930
|
|
|
555
931
|
// Method to add accounts to the polling list
|
|
932
|
+
/**
|
|
933
|
+
* Add an account to the monitored set.
|
|
934
|
+
* - Monitored accounts are subject to initial fetch and periodic batch polls
|
|
935
|
+
* if WS notifications are not observed within `pollingIntervalMs`.
|
|
936
|
+
*/
|
|
556
937
|
addAccountToMonitor(accountId: PublicKey): void {
|
|
557
938
|
const accountIdString = accountId.toBase58();
|
|
558
939
|
this.accountsToMonitor.add(accountIdString);
|
|
@@ -586,6 +967,10 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
586
967
|
}
|
|
587
968
|
|
|
588
969
|
// Method to set polling interval
|
|
970
|
+
/**
|
|
971
|
+
* Set the monitoring/polling interval for monitored accounts.
|
|
972
|
+
* Shorter intervals detect missed updates sooner but increase RPC load.
|
|
973
|
+
*/
|
|
589
974
|
setPollingInterval(intervalMs: number): void {
|
|
590
975
|
this.pollingIntervalMs = intervalMs;
|
|
591
976
|
// Restart monitoring with new interval if already subscribed
|
|
@@ -593,4 +978,18 @@ export class WebSocketProgramAccountSubscriberV2<T>
|
|
|
593
978
|
this.startMonitoringForAccounts();
|
|
594
979
|
}
|
|
595
980
|
}
|
|
981
|
+
|
|
982
|
+
private updateBufferAndHandleChange(
|
|
983
|
+
newBuffer: Buffer,
|
|
984
|
+
newSlot: number,
|
|
985
|
+
accountIdString: string
|
|
986
|
+
) {
|
|
987
|
+
this.bufferAndSlotMap.set(accountIdString, {
|
|
988
|
+
buffer: newBuffer,
|
|
989
|
+
slot: newSlot,
|
|
990
|
+
});
|
|
991
|
+
const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
|
|
992
|
+
const accountIdPubkey = new PublicKey(accountIdString);
|
|
993
|
+
this.onChange(accountIdPubkey, account, { slot: newSlot }, newBuffer);
|
|
994
|
+
}
|
|
596
995
|
}
|