@velocity-exchange/sdk 0.4.0 → 0.6.1

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.
Files changed (325) hide show
  1. package/README.md +3 -3
  2. package/lib/browser/adminClient.d.ts +52 -0
  3. package/lib/browser/adminClient.js +97 -0
  4. package/lib/browser/config.js +6 -7
  5. package/lib/browser/constants/numericConstants.d.ts +1 -1
  6. package/lib/browser/constants/numericConstants.js +1 -1
  7. package/lib/browser/decode/user.js +5 -1
  8. package/lib/browser/idl/velocity.d.ts +191 -15
  9. package/lib/browser/idl/velocity.json +192 -16
  10. package/lib/browser/orderSubscriber/OrderSubscriber.d.ts +1 -0
  11. package/lib/browser/orderSubscriber/OrderSubscriber.js +10 -4
  12. package/lib/browser/pyth/pythLazerSubscriber.js +37 -27
  13. package/lib/browser/types.d.ts +6 -0
  14. package/lib/browser/user.d.ts +20 -3
  15. package/lib/browser/user.js +52 -4
  16. package/lib/browser/velocityClient.d.ts +46 -10
  17. package/lib/browser/velocityClient.js +107 -35
  18. package/lib/node/adminClient.d.ts +52 -0
  19. package/lib/node/adminClient.d.ts.map +1 -1
  20. package/lib/node/adminClient.js +97 -0
  21. package/lib/node/config.d.ts.map +1 -1
  22. package/lib/node/config.js +6 -7
  23. package/lib/node/constants/numericConstants.d.ts +1 -1
  24. package/lib/node/constants/numericConstants.d.ts.map +1 -1
  25. package/lib/node/constants/numericConstants.js +1 -1
  26. package/lib/node/decode/user.d.ts.map +1 -1
  27. package/lib/node/decode/user.js +5 -1
  28. package/lib/node/idl/velocity.d.ts +191 -15
  29. package/lib/node/idl/velocity.d.ts.map +1 -1
  30. package/lib/node/idl/velocity.json +192 -16
  31. package/lib/node/orderSubscriber/OrderSubscriber.d.ts +1 -0
  32. package/lib/node/orderSubscriber/OrderSubscriber.d.ts.map +1 -1
  33. package/lib/node/orderSubscriber/OrderSubscriber.js +10 -4
  34. package/lib/node/pyth/pythLazerSubscriber.d.ts.map +1 -1
  35. package/lib/node/pyth/pythLazerSubscriber.js +37 -27
  36. package/lib/node/types.d.ts +6 -0
  37. package/lib/node/types.d.ts.map +1 -1
  38. package/lib/node/user.d.ts +20 -3
  39. package/lib/node/user.d.ts.map +1 -1
  40. package/lib/node/user.js +52 -4
  41. package/lib/node/velocityClient.d.ts +46 -10
  42. package/lib/node/velocityClient.d.ts.map +1 -1
  43. package/lib/node/velocityClient.js +107 -35
  44. package/package.json +7 -3
  45. package/.prettierignore +0 -1
  46. package/CHANGELOG.md +0 -132
  47. package/build-browser.js +0 -58
  48. package/bunfig.toml +0 -3
  49. package/esbuild-shims.js +0 -12
  50. package/scripts/deposit-isolated-positions.ts +0 -110
  51. package/scripts/find-flagged-users.ts +0 -213
  52. package/scripts/grpc-client-test-comparison.ts +0 -372
  53. package/scripts/grpc-multiuser-client-test-comparison.ts +0 -158
  54. package/scripts/postbuild.js +0 -95
  55. package/scripts/single-grpc-client-test.ts +0 -284
  56. package/scripts/withdraw-isolated-positions.ts +0 -174
  57. package/src/accounts/README_WebSocketAccountSubscriberV2.md +0 -95
  58. package/src/accounts/README_WebSocketProgramAccountSubscriberV2.md +0 -135
  59. package/src/accounts/basicUserAccountSubscriber.ts +0 -79
  60. package/src/accounts/basicUserStatsAccountSubscriber.ts +0 -87
  61. package/src/accounts/bulkAccountLoader.ts +0 -358
  62. package/src/accounts/bulkUserStatsSubscription.ts +0 -40
  63. package/src/accounts/bulkUserSubscription.ts +0 -39
  64. package/src/accounts/customizedCadenceBulkAccountLoader.ts +0 -250
  65. package/src/accounts/fetch.ts +0 -200
  66. package/src/accounts/grpcAccountSubscriber.ts +0 -225
  67. package/src/accounts/grpcInsuranceFundStakeAccountSubscriber.ts +0 -73
  68. package/src/accounts/grpcMultiAccountSubscriber.ts +0 -566
  69. package/src/accounts/grpcMultiUserAccountSubscriber.ts +0 -328
  70. package/src/accounts/grpcProgramAccountSubscriber.ts +0 -264
  71. package/src/accounts/grpcUserAccountSubscriber.ts +0 -63
  72. package/src/accounts/grpcUserStatsAccountSubscriber.ts +0 -65
  73. package/src/accounts/grpcVelocityClientAccountSubscriber.ts +0 -257
  74. package/src/accounts/grpcVelocityClientAccountSubscriberV2.ts +0 -941
  75. package/src/accounts/laserProgramAccountSubscriber.ts +0 -266
  76. package/src/accounts/oneShotUserAccountSubscriber.ts +0 -88
  77. package/src/accounts/oneShotUserStatsAccountSubscriber.ts +0 -94
  78. package/src/accounts/pollingInsuranceFundStakeAccountSubscriber.ts +0 -223
  79. package/src/accounts/pollingOracleAccountSubscriber.ts +0 -151
  80. package/src/accounts/pollingTokenAccountSubscriber.ts +0 -147
  81. package/src/accounts/pollingUserAccountSubscriber.ts +0 -198
  82. package/src/accounts/pollingUserStatsAccountSubscriber.ts +0 -180
  83. package/src/accounts/pollingVelocityClientAccountSubscriber.ts +0 -774
  84. package/src/accounts/testBulkAccountLoader.ts +0 -59
  85. package/src/accounts/types.ts +0 -428
  86. package/src/accounts/utils.ts +0 -90
  87. package/src/accounts/webSocketAccountSubscriber.ts +0 -304
  88. package/src/accounts/webSocketAccountSubscriberV2.ts +0 -536
  89. package/src/accounts/webSocketInsuranceFundStakeAccountSubscriber.ts +0 -171
  90. package/src/accounts/webSocketProgramAccountSubscriber.ts +0 -244
  91. package/src/accounts/webSocketProgramAccountSubscriberV2.ts +0 -1048
  92. package/src/accounts/webSocketProgramAccountsSubscriberV2.ts +0 -1042
  93. package/src/accounts/webSocketUserAccountSubscriber.ts +0 -139
  94. package/src/accounts/webSocketUserStatsAccountSubsriber.ts +0 -131
  95. package/src/accounts/webSocketVelocityClientAccountSubscriber.ts +0 -865
  96. package/src/accounts/webSocketVelocityClientAccountSubscriberV2.ts +0 -897
  97. package/src/accounts/websocketProgramUserAccountSubscriber.ts +0 -127
  98. package/src/addresses/marketAddresses.ts +0 -28
  99. package/src/addresses/pda.ts +0 -620
  100. package/src/adminClient.ts +0 -8136
  101. package/src/assert/assert.ts +0 -5
  102. package/src/auctionSubscriber/auctionSubscriber.ts +0 -86
  103. package/src/auctionSubscriber/auctionSubscriberGrpc.ts +0 -99
  104. package/src/auctionSubscriber/index.ts +0 -9
  105. package/src/auctionSubscriber/types.ts +0 -30
  106. package/src/bankrun/bankrunConnection.ts +0 -630
  107. package/src/blockhashSubscriber/BlockhashSubscriber.ts +0 -160
  108. package/src/blockhashSubscriber/index.ts +0 -6
  109. package/src/blockhashSubscriber/types.ts +0 -13
  110. package/src/clock/clockSubscriber.ts +0 -146
  111. package/src/config.ts +0 -312
  112. package/src/constants/index.ts +0 -9
  113. package/src/constants/numericConstants.ts +0 -194
  114. package/src/constants/perpMarkets.ts +0 -127
  115. package/src/constants/spotMarkets.ts +0 -104
  116. package/src/constants/txConstants.ts +0 -2
  117. package/src/constituentMap/constituentMap.ts +0 -305
  118. package/src/constituentMap/pollingConstituentAccountSubscriber.ts +0 -99
  119. package/src/constituentMap/webSocketConstituentAccountSubscriber.ts +0 -121
  120. package/src/core/VelocityCore.ts +0 -606
  121. package/src/core/index.ts +0 -12
  122. package/src/core/instructions/deposit.ts +0 -64
  123. package/src/core/instructions/fill.ts +0 -51
  124. package/src/core/instructions/funding.ts +0 -33
  125. package/src/core/instructions/liquidation.ts +0 -55
  126. package/src/core/instructions/orders.ts +0 -84
  127. package/src/core/instructions/perpOrders.ts +0 -312
  128. package/src/core/instructions/settlement.ts +0 -40
  129. package/src/core/instructions/trigger.ts +0 -41
  130. package/src/core/instructions/withdraw.ts +0 -65
  131. package/src/core/remainingAccounts.ts +0 -320
  132. package/src/core/signedMsg.ts +0 -109
  133. package/src/decode/customCoder.ts +0 -148
  134. package/src/decode/user.ts +0 -401
  135. package/src/dlob/DLOB.ts +0 -2793
  136. package/src/dlob/DLOBNode.ts +0 -305
  137. package/src/dlob/DLOBSubscriber.ts +0 -269
  138. package/src/dlob/NodeList.ts +0 -244
  139. package/src/dlob/orderBookLevels.ts +0 -695
  140. package/src/dlob/types.ts +0 -37
  141. package/src/events/eventList.ts +0 -114
  142. package/src/events/eventSubscriber.ts +0 -521
  143. package/src/events/eventsServerLogProvider.ts +0 -189
  144. package/src/events/fetchLogs.ts +0 -219
  145. package/src/events/parse.ts +0 -271
  146. package/src/events/pollingLogProvider.ts +0 -113
  147. package/src/events/sort.ts +0 -51
  148. package/src/events/txEventCache.ts +0 -92
  149. package/src/events/types.ts +0 -337
  150. package/src/events/webSocketLogProvider.ts +0 -152
  151. package/src/factory/bigNum.ts +0 -709
  152. package/src/factory/oracleClient.ts +0 -73
  153. package/src/idl/pyth.json +0 -142
  154. package/src/idl/pyth.ts +0 -97
  155. package/src/idl/pyth_solana_receiver.json +0 -628
  156. package/src/idl/token_faucet.json +0 -229
  157. package/src/idl/token_faucet.ts +0 -197
  158. package/src/idl/velocity.json +0 -23974
  159. package/src/idl/velocity.ts +0 -23980
  160. package/src/index.ts +0 -150
  161. package/src/indicative-quotes/indicativeQuotesSender.ts +0 -233
  162. package/src/isomorphic/README.md +0 -19
  163. package/src/isomorphic/anchor.browser.ts +0 -44
  164. package/src/isomorphic/anchor.node.ts +0 -1
  165. package/src/isomorphic/anchor.ts +0 -1
  166. package/src/isomorphic/anchor29.browser.ts +0 -24
  167. package/src/isomorphic/anchor29.node.ts +0 -1
  168. package/src/isomorphic/anchor29.ts +0 -1
  169. package/src/isomorphic/grpc.browser.ts +0 -4
  170. package/src/isomorphic/grpc.node.ts +0 -130
  171. package/src/isomorphic/grpc.ts +0 -1
  172. package/src/jupiter/jupiterClient.ts +0 -502
  173. package/src/keypair.ts +0 -32
  174. package/src/margin/README.md +0 -138
  175. package/src/marginCalculation.ts +0 -407
  176. package/src/marinade/idl/idl.json +0 -1962
  177. package/src/marinade/index.ts +0 -73
  178. package/src/marinade/types.ts +0 -3925
  179. package/src/math/amm.ts +0 -1758
  180. package/src/math/auction.ts +0 -480
  181. package/src/math/bankruptcy.ts +0 -138
  182. package/src/math/builder.ts +0 -130
  183. package/src/math/conversion.ts +0 -45
  184. package/src/math/exchangeStatus.ts +0 -261
  185. package/src/math/funding.ts +0 -498
  186. package/src/math/insurance.ts +0 -171
  187. package/src/math/liquidation.ts +0 -433
  188. package/src/math/margin.ts +0 -327
  189. package/src/math/market.ts +0 -487
  190. package/src/math/oracles.ts +0 -477
  191. package/src/math/orders.ts +0 -500
  192. package/src/math/position.ts +0 -381
  193. package/src/math/repeg.ts +0 -223
  194. package/src/math/spotBalance.ts +0 -996
  195. package/src/math/spotMarket.ts +0 -115
  196. package/src/math/spotPosition.ts +0 -263
  197. package/src/math/state.ts +0 -71
  198. package/src/math/superStake.ts +0 -602
  199. package/src/math/tiers.ts +0 -73
  200. package/src/math/trade.ts +0 -879
  201. package/src/math/utils.ts +0 -183
  202. package/src/memcmp.ts +0 -279
  203. package/src/oracles/oracleClientCache.ts +0 -41
  204. package/src/oracles/oracleId.ts +0 -99
  205. package/src/oracles/prelaunchOracleClient.ts +0 -63
  206. package/src/oracles/pythClient.ts +0 -125
  207. package/src/oracles/pythLazerClient.ts +0 -141
  208. package/src/oracles/quoteAssetOracleClient.ts +0 -43
  209. package/src/oracles/strictOraclePrice.ts +0 -40
  210. package/src/oracles/types.ts +0 -82
  211. package/src/oracles/utils.ts +0 -45
  212. package/src/orderParams.ts +0 -117
  213. package/src/orderSubscriber/OrderSubscriber.ts +0 -378
  214. package/src/orderSubscriber/PollingSubscription.ts +0 -50
  215. package/src/orderSubscriber/WebsocketSubscription.ts +0 -137
  216. package/src/orderSubscriber/grpcSubscription.ts +0 -155
  217. package/src/orderSubscriber/index.ts +0 -7
  218. package/src/orderSubscriber/types.ts +0 -64
  219. package/src/priorityFee/averageOverSlotsStrategy.ts +0 -17
  220. package/src/priorityFee/averageStrategy.ts +0 -13
  221. package/src/priorityFee/ewmaStrategy.ts +0 -51
  222. package/src/priorityFee/heliusPriorityFeeMethod.ts +0 -68
  223. package/src/priorityFee/index.ts +0 -16
  224. package/src/priorityFee/maxOverSlotsStrategy.ts +0 -18
  225. package/src/priorityFee/maxStrategy.ts +0 -8
  226. package/src/priorityFee/priorityFeeSubscriber.ts +0 -328
  227. package/src/priorityFee/priorityFeeSubscriberMap.ts +0 -134
  228. package/src/priorityFee/solanaPriorityFeeMethod.ts +0 -46
  229. package/src/priorityFee/types.ts +0 -82
  230. package/src/priorityFee/velocityPriorityFeeMethod.ts +0 -53
  231. package/src/pyth/constants.ts +0 -9
  232. package/src/pyth/index.ts +0 -15
  233. package/src/pyth/pythLazerSubscriber.ts +0 -365
  234. package/src/pyth/types.ts +0 -4451
  235. package/src/pyth/utils.ts +0 -13
  236. package/src/slot/SlotSubscriber.ts +0 -126
  237. package/src/slot/SlothashSubscriber.ts +0 -163
  238. package/src/swap/UnifiedSwapClient.ts +0 -315
  239. package/src/swift/grpcSignedMsgUserAccountSubscriber.ts +0 -81
  240. package/src/swift/index.ts +0 -10
  241. package/src/swift/signedMsgUserAccountSubscriber.ts +0 -251
  242. package/src/swift/swiftOrderSubscriber.ts +0 -351
  243. package/src/testClient.ts +0 -42
  244. package/src/titan/titanClient.ts +0 -438
  245. package/src/token/index.ts +0 -13
  246. package/src/tokenFaucet.ts +0 -296
  247. package/src/tx/baseTxSender.ts +0 -651
  248. package/src/tx/blockhashFetcher/baseBlockhashFetcher.ts +0 -31
  249. package/src/tx/blockhashFetcher/cachedBlockhashFetcher.ts +0 -105
  250. package/src/tx/blockhashFetcher/types.ts +0 -9
  251. package/src/tx/fastSingleTxSender.ts +0 -200
  252. package/src/tx/forwardOnlyTxSender.ts +0 -193
  253. package/src/tx/priorityFeeCalculator.ts +0 -117
  254. package/src/tx/reportTransactionError.ts +0 -176
  255. package/src/tx/retryTxSender.ts +0 -177
  256. package/src/tx/txHandler.ts +0 -1009
  257. package/src/tx/txParamProcessor.ts +0 -225
  258. package/src/tx/types.ts +0 -117
  259. package/src/tx/utils.ts +0 -133
  260. package/src/tx/whileValidTxSender.ts +0 -377
  261. package/src/types.ts +0 -2575
  262. package/src/user.ts +0 -5316
  263. package/src/userConfig.ts +0 -38
  264. package/src/userMap/PollingSubscription.ts +0 -61
  265. package/src/userMap/WebsocketSubscription.ts +0 -93
  266. package/src/userMap/grpcSubscription.ts +0 -93
  267. package/src/userMap/referrerMap.ts +0 -360
  268. package/src/userMap/revenueShareEscrowMap.ts +0 -342
  269. package/src/userMap/userMap.ts +0 -784
  270. package/src/userMap/userMapConfig.ts +0 -82
  271. package/src/userMap/userStatsMap.ts +0 -393
  272. package/src/userName.ts +0 -37
  273. package/src/userStats.ts +0 -159
  274. package/src/userStatsConfig.ts +0 -35
  275. package/src/util/TransactionConfirmationManager.ts +0 -333
  276. package/src/util/chainClock.ts +0 -65
  277. package/src/util/computeUnits.ts +0 -101
  278. package/src/util/digest.ts +0 -33
  279. package/src/util/ed25519Utils.ts +0 -95
  280. package/src/util/promiseTimeout.ts +0 -23
  281. package/src/velocityClient.ts +0 -14802
  282. package/src/velocityClientConfig.ts +0 -119
  283. package/src/wallet.ts +0 -91
  284. package/tests/VelocityCore/builder_escrow.test.ts +0 -121
  285. package/tests/VelocityCore/decode.test.ts +0 -16
  286. package/tests/VelocityCore/fill_trigger.test.ts +0 -73
  287. package/tests/VelocityCore/instructions.test.ts +0 -46
  288. package/tests/VelocityCore/pdas.test.ts +0 -30
  289. package/tests/VelocityCore/perp_orders.test.ts +0 -205
  290. package/tests/VelocityCore/remainingAccounts.test.ts +0 -72
  291. package/tests/VelocityCore/settlement_liquidation.test.ts +0 -69
  292. package/tests/accounts/customizedCadenceBulkAccountLoader.test.ts +0 -211
  293. package/tests/amm/test.ts +0 -2402
  294. package/tests/auctions/test.ts +0 -81
  295. package/tests/bn/test.ts +0 -355
  296. package/tests/builder/builderFee.test.ts +0 -42
  297. package/tests/ci/verifyConstants.ts +0 -393
  298. package/tests/decode/test.ts +0 -262
  299. package/tests/decode/userAccountBufferStrings.ts +0 -102
  300. package/tests/dlob/helpers.ts +0 -748
  301. package/tests/dlob/test.ts +0 -6969
  302. package/tests/dlob/tickSizeStandardization.ts +0 -545
  303. package/tests/events/parseLogsForCuUsage.ts +0 -139
  304. package/tests/exchangeStatus/test.ts +0 -45
  305. package/tests/insurance/test.ts +0 -40
  306. package/tests/liquidation/test.ts +0 -125
  307. package/tests/oracles/mmOracleGate.test.ts +0 -379
  308. package/tests/oracles/pythPegSnap.test.ts +0 -76
  309. package/tests/sdkParity/enumParity.test.ts +0 -84
  310. package/tests/sdkParity/marginCategoryFill.test.ts +0 -143
  311. package/tests/sdkParity/memcmpOffsets.test.ts +0 -139
  312. package/tests/spot/test.ts +0 -281
  313. package/tests/tx/TransactionConfirmationManager.test.ts +0 -312
  314. package/tests/tx/cachedBlockhashFetcher.test.ts +0 -96
  315. package/tests/tx/priorityFeeCalculator.ts +0 -77
  316. package/tests/tx/priorityFeeStrategy.ts +0 -95
  317. package/tests/user/bankruptcy.ts +0 -165
  318. package/tests/user/feeAndWithdrawLimits.ts +0 -284
  319. package/tests/user/getMarginCalculation.ts +0 -586
  320. package/tests/user/helpers.ts +0 -184
  321. package/tests/user/liquidations.ts +0 -129
  322. package/tests/user/marginCalculations.test.ts +0 -321
  323. package/tests/user/test.ts +0 -833
  324. package/tsconfig.browser.json +0 -18
  325. package/tsconfig.json +0 -36
@@ -1,1048 +0,0 @@
1
- import { BufferAndSlot, ProgramAccountSubscriber, ResubOpts } from './types';
2
- import { AnchorProvider } from '../isomorphic/anchor';
3
- import { VelocityProgram } from '../config';
4
- import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js';
5
- import {
6
- AccountInfoBase,
7
- AccountInfoWithBase58EncodedData,
8
- AccountInfoWithBase64EncodedData,
9
- createSolanaClient,
10
- isAddress,
11
- Lamports,
12
- Slot,
13
- Address,
14
- Commitment as GillCommitment,
15
- } from 'gill';
16
- import bs58 from 'bs58';
17
-
18
- type ProgramAccountSubscriptionAsyncIterable = AsyncIterable<
19
- Readonly<{
20
- context: Readonly<{
21
- slot: Slot;
22
- }>;
23
- value: Readonly<{
24
- account: Readonly<{
25
- executable: boolean;
26
- lamports: Lamports;
27
- owner: Address;
28
- rentEpoch: bigint;
29
- space: bigint;
30
- }> &
31
- Readonly<any>;
32
- pubkey: Address;
33
- }>;
34
- }>
35
- >;
36
- /**
37
- * WebSocketProgramAccountsSubscriberV2
38
- *
39
- * High-level overview
40
- * - WebSocket-first subscriber for Solana program accounts that also layers in
41
- * targeted polling to detect missed updates reliably.
42
- * - Emits decoded account updates via the provided `onChange` callback.
43
- * - Designed to focus extra work on the specific accounts the consumer cares
44
- * about ("monitored accounts") while keeping baseline WS behavior for the
45
- * full program subscription.
46
- *
47
- * Why polling if this is a WebSocket subscriber?
48
- * - WS infra can stall, drop, or reorder notifications under network stress or
49
- * provider hiccups. When that happens, critical account changes can be missed.
50
- * - To mitigate this, the class accepts a set of accounts (provided via constructor) to monitor
51
- * and uses light polling to verify whether a WS change was missed.
52
- * - If polling detects a newer slot with different data than the last seen
53
- * buffer, a centralized resubscription is triggered to restore a clean stream.
54
- *
55
- * Initial fetch (on subscribe)
56
- * - On `subscribe()`, we first perform a single batched fetch of all monitored
57
- * accounts ("initial monitor fetch").
58
- * - Purpose: seed the internal `bufferAndSlotMap` and emit the latest state so
59
- * consumers have up-to-date data immediately, even before WS events arrive.
60
- * - This step does not decide resubscription; it only establishes ground truth.
61
- *
62
- * Continuous polling (only for monitored accounts)
63
- * - After seeding, each monitored account is put into a monitoring cycle:
64
- * 1) If no WS notification for an account is observed for `pollingIntervalMs`,
65
- * we enqueue it for a batched fetch (buffered for a short window).
66
- * 2) Once an account enters the "currently polling" set, a shared batch poll
67
- * runs every `pollingIntervalMs` across all such accounts.
68
- * 3) If WS notifications resume for an account, that account is removed from
69
- * the polling set and returns to passive monitoring.
70
- * - Polling compares the newly fetched buffer with the last stored buffer at a
71
- * later slot. A difference indicates a missed update; we schedule a single
72
- * resubscription (coalesced across accounts) to re-sync.
73
- *
74
- * Accounts the consumer cares about
75
- * - Provide accounts up-front via the constructor `accountsToMonitor`, or add
76
- * them dynamically with `addAccountToMonitor()` and remove with
77
- * `removeAccountFromMonitor()`.
78
- * - Only these accounts incur additional polling safeguards; other accounts are
79
- * still processed from the WS stream normally.
80
- *
81
- * Resubscription strategy
82
- * - Missed updates from any monitored account are coalesced and trigger a single
83
- * resubscription after a short delay. This avoids rapid churn.
84
- * - If `resubOpts.resubTimeoutMs` is set, an inactivity timer also performs a
85
- * batch check of monitored accounts. If a missed update is found, the same
86
- * centralized resubscription flow is used.
87
- *
88
- * Tuning knobs
89
- * - `setPollingInterval(ms)`: adjust how often monitoring/polling runs
90
- * (default 30s). Shorter = faster detection, higher RPC load.
91
- * - Debounced immediate poll (~100ms): batches accounts added to polling right after inactivity.
92
- * - Batch size for `getMultipleAccounts` is limited to 100, requests are chunked
93
- * and processed concurrently.
94
- */
95
-
96
- export class WebSocketProgramAccountsSubscriberV2<T>
97
- implements ProgramAccountSubscriber<T>
98
- {
99
- subscriptionName: string;
100
- accountDiscriminator: string;
101
- bufferAndSlotMap: Map<string, BufferAndSlot> = new Map();
102
- program: VelocityProgram;
103
- decodeBuffer: (accountName: string, ix: Buffer) => T;
104
- private _onChange?: (
105
- accountId: PublicKey,
106
- data: T,
107
- context: Context,
108
- buffer: Buffer
109
- ) => void;
110
- get onChange(): (
111
- accountId: PublicKey,
112
- data: T,
113
- context: Context,
114
- buffer: Buffer
115
- ) => void {
116
- if (!this._onChange) {
117
- throw new Error('onChange callback function must be set');
118
- }
119
- return this._onChange;
120
- }
121
- set onChange(
122
- onChange: (
123
- accountId: PublicKey,
124
- data: T,
125
- context: Context,
126
- buffer: Buffer
127
- ) => void
128
- ) {
129
- this._onChange = onChange;
130
- }
131
- listenerId?: number;
132
- resubOpts: ResubOpts;
133
- isUnsubscribing = false;
134
- timeoutId?: ReturnType<typeof setTimeout>;
135
- options: { filters: MemcmpFilter[]; commitment?: Commitment };
136
-
137
- receivingData = false;
138
-
139
- // Gill client components
140
- private rpc: ReturnType<typeof createSolanaClient>['rpc'];
141
- private rpcSubscriptions: ReturnType<
142
- typeof createSolanaClient
143
- >['rpcSubscriptions'];
144
- private abortController?: AbortController;
145
-
146
- // Polling logic for specific accounts
147
- private accountsToMonitor: Set<string> = new Set();
148
- private pollingIntervalMs: number = 30000; // 30 seconds
149
- private pollingTimeouts: Map<string, ReturnType<typeof setTimeout>> =
150
- new Map();
151
- private lastWsNotificationTime: Map<string, number> = new Map(); // Track last WS notification time per account
152
- private accountsCurrentlyPolling: Set<string> = new Set(); // Track which accounts are being polled
153
- private batchPollingTimeout?: ReturnType<typeof setTimeout>; // Single timeout for batch polling
154
-
155
- // Debounced immediate poll to batch multiple additions within a short window
156
- private debouncedImmediatePollTimeout?: ReturnType<typeof setTimeout>;
157
- private debouncedImmediatePollMs: number = 100; // configurable short window
158
-
159
- // Centralized resubscription handling
160
- private missedChangeDetected = false; // Flag to track if any missed change was detected
161
- private resubscriptionTimeout?: ReturnType<typeof setTimeout>; // Timeout for delayed resubscription
162
- private accountsWithMissedUpdates: Set<string> = new Set(); // Track which accounts had missed updates
163
-
164
- /**
165
- * @param subscriptionName Human-readable name for logging.
166
- * @param accountDiscriminator Anchor account type name passed to `decodeBufferFn` for each update.
167
- * @param program Anchor program whose accounts to watch (filtered to `program.programId` as owner); its connection's RPC endpoint is reused to build the `gill` client.
168
- * @param decodeBufferFn Decode function for each account's raw buffer.
169
- * @param options `filters` narrowing which program accounts are watched; `commitment` for the WS subscription and RPC polls.
170
- * @param resubOpts Resubscription/polling options; defaults to `{ resubTimeoutMs: 30000, usePollingInsteadOfResub: true }` if omitted (stricter than the v1 `WebSocketProgramAccountSubscriber`, which defaults to no watchdog at all).
171
- * @param accountsToMonitor Initial set of accounts to apply the polling safeguard to; more can be added later via `addAccountToMonitor`.
172
- */
173
- public constructor(
174
- subscriptionName: string,
175
- accountDiscriminator: string,
176
- program: VelocityProgram,
177
- decodeBufferFn: (accountName: string, ix: Buffer) => T,
178
- options: { filters: MemcmpFilter[]; commitment?: Commitment } = {
179
- filters: [],
180
- },
181
- resubOpts?: ResubOpts,
182
- accountsToMonitor?: PublicKey[] // Optional list of accounts to poll
183
- ) {
184
- this.subscriptionName = subscriptionName;
185
- this.accountDiscriminator = accountDiscriminator;
186
- this.program = program;
187
- this.decodeBuffer = decodeBufferFn;
188
- this.resubOpts = resubOpts ?? {
189
- resubTimeoutMs: 30000,
190
- usePollingInsteadOfResub: true,
191
- logResubMessages: false,
192
- };
193
- if (
194
- this.resubOpts?.resubTimeoutMs !== undefined &&
195
- this.resubOpts.resubTimeoutMs < 1000
196
- ) {
197
- console.log(
198
- 'resubTimeoutMs should be at least 1000ms to avoid spamming resub'
199
- );
200
- }
201
- this.options = options;
202
- this.receivingData = false;
203
-
204
- // Initialize accounts to monitor
205
- if (accountsToMonitor) {
206
- accountsToMonitor.forEach((account) => {
207
- this.accountsToMonitor.add(account.toBase58());
208
- });
209
- }
210
-
211
- // Initialize gill client using the same RPC URL as the program provider
212
- const rpcUrl = (this.program.provider as AnchorProvider).connection
213
- .rpcEndpoint;
214
- const { rpc, rpcSubscriptions } = createSolanaClient({
215
- urlOrMoniker: rpcUrl,
216
- });
217
- this.rpc = rpc;
218
- this.rpcSubscriptions = rpcSubscriptions;
219
- }
220
-
221
- private async handleNotificationLoop(
222
- notificationPromise: Promise<ProgramAccountSubscriptionAsyncIterable>
223
- ) {
224
- try {
225
- const subscriptionIterable = await notificationPromise;
226
- for await (const notification of subscriptionIterable) {
227
- try {
228
- if (this.resubOpts?.resubTimeoutMs) {
229
- this.receivingData = true;
230
- clearTimeout(this.timeoutId);
231
- this.handleRpcResponse(
232
- notification.context,
233
- notification.value.pubkey,
234
- notification.value.account.data
235
- );
236
- this.setTimeout();
237
- } else {
238
- this.handleRpcResponse(
239
- notification.context,
240
- notification.value.pubkey,
241
- notification.value.account.data
242
- );
243
- }
244
- } catch (error) {
245
- console.error(
246
- `Error handling RPC response for pubkey ${notification.value.pubkey}:`,
247
- error
248
- );
249
- }
250
- }
251
- } catch (error) {
252
- console.error(
253
- `[${this.subscriptionName}] Error in notification loop:`,
254
- error
255
- );
256
- }
257
- }
258
-
259
- async subscribe(
260
- onChange: (
261
- accountId: PublicKey,
262
- data: T,
263
- context: Context,
264
- buffer: Buffer
265
- ) => void
266
- ): Promise<void> {
267
- /**
268
- * Start the WebSocket subscription and initialize polling safeguards.
269
- *
270
- * Flow
271
- * - Seeds all monitored accounts with a single batched RPC fetch and emits
272
- * their current state.
273
- * - Subscribes to program notifications via WS using gill.
274
- * - If `resubOpts.resubTimeoutMs` is set, starts an inactivity timer that
275
- * batch-checks monitored accounts when WS goes quiet.
276
- * - Begins monitoring for accounts that may need polling when WS
277
- * notifications are not observed within `pollingIntervalMs`.
278
- *
279
- * @param onChange Callback invoked with decoded account data when an update
280
- * is detected (via WS or batch RPC fetch).
281
- */
282
- const startTime = performance.now();
283
- if (this.listenerId != null || this.isUnsubscribing) {
284
- return;
285
- }
286
-
287
- if (this.resubOpts?.logResubMessages) {
288
- console.log(
289
- `[${this.subscriptionName}] initializing subscription. This many monitored accounts: ${this.accountsToMonitor.size}`
290
- );
291
- }
292
-
293
- this.onChange = onChange;
294
-
295
- // initial fetch of monitored data - only fetch and populate, don't check for missed changes
296
- await this.fetchAndPopulateAllMonitoredAccounts();
297
-
298
- // Create abort controller for proper cleanup
299
- const abortController = new AbortController();
300
- this.abortController = abortController;
301
-
302
- this.listenerId = Math.random(); // Unique ID for logging purposes
303
-
304
- if (this.resubOpts?.resubTimeoutMs) {
305
- this.receivingData = true;
306
- this.setTimeout();
307
- }
308
-
309
- // Subscribe to program account changes using gill's rpcSubscriptions
310
- const programId = this.program.programId.toBase58();
311
- if (isAddress(programId)) {
312
- const subscriptionPromise = this.rpcSubscriptions
313
- .programNotifications(programId, {
314
- commitment: this.options.commitment as GillCommitment,
315
- encoding: 'base64',
316
- filters: this.options.filters.map((filter) => {
317
- // Convert filter bytes from base58 to base64 if needed
318
- let bytes = filter.memcmp.bytes;
319
- if (
320
- typeof bytes === 'string' &&
321
- /^[1-9A-HJ-NP-Za-km-z]+$/.test(bytes)
322
- ) {
323
- // Looks like base58 - convert to base64
324
- const decoded = bs58.decode(bytes);
325
- bytes = Buffer.from(decoded).toString('base64');
326
- }
327
-
328
- return {
329
- memcmp: {
330
- offset: BigInt(filter.memcmp.offset),
331
- bytes: bytes as any,
332
- encoding: 'base64' as const,
333
- },
334
- };
335
- }),
336
- })
337
- .subscribe({
338
- abortSignal: abortController.signal,
339
- });
340
-
341
- // Start notification loop without awaiting
342
- this.handleNotificationLoop(subscriptionPromise);
343
- // Start monitoring for accounts that may need polling if no WS event is received
344
- this.startMonitoringForAccounts();
345
- }
346
- const endTime = performance.now();
347
- console.log(
348
- `[PROFILING] ${this.subscriptionName}.subscribe() completed in ${
349
- endTime - startTime
350
- }ms`
351
- );
352
- }
353
-
354
- protected setTimeout(): void {
355
- if (!this._onChange) {
356
- throw new Error('onChange callback function must be set');
357
- }
358
- this.timeoutId = setTimeout(
359
- async () => {
360
- if (this.isUnsubscribing) {
361
- // If we are in the process of unsubscribing, do not attempt to resubscribe
362
- return;
363
- }
364
-
365
- if (this.receivingData) {
366
- if (this.resubOpts?.logResubMessages) {
367
- console.log(
368
- `No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, checking for missed changes`
369
- );
370
- }
371
-
372
- // Check for missed changes in monitored accounts
373
- const missedChangeDetected = await this.fetchAllMonitoredAccounts();
374
-
375
- if (missedChangeDetected) {
376
- // Signal missed change with a generic identifier since we don't have specific account IDs from this context
377
- this.signalMissedChange('timeout-check');
378
- } else {
379
- // No missed changes, continue monitoring
380
- this.receivingData = false;
381
- this.setTimeout();
382
- }
383
- }
384
- },
385
- this.resubOpts?.resubTimeoutMs
386
- );
387
- }
388
-
389
- /**
390
- * Applies a raw notification (or polled response) for one account: decodes and stores it
391
- * (updating that account's entry in `bufferAndSlotMap` and invoking `onChange`) only if the
392
- * slot is not older than the cached one for that specific account and the buffer's bytes
393
- * actually changed. Also records the WS notification time and, if this account was in the
394
- * polling set, removes it from polling now that a fresh WS update arrived.
395
- */
396
- handleRpcResponse(
397
- context: { slot: bigint },
398
- accountId: Address,
399
- accountInfo?: AccountInfoBase &
400
- (
401
- | AccountInfoWithBase58EncodedData
402
- | AccountInfoWithBase64EncodedData
403
- )['data']
404
- ): void {
405
- const newSlot = Number(context.slot);
406
- let newBuffer: Buffer | undefined = undefined;
407
-
408
- if (accountInfo) {
409
- // Handle different data formats from gill
410
- if (Array.isArray(accountInfo)) {
411
- // If it's a tuple [data, encoding]
412
- const [data, encoding] = accountInfo;
413
-
414
- if (encoding === ('base58' as any)) {
415
- // Convert base58 to buffer using bs58
416
- newBuffer = Buffer.from(bs58.decode(data));
417
- } else {
418
- newBuffer = Buffer.from(data, 'base64');
419
- }
420
- }
421
- }
422
-
423
- const accountIdString = accountId.toString();
424
- const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
425
-
426
- // Track WebSocket notification time for this account
427
- this.lastWsNotificationTime.set(accountIdString, Date.now());
428
-
429
- // If this account was being polled, stop polling it if the buffer has changed
430
- if (
431
- this.accountsCurrentlyPolling.has(accountIdString) &&
432
- newBuffer &&
433
- (!existingBufferAndSlot?.buffer ||
434
- !newBuffer.equals(existingBufferAndSlot.buffer))
435
- ) {
436
- this.accountsCurrentlyPolling.delete(accountIdString);
437
-
438
- // If no more accounts are being polled, stop batch polling
439
- if (
440
- this.accountsCurrentlyPolling.size === 0 &&
441
- this.batchPollingTimeout
442
- ) {
443
- clearTimeout(this.batchPollingTimeout);
444
- this.batchPollingTimeout = undefined;
445
- }
446
- }
447
-
448
- if (!existingBufferAndSlot) {
449
- if (newBuffer) {
450
- this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
451
- }
452
- return;
453
- }
454
-
455
- if (newSlot < existingBufferAndSlot.slot) {
456
- return;
457
- }
458
-
459
- const oldBuffer = existingBufferAndSlot.buffer;
460
- if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) {
461
- this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
462
- }
463
- }
464
-
465
- private startMonitoringForAccounts(): void {
466
- // Clear any existing polling timeouts
467
- this.clearPollingTimeouts();
468
-
469
- // Start monitoring for each account in the accountsToMonitor set
470
- this.accountsToMonitor.forEach((accountIdString) => {
471
- this.startMonitoringForAccount(accountIdString);
472
- });
473
- }
474
-
475
- private startMonitoringForAccount(accountIdString: string): void {
476
- // Clear existing timeout for this account
477
- const existingTimeout = this.pollingTimeouts.get(accountIdString);
478
- if (existingTimeout) {
479
- clearTimeout(existingTimeout);
480
- }
481
-
482
- // Set up monitoring timeout - only start polling if no WS notification in 30s
483
- const timeoutId = setTimeout(async () => {
484
- // Check if we've received a WS notification for this account recently
485
- const lastNotificationTime =
486
- this.lastWsNotificationTime.get(accountIdString) || 0;
487
- const currentTime = Date.now();
488
-
489
- if (
490
- !lastNotificationTime ||
491
- currentTime - lastNotificationTime >= this.pollingIntervalMs
492
- ) {
493
- if (this.resubOpts?.logResubMessages) {
494
- console.debug(
495
- `[${this.subscriptionName}] No recent WS notification for ${accountIdString}, adding to polling set`
496
- );
497
- }
498
- // No recent WS notification: add to polling and schedule debounced poll
499
- this.accountsCurrentlyPolling.add(accountIdString);
500
- this.scheduleDebouncedImmediatePoll();
501
- } else {
502
- // We received a WS notification recently, continue monitoring
503
- this.startMonitoringForAccount(accountIdString);
504
- }
505
- }, this.pollingIntervalMs);
506
-
507
- this.pollingTimeouts.set(accountIdString, timeoutId);
508
- }
509
-
510
- private scheduleDebouncedImmediatePoll(): void {
511
- if (this.debouncedImmediatePollTimeout) {
512
- clearTimeout(this.debouncedImmediatePollTimeout);
513
- }
514
- this.debouncedImmediatePollTimeout = setTimeout(async () => {
515
- try {
516
- await this.pollAllAccounts();
517
- // After the immediate poll, ensure continuous batch polling is active
518
- if (
519
- !this.batchPollingTimeout &&
520
- this.accountsCurrentlyPolling.size > 0
521
- ) {
522
- this.startBatchPolling();
523
- }
524
- } catch (e) {
525
- if (this.resubOpts?.logResubMessages) {
526
- console.log(
527
- `[${this.subscriptionName}] Error during debounced immediate poll:`,
528
- e
529
- );
530
- }
531
- }
532
- }, this.debouncedImmediatePollMs);
533
- }
534
-
535
- private startBatchPolling(): void {
536
- if (this.resubOpts?.logResubMessages) {
537
- console.debug(`[${this.subscriptionName}] Scheduling batch polling`);
538
- }
539
- // Clear existing batch polling timeout
540
- if (this.batchPollingTimeout) {
541
- clearTimeout(this.batchPollingTimeout);
542
- }
543
-
544
- // Set up batch polling interval
545
- this.batchPollingTimeout = setTimeout(async () => {
546
- await this.pollAllAccounts();
547
- // Schedule next batch poll
548
- this.startBatchPolling();
549
- }, this.pollingIntervalMs);
550
- }
551
-
552
- private async pollAllAccounts(): Promise<void> {
553
- try {
554
- // Get all accounts currently being polled
555
- const accountsToPoll = Array.from(this.accountsCurrentlyPolling);
556
- if (accountsToPoll.length === 0) {
557
- return;
558
- }
559
-
560
- if (this.resubOpts?.logResubMessages) {
561
- console.debug(
562
- `[${this.subscriptionName}] Polling all accounts`,
563
- accountsToPoll.length,
564
- 'accounts'
565
- );
566
- }
567
-
568
- // Use the shared batch fetch method
569
- await this.fetchAccountsBatch(accountsToPoll);
570
- } catch (error) {
571
- if (this.resubOpts?.logResubMessages) {
572
- console.log(
573
- `[${this.subscriptionName}] Error batch polling accounts:`,
574
- error
575
- );
576
- }
577
- }
578
- }
579
-
580
- /**
581
- * Fetches and populates all monitored accounts data without checking for missed changes
582
- * This is used during initial subscription to populate data
583
- */
584
- private async fetchAndPopulateAllMonitoredAccounts(): Promise<void> {
585
- try {
586
- // Get all accounts currently being polled
587
- const accountsToMonitor = Array.from(this.accountsToMonitor);
588
- if (accountsToMonitor.length === 0) {
589
- return;
590
- }
591
-
592
- // Fetch all accounts in a single batch request
593
- const accountAddresses = accountsToMonitor.map(
594
- (accountId) => accountId as Address
595
- );
596
- const rpcResponse = await this.rpc
597
- .getMultipleAccounts(accountAddresses, {
598
- commitment: this.options.commitment as GillCommitment,
599
- encoding: 'base64',
600
- })
601
- .send();
602
-
603
- const currentSlot = Number(rpcResponse.context.slot);
604
-
605
- // Process each account response
606
- for (let i = 0; i < accountsToMonitor.length; i++) {
607
- const accountIdString = accountsToMonitor[i];
608
- const accountInfo = rpcResponse.value[i];
609
-
610
- if (!accountInfo) {
611
- continue;
612
- }
613
-
614
- const existingBufferAndSlot =
615
- this.bufferAndSlotMap.get(accountIdString);
616
-
617
- if (!existingBufferAndSlot) {
618
- // Account not in our map yet, add it
619
- let newBuffer: Buffer | undefined = undefined;
620
- if (accountInfo) {
621
- if (Array.isArray(accountInfo.data)) {
622
- const [data, encoding] = accountInfo.data;
623
- newBuffer = Buffer.from(data, encoding);
624
- }
625
- }
626
-
627
- if (newBuffer) {
628
- this.updateBufferAndHandleChange(
629
- newBuffer,
630
- currentSlot,
631
- accountIdString
632
- );
633
- }
634
- continue;
635
- }
636
-
637
- // For initial population, just update the slot if we have newer data
638
- if (currentSlot > existingBufferAndSlot.slot) {
639
- let newBuffer: Buffer | undefined = undefined;
640
- if (accountInfo.data) {
641
- if (Array.isArray(accountInfo.data)) {
642
- const [data, encoding] = accountInfo.data;
643
- if (encoding === ('base58' as any)) {
644
- newBuffer = Buffer.from(bs58.decode(data));
645
- } else {
646
- newBuffer = Buffer.from(data, 'base64');
647
- }
648
- }
649
- }
650
-
651
- // Update with newer data if available
652
- if (newBuffer) {
653
- this.updateBufferAndHandleChange(
654
- newBuffer,
655
- currentSlot,
656
- accountIdString
657
- );
658
- }
659
- }
660
- }
661
- } catch (error) {
662
- if (this.resubOpts?.logResubMessages) {
663
- console.log(
664
- `[${this.subscriptionName}] Error fetching and populating monitored accounts:`,
665
- error
666
- );
667
- }
668
- }
669
- }
670
-
671
- /**
672
- * Fetches all monitored accounts and checks for missed changes
673
- * Returns true if a missed change was detected and resubscription is needed
674
- */
675
- private async fetchAllMonitoredAccounts(): Promise<boolean> {
676
- try {
677
- // Get all accounts currently being polled
678
- const accountsToMonitor = Array.from(this.accountsToMonitor);
679
- if (accountsToMonitor.length === 0) {
680
- return false;
681
- }
682
-
683
- // Fetch all accounts in a single batch request
684
- const accountAddresses = accountsToMonitor.map(
685
- (accountId) => accountId as Address
686
- );
687
- const rpcResponse = await this.rpc
688
- .getMultipleAccounts(accountAddresses, {
689
- commitment: this.options.commitment as GillCommitment,
690
- encoding: 'base64',
691
- })
692
- .send();
693
-
694
- const currentSlot = Number(rpcResponse.context.slot);
695
-
696
- // Process each account response
697
- for (let i = 0; i < accountsToMonitor.length; i++) {
698
- const accountIdString = accountsToMonitor[i];
699
- const accountInfo = rpcResponse.value[i];
700
-
701
- if (!accountInfo) {
702
- continue;
703
- }
704
-
705
- const existingBufferAndSlot =
706
- this.bufferAndSlotMap.get(accountIdString);
707
-
708
- if (!existingBufferAndSlot) {
709
- // Account not in our map yet, add it
710
- let newBuffer: Buffer | undefined = undefined;
711
- if (accountInfo.data) {
712
- if (Array.isArray(accountInfo.data)) {
713
- const [data, encoding] = accountInfo.data;
714
- newBuffer = Buffer.from(data, encoding);
715
- }
716
- }
717
-
718
- if (newBuffer) {
719
- this.updateBufferAndHandleChange(
720
- newBuffer,
721
- currentSlot,
722
- accountIdString
723
- );
724
- }
725
- continue;
726
- }
727
-
728
- // Check if we missed an update
729
- if (currentSlot > existingBufferAndSlot.slot) {
730
- let newBuffer: Buffer | undefined = undefined;
731
- if (accountInfo.data) {
732
- if (Array.isArray(accountInfo.data)) {
733
- const [data, encoding] = accountInfo.data;
734
- if (encoding === ('base58' as any)) {
735
- newBuffer = Buffer.from(bs58.decode(data));
736
- } else {
737
- newBuffer = Buffer.from(data, 'base64');
738
- }
739
- }
740
- }
741
-
742
- // Check if buffer has changed
743
- if (
744
- newBuffer &&
745
- (!existingBufferAndSlot.buffer ||
746
- !newBuffer.equals(existingBufferAndSlot.buffer))
747
- ) {
748
- if (this.resubOpts?.logResubMessages) {
749
- console.log(
750
- `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`
751
- );
752
- }
753
- // We missed an update, return true to indicate resubscription is needed
754
- return true;
755
- }
756
- }
757
- }
758
-
759
- // No missed changes detected
760
- return false;
761
- } catch (error) {
762
- if (this.resubOpts?.logResubMessages) {
763
- console.log(
764
- `[${this.subscriptionName}] Error batch polling accounts:`,
765
- error
766
- );
767
- }
768
- return false;
769
- }
770
- }
771
-
772
- private async fetchAccountsBatch(accountIds: string[]): Promise<void> {
773
- try {
774
- // Chunk account IDs into groups of 100 (getMultipleAccounts limit)
775
- const chunkSize = 100;
776
- const chunks: string[][] = [];
777
- for (let i = 0; i < accountIds.length; i += chunkSize) {
778
- chunks.push(accountIds.slice(i, i + chunkSize));
779
- }
780
-
781
- // Process all chunks concurrently
782
- await Promise.all(
783
- chunks.map(async (chunk) => {
784
- const accountAddresses = chunk.map(
785
- (accountId) => accountId as Address
786
- );
787
- const rpcResponse = await this.rpc
788
- .getMultipleAccounts(accountAddresses, {
789
- commitment: this.options.commitment as GillCommitment,
790
- encoding: 'base64',
791
- })
792
- .send();
793
-
794
- const currentSlot = Number(rpcResponse.context.slot);
795
-
796
- // Process each account response in this chunk
797
- for (let i = 0; i < chunk.length; i++) {
798
- const accountIdString = chunk[i];
799
- const accountInfo = rpcResponse.value[i];
800
-
801
- if (!accountInfo) {
802
- continue;
803
- }
804
-
805
- const existingBufferAndSlot =
806
- this.bufferAndSlotMap.get(accountIdString);
807
-
808
- if (!existingBufferAndSlot) {
809
- // Account not in our map yet, add it
810
- let newBuffer: Buffer | undefined = undefined;
811
- if (accountInfo.data) {
812
- if (Array.isArray(accountInfo.data)) {
813
- const [data, encoding] = accountInfo.data;
814
- newBuffer = Buffer.from(data, encoding);
815
- }
816
- }
817
-
818
- if (newBuffer) {
819
- this.updateBufferAndHandleChange(
820
- newBuffer,
821
- currentSlot,
822
- accountIdString
823
- );
824
- }
825
- continue;
826
- }
827
-
828
- // Check if we missed an update
829
- if (currentSlot > existingBufferAndSlot.slot) {
830
- let newBuffer: Buffer | undefined = undefined;
831
- if (accountInfo.data) {
832
- if (Array.isArray(accountInfo.data)) {
833
- const [data, encoding] = accountInfo.data;
834
- if (encoding === ('base58' as any)) {
835
- newBuffer = Buffer.from(bs58.decode(data));
836
- } else {
837
- newBuffer = Buffer.from(data, 'base64');
838
- }
839
- }
840
- }
841
-
842
- // Check if buffer has changed
843
- if (
844
- newBuffer &&
845
- (!existingBufferAndSlot.buffer ||
846
- !newBuffer.equals(existingBufferAndSlot.buffer))
847
- ) {
848
- if (this.resubOpts?.logResubMessages) {
849
- console.log(
850
- `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, signaling resubscription`
851
- );
852
- }
853
- // Signal missed change instead of immediately resubscribing
854
- this.signalMissedChange(accountIdString);
855
- return;
856
- }
857
- }
858
- }
859
- })
860
- );
861
- } catch (error) {
862
- if (this.resubOpts?.logResubMessages) {
863
- console.log(
864
- `[${this.subscriptionName}] Error fetching accounts batch:`,
865
- error
866
- );
867
- }
868
- }
869
- }
870
-
871
- private clearPollingTimeouts(): void {
872
- this.pollingTimeouts.forEach((timeoutId) => {
873
- clearTimeout(timeoutId);
874
- });
875
- this.pollingTimeouts.clear();
876
-
877
- // Clear batch polling timeout
878
- if (this.batchPollingTimeout) {
879
- clearTimeout(this.batchPollingTimeout);
880
- this.batchPollingTimeout = undefined;
881
- }
882
-
883
- // Clear initial fetch timeout
884
- // if (this.initialFetchTimeout) {
885
- // clearTimeout(this.initialFetchTimeout);
886
- // this.initialFetchTimeout = undefined;
887
- // }
888
-
889
- // Clear resubscription timeout
890
- if (this.resubscriptionTimeout) {
891
- clearTimeout(this.resubscriptionTimeout);
892
- this.resubscriptionTimeout = undefined;
893
- }
894
-
895
- // Clear accounts currently polling
896
- this.accountsCurrentlyPolling.clear();
897
-
898
- // Clear accounts pending initial monitor fetch
899
- // this.accountsPendingInitialMonitorFetch.clear();
900
-
901
- // Reset missed change flag and clear accounts with missed updates
902
- this.missedChangeDetected = false;
903
- this.accountsWithMissedUpdates.clear();
904
- }
905
-
906
- /**
907
- * Centralized resubscription handler that only resubscribes once after checking all accounts
908
- */
909
- private async handleResubscription(): Promise<void> {
910
- if (this.missedChangeDetected) {
911
- if (this.resubOpts?.logResubMessages) {
912
- console.log(
913
- `[${this.subscriptionName}] Missed change detected for ${
914
- this.accountsWithMissedUpdates.size
915
- } accounts: ${Array.from(this.accountsWithMissedUpdates).join(
916
- ', '
917
- )}, resubscribing`
918
- );
919
- }
920
- await this.unsubscribe(true);
921
- this.receivingData = false;
922
- await this.subscribe(this.onChange);
923
- this.missedChangeDetected = false;
924
- this.accountsWithMissedUpdates.clear();
925
- }
926
- }
927
-
928
- /**
929
- * Signal that a missed change was detected and schedule resubscription
930
- */
931
- private signalMissedChange(accountIdString: string): void {
932
- if (!this.missedChangeDetected) {
933
- this.missedChangeDetected = true;
934
- this.accountsWithMissedUpdates.add(accountIdString);
935
-
936
- // Clear any existing resubscription timeout
937
- if (this.resubscriptionTimeout) {
938
- clearTimeout(this.resubscriptionTimeout);
939
- }
940
-
941
- // Schedule resubscription after a short delay to allow for batch processing
942
- this.resubscriptionTimeout = setTimeout(async () => {
943
- await this.handleResubscription();
944
- }, 100); // 100ms delay to allow for batch processing
945
- } else {
946
- // If already detected, just add the account to the set
947
- this.accountsWithMissedUpdates.add(accountIdString);
948
- }
949
- }
950
-
951
- /**
952
- * Tears down all polling timeouts and aborts the WebSocket subscription.
953
- * @param onResub Internal flag set to `true` when called as part of an automatic resubscribe cycle, which preserves `resubOpts.resubTimeoutMs` instead of clearing it. Callers should omit this.
954
- */
955
- unsubscribe(onResub = false): Promise<void> {
956
- if (!onResub) {
957
- this.resubOpts.resubTimeoutMs = undefined;
958
- }
959
- this.isUnsubscribing = true;
960
- clearTimeout(this.timeoutId);
961
- this.timeoutId = undefined;
962
-
963
- // Clear polling timeouts
964
- this.clearPollingTimeouts();
965
-
966
- // Abort the WebSocket subscription
967
- if (this.abortController) {
968
- this.abortController.abort('unsubscribing');
969
- this.abortController = undefined;
970
- }
971
-
972
- this.listenerId = undefined;
973
- this.isUnsubscribing = false;
974
-
975
- return Promise.resolve();
976
- }
977
-
978
- // Method to add accounts to the polling list
979
- /**
980
- * Add an account to the monitored set.
981
- * - Monitored accounts are subject to initial fetch and periodic batch polls
982
- * if WS notifications are not observed within `pollingIntervalMs`.
983
- */
984
- addAccountToMonitor(accountId: PublicKey): void {
985
- const accountIdString = accountId.toBase58();
986
- this.accountsToMonitor.add(accountIdString);
987
-
988
- // If already subscribed, start monitoring for this account
989
- if (this.listenerId != null && !this.isUnsubscribing) {
990
- this.startMonitoringForAccount(accountIdString);
991
- }
992
- }
993
-
994
- // Method to remove accounts from the polling list
995
- /** Removes an account from the monitored set and clears any pending/active polling state for it. */
996
- removeAccountFromMonitor(accountId: PublicKey): void {
997
- const accountIdString = accountId.toBase58();
998
- this.accountsToMonitor.delete(accountIdString);
999
-
1000
- // Clear monitoring timeout for this account
1001
- const timeoutId = this.pollingTimeouts.get(accountIdString);
1002
- if (timeoutId) {
1003
- clearTimeout(timeoutId);
1004
- this.pollingTimeouts.delete(accountIdString);
1005
- }
1006
-
1007
- // Remove from currently polling set if it was being polled
1008
- this.accountsCurrentlyPolling.delete(accountIdString);
1009
-
1010
- // Clear stale monitoring bookkeeping so a re-added pubkey isn't mistaken for
1011
- // having a recent WS notification or a still-pending missed update.
1012
- this.lastWsNotificationTime.delete(accountIdString);
1013
- this.accountsWithMissedUpdates.delete(accountIdString);
1014
-
1015
- // If no more accounts are being polled, stop batch polling
1016
- if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) {
1017
- clearTimeout(this.batchPollingTimeout);
1018
- this.batchPollingTimeout = undefined;
1019
- }
1020
- }
1021
-
1022
- // Method to set polling interval
1023
- /**
1024
- * Set the monitoring/polling interval for monitored accounts.
1025
- * Shorter intervals detect missed updates sooner but increase RPC load.
1026
- */
1027
- setPollingInterval(intervalMs: number): void {
1028
- this.pollingIntervalMs = intervalMs;
1029
- // Restart monitoring with new interval if already subscribed
1030
- if (this.listenerId != null && !this.isUnsubscribing) {
1031
- this.startMonitoringForAccounts();
1032
- }
1033
- }
1034
-
1035
- private updateBufferAndHandleChange(
1036
- newBuffer: Buffer,
1037
- newSlot: number,
1038
- accountIdString: string
1039
- ) {
1040
- this.bufferAndSlotMap.set(accountIdString, {
1041
- buffer: newBuffer,
1042
- slot: newSlot,
1043
- });
1044
- const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
1045
- const accountIdPubkey = new PublicKey(accountIdString);
1046
- this.onChange(accountIdPubkey, account, { slot: newSlot }, newBuffer);
1047
- }
1048
- }