@lifi/perps-sdk-provider-ondo 0.0.0-preview-7ff840c

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 (280) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +38 -0
  3. package/dist/cjs/OndoProvider.d.ts +8 -0
  4. package/dist/cjs/OndoProvider.d.ts.map +1 -0
  5. package/dist/cjs/OndoProvider.js +338 -0
  6. package/dist/cjs/OndoProvider.js.map +1 -0
  7. package/dist/cjs/accountConfig.d.ts +3 -0
  8. package/dist/cjs/accountConfig.d.ts.map +1 -0
  9. package/dist/cjs/accountConfig.js +59 -0
  10. package/dist/cjs/accountConfig.js.map +1 -0
  11. package/dist/cjs/accountSummary.d.ts +3 -0
  12. package/dist/cjs/accountSummary.d.ts.map +1 -0
  13. package/dist/cjs/accountSummary.js +8 -0
  14. package/dist/cjs/accountSummary.js.map +1 -0
  15. package/dist/cjs/auth/OndoTokenStore.d.ts +13 -0
  16. package/dist/cjs/auth/OndoTokenStore.d.ts.map +1 -0
  17. package/dist/cjs/auth/OndoTokenStore.js +53 -0
  18. package/dist/cjs/auth/OndoTokenStore.js.map +1 -0
  19. package/dist/cjs/auth/completeSiweLogin.d.ts +13 -0
  20. package/dist/cjs/auth/completeSiweLogin.d.ts.map +1 -0
  21. package/dist/cjs/auth/completeSiweLogin.js +15 -0
  22. package/dist/cjs/auth/completeSiweLogin.js.map +1 -0
  23. package/dist/cjs/auth/index.d.ts +4 -0
  24. package/dist/cjs/auth/index.d.ts.map +1 -0
  25. package/dist/cjs/auth/index.js +11 -0
  26. package/dist/cjs/auth/index.js.map +1 -0
  27. package/dist/cjs/auth/signActions.d.ts +13 -0
  28. package/dist/cjs/auth/signActions.d.ts.map +1 -0
  29. package/dist/cjs/auth/signActions.js +86 -0
  30. package/dist/cjs/auth/signActions.js.map +1 -0
  31. package/dist/cjs/constants.d.ts +9 -0
  32. package/dist/cjs/constants.d.ts.map +1 -0
  33. package/dist/cjs/constants.js +12 -0
  34. package/dist/cjs/constants.js.map +1 -0
  35. package/dist/cjs/index.d.ts +9 -0
  36. package/dist/cjs/index.d.ts.map +1 -0
  37. package/dist/cjs/index.js +37 -0
  38. package/dist/cjs/index.js.map +1 -0
  39. package/dist/cjs/package.json +1 -0
  40. package/dist/cjs/types/auth.d.ts +16 -0
  41. package/dist/cjs/types/auth.d.ts.map +1 -0
  42. package/dist/cjs/types/auth.js +3 -0
  43. package/dist/cjs/types/auth.js.map +1 -0
  44. package/dist/cjs/types/index.d.ts +4 -0
  45. package/dist/cjs/types/index.d.ts.map +1 -0
  46. package/dist/cjs/types/index.js +3 -0
  47. package/dist/cjs/types/index.js.map +1 -0
  48. package/dist/cjs/types/wire.d.ts +154 -0
  49. package/dist/cjs/types/wire.d.ts.map +1 -0
  50. package/dist/cjs/types/wire.js +3 -0
  51. package/dist/cjs/types/wire.js.map +1 -0
  52. package/dist/cjs/types/ws.d.ts +92 -0
  53. package/dist/cjs/types/ws.d.ts.map +1 -0
  54. package/dist/cjs/types/ws.js +3 -0
  55. package/dist/cjs/types/ws.js.map +1 -0
  56. package/dist/cjs/utils/activityCursor.d.ts +9 -0
  57. package/dist/cjs/utils/activityCursor.d.ts.map +1 -0
  58. package/dist/cjs/utils/activityCursor.js +79 -0
  59. package/dist/cjs/utils/activityCursor.js.map +1 -0
  60. package/dist/cjs/utils/apiClient.d.ts +43 -0
  61. package/dist/cjs/utils/apiClient.d.ts.map +1 -0
  62. package/dist/cjs/utils/apiClient.js +134 -0
  63. package/dist/cjs/utils/apiClient.js.map +1 -0
  64. package/dist/cjs/utils/index.d.ts +11 -0
  65. package/dist/cjs/utils/index.d.ts.map +1 -0
  66. package/dist/cjs/utils/index.js +39 -0
  67. package/dist/cjs/utils/index.js.map +1 -0
  68. package/dist/cjs/utils/liquidation.d.ts +4 -0
  69. package/dist/cjs/utils/liquidation.d.ts.map +1 -0
  70. package/dist/cjs/utils/liquidation.js +16 -0
  71. package/dist/cjs/utils/liquidation.js.map +1 -0
  72. package/dist/cjs/utils/mapActivity.d.ts +5 -0
  73. package/dist/cjs/utils/mapActivity.d.ts.map +1 -0
  74. package/dist/cjs/utils/mapActivity.js +34 -0
  75. package/dist/cjs/utils/mapActivity.js.map +1 -0
  76. package/dist/cjs/utils/mapFill.d.ts +4 -0
  77. package/dist/cjs/utils/mapFill.d.ts.map +1 -0
  78. package/dist/cjs/utils/mapFill.js +36 -0
  79. package/dist/cjs/utils/mapFill.js.map +1 -0
  80. package/dist/cjs/utils/mapOrder.d.ts +16 -0
  81. package/dist/cjs/utils/mapOrder.d.ts.map +1 -0
  82. package/dist/cjs/utils/mapOrder.js +135 -0
  83. package/dist/cjs/utils/mapOrder.js.map +1 -0
  84. package/dist/cjs/utils/mapPosition.d.ts +5 -0
  85. package/dist/cjs/utils/mapPosition.d.ts.map +1 -0
  86. package/dist/cjs/utils/mapPosition.js +26 -0
  87. package/dist/cjs/utils/mapPosition.js.map +1 -0
  88. package/dist/cjs/utils/ohlcvInterval.d.ts +4 -0
  89. package/dist/cjs/utils/ohlcvInterval.d.ts.map +1 -0
  90. package/dist/cjs/utils/ohlcvInterval.js +49 -0
  91. package/dist/cjs/utils/ohlcvInterval.js.map +1 -0
  92. package/dist/cjs/utils/ondoAsset.d.ts +3 -0
  93. package/dist/cjs/utils/ondoAsset.d.ts.map +1 -0
  94. package/dist/cjs/utils/ondoAsset.js +12 -0
  95. package/dist/cjs/utils/ondoAsset.js.map +1 -0
  96. package/dist/cjs/utils/orderFormatting.d.ts +4 -0
  97. package/dist/cjs/utils/orderFormatting.d.ts.map +1 -0
  98. package/dist/cjs/utils/orderFormatting.js +40 -0
  99. package/dist/cjs/utils/orderFormatting.js.map +1 -0
  100. package/dist/cjs/websocket/OndoWsProvider.d.ts +56 -0
  101. package/dist/cjs/websocket/OndoWsProvider.d.ts.map +1 -0
  102. package/dist/cjs/websocket/OndoWsProvider.js +515 -0
  103. package/dist/cjs/websocket/OndoWsProvider.js.map +1 -0
  104. package/dist/esm/OndoProvider.d.ts +39 -0
  105. package/dist/esm/OndoProvider.d.ts.map +1 -0
  106. package/dist/esm/OndoProvider.js +366 -0
  107. package/dist/esm/OndoProvider.js.map +1 -0
  108. package/dist/esm/accountConfig.d.ts +13 -0
  109. package/dist/esm/accountConfig.d.ts.map +1 -0
  110. package/dist/esm/accountConfig.js +77 -0
  111. package/dist/esm/accountConfig.js.map +1 -0
  112. package/dist/esm/accountSummary.d.ts +12 -0
  113. package/dist/esm/accountSummary.d.ts.map +1 -0
  114. package/dist/esm/accountSummary.js +14 -0
  115. package/dist/esm/accountSummary.js.map +1 -0
  116. package/dist/esm/auth/OndoTokenStore.d.ts +20 -0
  117. package/dist/esm/auth/OndoTokenStore.d.ts.map +1 -0
  118. package/dist/esm/auth/OndoTokenStore.js +61 -0
  119. package/dist/esm/auth/OndoTokenStore.js.map +1 -0
  120. package/dist/esm/auth/completeSiweLogin.d.ts +28 -0
  121. package/dist/esm/auth/completeSiweLogin.d.ts.map +1 -0
  122. package/dist/esm/auth/completeSiweLogin.js +21 -0
  123. package/dist/esm/auth/completeSiweLogin.js.map +1 -0
  124. package/dist/esm/auth/index.d.ts +4 -0
  125. package/dist/esm/auth/index.d.ts.map +1 -0
  126. package/dist/esm/auth/index.js +5 -0
  127. package/dist/esm/auth/index.js.map +1 -0
  128. package/dist/esm/auth/signActions.d.ts +35 -0
  129. package/dist/esm/auth/signActions.d.ts.map +1 -0
  130. package/dist/esm/auth/signActions.js +103 -0
  131. package/dist/esm/auth/signActions.js.map +1 -0
  132. package/dist/esm/constants.d.ts +35 -0
  133. package/dist/esm/constants.d.ts.map +1 -0
  134. package/dist/esm/constants.js +35 -0
  135. package/dist/esm/constants.js.map +1 -0
  136. package/dist/esm/index.d.ts +19 -0
  137. package/dist/esm/index.d.ts.map +1 -0
  138. package/dist/esm/index.js +21 -0
  139. package/dist/esm/index.js.map +1 -0
  140. package/dist/esm/package.json +1 -0
  141. package/dist/esm/types/auth.d.ts +32 -0
  142. package/dist/esm/types/auth.d.ts.map +1 -0
  143. package/dist/esm/types/auth.js +2 -0
  144. package/dist/esm/types/auth.js.map +1 -0
  145. package/dist/esm/types/index.d.ts +4 -0
  146. package/dist/esm/types/index.d.ts.map +1 -0
  147. package/dist/esm/types/index.js +3 -0
  148. package/dist/esm/types/index.js.map +1 -0
  149. package/dist/esm/types/wire.d.ts +195 -0
  150. package/dist/esm/types/wire.d.ts.map +1 -0
  151. package/dist/esm/types/wire.js +7 -0
  152. package/dist/esm/types/wire.js.map +1 -0
  153. package/dist/esm/types/ws.d.ts +108 -0
  154. package/dist/esm/types/ws.d.ts.map +1 -0
  155. package/dist/esm/types/ws.js +7 -0
  156. package/dist/esm/types/ws.js.map +1 -0
  157. package/dist/esm/utils/activityCursor.d.ts +47 -0
  158. package/dist/esm/utils/activityCursor.d.ts.map +1 -0
  159. package/dist/esm/utils/activityCursor.js +93 -0
  160. package/dist/esm/utils/activityCursor.js.map +1 -0
  161. package/dist/esm/utils/apiClient.d.ts +90 -0
  162. package/dist/esm/utils/apiClient.d.ts.map +1 -0
  163. package/dist/esm/utils/apiClient.js +169 -0
  164. package/dist/esm/utils/apiClient.js.map +1 -0
  165. package/dist/esm/utils/index.d.ts +11 -0
  166. package/dist/esm/utils/index.d.ts.map +1 -0
  167. package/dist/esm/utils/index.js +12 -0
  168. package/dist/esm/utils/index.js.map +1 -0
  169. package/dist/esm/utils/liquidation.d.ts +20 -0
  170. package/dist/esm/utils/liquidation.d.ts.map +1 -0
  171. package/dist/esm/utils/liquidation.js +29 -0
  172. package/dist/esm/utils/liquidation.js.map +1 -0
  173. package/dist/esm/utils/mapActivity.d.ts +21 -0
  174. package/dist/esm/utils/mapActivity.d.ts.map +1 -0
  175. package/dist/esm/utils/mapActivity.js +45 -0
  176. package/dist/esm/utils/mapActivity.js.map +1 -0
  177. package/dist/esm/utils/mapFill.d.ts +12 -0
  178. package/dist/esm/utils/mapFill.d.ts.map +1 -0
  179. package/dist/esm/utils/mapFill.js +37 -0
  180. package/dist/esm/utils/mapFill.js.map +1 -0
  181. package/dist/esm/utils/mapOrder.d.ts +50 -0
  182. package/dist/esm/utils/mapOrder.d.ts.map +1 -0
  183. package/dist/esm/utils/mapOrder.js +155 -0
  184. package/dist/esm/utils/mapOrder.js.map +1 -0
  185. package/dist/esm/utils/mapPosition.d.ts +19 -0
  186. package/dist/esm/utils/mapPosition.d.ts.map +1 -0
  187. package/dist/esm/utils/mapPosition.js +32 -0
  188. package/dist/esm/utils/mapPosition.js.map +1 -0
  189. package/dist/esm/utils/ohlcvInterval.d.ts +11 -0
  190. package/dist/esm/utils/ohlcvInterval.d.ts.map +1 -0
  191. package/dist/esm/utils/ohlcvInterval.js +59 -0
  192. package/dist/esm/utils/ohlcvInterval.js.map +1 -0
  193. package/dist/esm/utils/ondoAsset.d.ts +4 -0
  194. package/dist/esm/utils/ondoAsset.d.ts.map +1 -0
  195. package/dist/esm/utils/ondoAsset.js +9 -0
  196. package/dist/esm/utils/ondoAsset.js.map +1 -0
  197. package/dist/esm/utils/orderFormatting.d.ts +33 -0
  198. package/dist/esm/utils/orderFormatting.d.ts.map +1 -0
  199. package/dist/esm/utils/orderFormatting.js +62 -0
  200. package/dist/esm/utils/orderFormatting.js.map +1 -0
  201. package/dist/esm/websocket/OndoWsProvider.d.ts +122 -0
  202. package/dist/esm/websocket/OndoWsProvider.d.ts.map +1 -0
  203. package/dist/esm/websocket/OndoWsProvider.js +591 -0
  204. package/dist/esm/websocket/OndoWsProvider.js.map +1 -0
  205. package/dist/types/OndoProvider.d.ts +39 -0
  206. package/dist/types/OndoProvider.d.ts.map +1 -0
  207. package/dist/types/accountConfig.d.ts +13 -0
  208. package/dist/types/accountConfig.d.ts.map +1 -0
  209. package/dist/types/accountSummary.d.ts +12 -0
  210. package/dist/types/accountSummary.d.ts.map +1 -0
  211. package/dist/types/auth/OndoTokenStore.d.ts +20 -0
  212. package/dist/types/auth/OndoTokenStore.d.ts.map +1 -0
  213. package/dist/types/auth/completeSiweLogin.d.ts +28 -0
  214. package/dist/types/auth/completeSiweLogin.d.ts.map +1 -0
  215. package/dist/types/auth/index.d.ts +4 -0
  216. package/dist/types/auth/index.d.ts.map +1 -0
  217. package/dist/types/auth/signActions.d.ts +35 -0
  218. package/dist/types/auth/signActions.d.ts.map +1 -0
  219. package/dist/types/constants.d.ts +35 -0
  220. package/dist/types/constants.d.ts.map +1 -0
  221. package/dist/types/index.d.ts +19 -0
  222. package/dist/types/index.d.ts.map +1 -0
  223. package/dist/types/types/auth.d.ts +32 -0
  224. package/dist/types/types/auth.d.ts.map +1 -0
  225. package/dist/types/types/index.d.ts +4 -0
  226. package/dist/types/types/index.d.ts.map +1 -0
  227. package/dist/types/types/wire.d.ts +195 -0
  228. package/dist/types/types/wire.d.ts.map +1 -0
  229. package/dist/types/types/ws.d.ts +108 -0
  230. package/dist/types/types/ws.d.ts.map +1 -0
  231. package/dist/types/utils/activityCursor.d.ts +47 -0
  232. package/dist/types/utils/activityCursor.d.ts.map +1 -0
  233. package/dist/types/utils/apiClient.d.ts +90 -0
  234. package/dist/types/utils/apiClient.d.ts.map +1 -0
  235. package/dist/types/utils/index.d.ts +11 -0
  236. package/dist/types/utils/index.d.ts.map +1 -0
  237. package/dist/types/utils/liquidation.d.ts +20 -0
  238. package/dist/types/utils/liquidation.d.ts.map +1 -0
  239. package/dist/types/utils/mapActivity.d.ts +21 -0
  240. package/dist/types/utils/mapActivity.d.ts.map +1 -0
  241. package/dist/types/utils/mapFill.d.ts +12 -0
  242. package/dist/types/utils/mapFill.d.ts.map +1 -0
  243. package/dist/types/utils/mapOrder.d.ts +50 -0
  244. package/dist/types/utils/mapOrder.d.ts.map +1 -0
  245. package/dist/types/utils/mapPosition.d.ts +19 -0
  246. package/dist/types/utils/mapPosition.d.ts.map +1 -0
  247. package/dist/types/utils/ohlcvInterval.d.ts +11 -0
  248. package/dist/types/utils/ohlcvInterval.d.ts.map +1 -0
  249. package/dist/types/utils/ondoAsset.d.ts +4 -0
  250. package/dist/types/utils/ondoAsset.d.ts.map +1 -0
  251. package/dist/types/utils/orderFormatting.d.ts +33 -0
  252. package/dist/types/utils/orderFormatting.d.ts.map +1 -0
  253. package/dist/types/websocket/OndoWsProvider.d.ts +122 -0
  254. package/dist/types/websocket/OndoWsProvider.d.ts.map +1 -0
  255. package/package.json +64 -0
  256. package/src/OndoProvider.ts +635 -0
  257. package/src/accountConfig.ts +105 -0
  258. package/src/accountSummary.ts +22 -0
  259. package/src/auth/OndoTokenStore.ts +83 -0
  260. package/src/auth/completeSiweLogin.ts +43 -0
  261. package/src/auth/index.ts +12 -0
  262. package/src/auth/signActions.ts +160 -0
  263. package/src/constants.ts +38 -0
  264. package/src/index.ts +36 -0
  265. package/src/types/auth.ts +32 -0
  266. package/src/types/index.ts +5 -0
  267. package/src/types/wire.ts +228 -0
  268. package/src/types/ws.ts +79 -0
  269. package/src/utils/activityCursor.ts +140 -0
  270. package/src/utils/apiClient.ts +257 -0
  271. package/src/utils/index.ts +35 -0
  272. package/src/utils/liquidation.ts +37 -0
  273. package/src/utils/mapActivity.ts +61 -0
  274. package/src/utils/mapFill.ts +47 -0
  275. package/src/utils/mapOrder.ts +201 -0
  276. package/src/utils/mapPosition.ts +44 -0
  277. package/src/utils/ohlcvInterval.ts +69 -0
  278. package/src/utils/ondoAsset.ts +10 -0
  279. package/src/utils/orderFormatting.ts +70 -0
  280. package/src/websocket/OndoWsProvider.ts +752 -0
@@ -0,0 +1,752 @@
1
+ import {
2
+ getMarketRegistry,
3
+ localStorageAdapter,
4
+ type MarketRegistry,
5
+ type PerpsSDKClient,
6
+ type ProviderGetQuoteParams,
7
+ type QuoteListener,
8
+ ReconnectingWebSocket,
9
+ resolveSubscribeQuote,
10
+ type StorageAdapter,
11
+ toMarketDisplay,
12
+ WsProviderBase,
13
+ type WsProviderFactory,
14
+ wsLog,
15
+ } from '@lifi/perps-sdk'
16
+ import type {
17
+ MarketContext,
18
+ MarketDisplay,
19
+ Subscription,
20
+ } from '@lifi/perps-types'
21
+ import type { Address } from 'viem'
22
+ import { OndoTokenStore } from '../auth/OndoTokenStore.js'
23
+ import {
24
+ DEFAULT_ONDO_API_URL,
25
+ DEFAULT_ONDO_WS_URL,
26
+ ONDO_BASE_FEE_TIER,
27
+ ONDO_PROVIDER_KEY,
28
+ } from '../constants.js'
29
+ import type {
30
+ OndoBalanceSummary,
31
+ OndoBookSnapshot,
32
+ OndoFill,
33
+ OndoFundingRate,
34
+ OndoKline,
35
+ OndoMarkPrice,
36
+ OndoOrder,
37
+ OndoPosition,
38
+ OndoWsMessage,
39
+ OndoWsTrade,
40
+ } from '../types/index.js'
41
+ import { OndoSessionExpiredError } from '../utils/apiClient.js'
42
+ import {
43
+ classifyAndMapOrders,
44
+ mapFill,
45
+ mapOpenPositions,
46
+ OndoApiClient,
47
+ } from '../utils/index.js'
48
+ import { intervalFromBarSpan, mapInterval } from '../utils/ohlcvInterval.js'
49
+
50
+ // Public channels (per market, demuxed from the frame's `market` field):
51
+ // orderbook → depthBooksPerps, trades → tradesPerps, candle → kLinePerps.
52
+ // `marketsContext`/`marketContext` merge two wire channels — markPricesPerps
53
+ // and fundingRatesPerps — into the aggregated context emit; Ondo publishes no
54
+ // mid price on either, so `markPrice` stands in for `midPrice`.
55
+ // Authenticated channels (account-wide, no market filter):
56
+ // orderUpdates → ordersPerps, fills → fillsPerps, positions → positionsPerps.
57
+ //
58
+ // Auth pattern (per the Ondo WS spec): one `{op:'login', args:{token}}` per
59
+ // connection, sent before the first private subscribe; the venue rejects a
60
+ // second login on the same connection. The JWT comes from the SIWE session in
61
+ // the token store — absent or expired reads back as null and surfaces as
62
+ // `OndoSessionExpiredError` so callers re-run the SIWE login. A dropped
63
+ // socket forgets the login, so the guard resets on every close.
64
+
65
+ const ONDO_AUTH_CHANNEL = {
66
+ orderUpdates: 'ordersPerps',
67
+ fills: 'fillsPerps',
68
+ positions: 'positionsPerps',
69
+ } as const
70
+
71
+ type AuthChannel = keyof typeof ONDO_AUTH_CHANNEL
72
+
73
+ const isAuthChannel = (
74
+ channel: Subscription['channel']
75
+ ): channel is AuthChannel => channel in ONDO_AUTH_CHANNEL
76
+
77
+ interface SubState {
78
+ /** Subscribe payload minus `op` — re-sent verbatim on every (re)open. */
79
+ frame: { channel: string; markets?: string[]; resolution?: string }
80
+ needsAuth: boolean
81
+ address?: Address
82
+ }
83
+
84
+ /**
85
+ * Options for {@link ondoWsProvider} / {@link OndoWsProvider}.
86
+ *
87
+ * @public
88
+ */
89
+ export interface OndoWsProviderOptions {
90
+ /**
91
+ * Ondo REST base URL — namespaces the session-token lookup, so it must
92
+ * match the `apiUrl` the SIWE login stored the token under. Defaults to
93
+ * production.
94
+ */
95
+ apiUrl?: string
96
+ /** Session-token persistence backend. Defaults to browser `localStorage`. */
97
+ storage?: StorageAdapter
98
+ }
99
+
100
+ /**
101
+ * Ondo realtime WS provider (extends {@link WsProviderBase}): subscribes to
102
+ * Ondo's WS channels (orderbook, trades, candles, market context, orders,
103
+ * fills, positions), logging in with the stored SIWE session JWT for the
104
+ * account channels. Construct via {@link ondoWsProvider}.
105
+ *
106
+ * @public
107
+ */
108
+ export class OndoWsProvider extends WsProviderBase<SubState> {
109
+ private readonly tokenStore: OndoTokenStore
110
+ private readonly client: PerpsSDKClient | undefined
111
+ private readonly registry: MarketRegistry | undefined
112
+ private readonly apiUrl: string
113
+
114
+ /**
115
+ * Ref count per shared authenticated wire channel (`positionsPerps`,
116
+ * `fillsPerps`, `ordersPerps`): `positions`/`fills` and the derived
117
+ * `accountSummary` ride the same wire subs, so neither's teardown may
118
+ * unsubscribe one the other still holds.
119
+ */
120
+ private readonly authWireRefs = new Map<string, number>()
121
+
122
+ /**
123
+ * Running account-summary inputs while `accountSummary` is subscribed
124
+ * (`undefined` otherwise). `walletBalance` is REST-seeded and refreshed on
125
+ * fills; `marginUsed`/`unrealizedPnl` track the latest positions frame.
126
+ */
127
+ private accountSummary:
128
+ | { walletBalance: number; marginUsed: number; unrealizedPnl: number }
129
+ | undefined
130
+
131
+ /** Complete (mark-price-bearing) contexts, keyed by venue market symbol. */
132
+ private contexts: Record<string, MarketContext> = {}
133
+ /** Funding seen before the market's first mark price. */
134
+ private pendingFunding = new Map<
135
+ string,
136
+ { rate: string; nextFundingTime: number }
137
+ >()
138
+
139
+ /** The single address this connection's login is bound to. */
140
+ private accountAddress: string | undefined
141
+ private loginPromise: Promise<void> | undefined
142
+
143
+ constructor(
144
+ wsUrl: string = DEFAULT_ONDO_WS_URL,
145
+ providerKey = ONDO_PROVIDER_KEY,
146
+ options: OndoWsProviderOptions = {},
147
+ client?: PerpsSDKClient
148
+ ) {
149
+ super(
150
+ new ReconnectingWebSocket(wsUrl, { pingPayload: '{"op":"ping"}' }),
151
+ providerKey
152
+ )
153
+ this.apiUrl = options.apiUrl ?? DEFAULT_ONDO_API_URL
154
+ this.tokenStore = new OndoTokenStore(
155
+ options.storage ?? localStorageAdapter,
156
+ this.apiUrl
157
+ )
158
+ this.client = client
159
+ this.registry = client && getMarketRegistry(client, providerKey)
160
+ // The venue's login lives and dies with the connection.
161
+ this.rws.on('close', () => {
162
+ this.loginPromise = undefined
163
+ })
164
+ }
165
+
166
+ async subscribeQuote(
167
+ params: ProviderGetQuoteParams,
168
+ onQuote: QuoteListener
169
+ ): Promise<() => void> {
170
+ const client = this.client
171
+ if (client === undefined) {
172
+ throw new Error(
173
+ 'OndoWsProvider: PerpsSDKClient not provided; cannot stream quotes. ' +
174
+ 'Construct via `ondoWsProvider({...})` and register with PerpsWsClient.'
175
+ )
176
+ }
177
+ return resolveSubscribeQuote(
178
+ client,
179
+ this.providerKey,
180
+ this,
181
+ params,
182
+ ONDO_BASE_FEE_TIER,
183
+ onQuote
184
+ )
185
+ }
186
+
187
+ protected toKey(sub: Subscription): string {
188
+ switch (sub.channel) {
189
+ case 'marketsContext':
190
+ return 'marketsContext'
191
+ case 'marketContext':
192
+ return `marketContext:${sub.marketId}`
193
+ case 'orderbook':
194
+ return `orderbook:${sub.marketId}`
195
+ case 'trades':
196
+ return `trades:${sub.marketId}`
197
+ case 'candle':
198
+ return `candle:${sub.marketId}:${sub.interval}`
199
+ case 'orderUpdates':
200
+ case 'fills':
201
+ case 'positions':
202
+ case 'accountSummary':
203
+ case 'spotBalances':
204
+ return `${sub.channel}:${sub.address.toLowerCase()}`
205
+ }
206
+ }
207
+
208
+ protected async openChannel(sub: Subscription): Promise<() => void> {
209
+ // Ondo streams no balance channel: `spotBalances` is always empty, so
210
+ // emit one empty snapshot and hold the wire untouched.
211
+ if (sub.channel === 'spotBalances') {
212
+ this.emit(this.toKey(sub), { channel: 'spotBalances', data: [] })
213
+ return () => {}
214
+ }
215
+
216
+ const wireSubs = this.resolveChannel(sub)
217
+ const needsLogin =
218
+ isAuthChannel(sub.channel) || sub.channel === 'accountSummary'
219
+
220
+ if (needsLogin) {
221
+ // One login per connection means one authenticated address.
222
+ const address = (sub as { address: Address }).address.toLowerCase()
223
+ if (
224
+ this.accountAddress !== undefined &&
225
+ this.accountAddress !== address
226
+ ) {
227
+ throw new Error(
228
+ `Ondo WS supports one authenticated address per connection; already bound to ${this.accountAddress}, cannot subscribe for ${address}.`
229
+ )
230
+ }
231
+ this.accountAddress = address
232
+ // Account frames carry venue market symbols; the registry supplies the
233
+ // market identity the mapped orders/fills/positions embed.
234
+ await this.registry?.sync()
235
+ }
236
+
237
+ if (sub.channel === 'accountSummary') {
238
+ await this.seedAccountSummary((sub as { address: Address }).address)
239
+ }
240
+ // Authenticated wire subs are shared and ref-counted; public ones are 1:1.
241
+ for (const [key, state] of wireSubs) {
242
+ await this.acquireWire(key, state, needsLogin)
243
+ }
244
+ await this.rws.ready()
245
+
246
+ return () => {
247
+ if (sub.channel === 'accountSummary') {
248
+ this.accountSummary = undefined
249
+ }
250
+ for (const [key, state] of wireSubs) {
251
+ this.releaseWire(key, state, needsLogin)
252
+ }
253
+ }
254
+ }
255
+
256
+ /** Register a wire sub, ref-counting the shared authenticated channels. */
257
+ private async acquireWire(
258
+ key: string,
259
+ state: SubState,
260
+ shared: boolean
261
+ ): Promise<void> {
262
+ if (!shared) {
263
+ await this.registerSub(key, state)
264
+ return
265
+ }
266
+ const count = this.authWireRefs.get(key) ?? 0
267
+ this.authWireRefs.set(key, count + 1)
268
+ if (count === 0) {
269
+ try {
270
+ await this.registerSub(key, state)
271
+ } catch (err) {
272
+ // Roll back the ref we just took so a later subscribe re-registers.
273
+ this.authWireRefs.delete(key)
274
+ throw err
275
+ }
276
+ }
277
+ }
278
+
279
+ /** Mirror of {@link acquireWire}: drop the wire sub on its last release. */
280
+ private releaseWire(key: string, state: SubState, shared: boolean): void {
281
+ if (shared) {
282
+ const remaining = (this.authWireRefs.get(key) ?? 1) - 1
283
+ if (remaining > 0) {
284
+ this.authWireRefs.set(key, remaining)
285
+ return
286
+ }
287
+ this.authWireRefs.delete(key)
288
+ }
289
+ this.unregisterSub(key)
290
+ this.rws.send(JSON.stringify({ op: 'unsubscribe', ...state.frame }))
291
+ }
292
+
293
+ protected async sendSubscribe(state: SubState): Promise<void> {
294
+ if (state.needsAuth && state.address !== undefined) {
295
+ await this.ensureLogin(state.address)
296
+ }
297
+ this.rws.send(JSON.stringify({ op: 'subscribe', ...state.frame }))
298
+ }
299
+
300
+ protected override onClose(): void {
301
+ this.contexts = {}
302
+ this.pendingFunding.clear()
303
+ this.loginPromise = undefined
304
+ }
305
+
306
+ /** Wire subs for `sub`, keyed uniquely for the base's replay registry. */
307
+ private resolveChannel(sub: Subscription): Array<[string, SubState]> {
308
+ switch (sub.channel) {
309
+ case 'marketsContext':
310
+ return [
311
+ [
312
+ 'markPricesPerps',
313
+ { frame: { channel: 'markPricesPerps' }, needsAuth: false },
314
+ ],
315
+ [
316
+ 'fundingRatesPerps',
317
+ { frame: { channel: 'fundingRatesPerps' }, needsAuth: false },
318
+ ],
319
+ ]
320
+ case 'marketContext':
321
+ return [
322
+ [
323
+ `markPricesPerps:${sub.marketId}`,
324
+ {
325
+ frame: { channel: 'markPricesPerps', markets: [sub.marketId] },
326
+ needsAuth: false,
327
+ },
328
+ ],
329
+ [
330
+ `fundingRatesPerps:${sub.marketId}`,
331
+ {
332
+ frame: { channel: 'fundingRatesPerps', markets: [sub.marketId] },
333
+ needsAuth: false,
334
+ },
335
+ ],
336
+ ]
337
+ case 'orderbook':
338
+ return [
339
+ [
340
+ `depthBooksPerps:${sub.marketId}`,
341
+ {
342
+ frame: { channel: 'depthBooksPerps', markets: [sub.marketId] },
343
+ needsAuth: false,
344
+ },
345
+ ],
346
+ ]
347
+ case 'trades':
348
+ return [
349
+ [
350
+ `tradesPerps:${sub.marketId}`,
351
+ {
352
+ frame: { channel: 'tradesPerps', markets: [sub.marketId] },
353
+ needsAuth: false,
354
+ },
355
+ ],
356
+ ]
357
+ case 'candle': {
358
+ const resolution = mapInterval(sub.interval)
359
+ return [
360
+ [
361
+ `kLinePerps:${sub.marketId}:${resolution}`,
362
+ {
363
+ frame: {
364
+ channel: 'kLinePerps',
365
+ markets: [sub.marketId],
366
+ resolution,
367
+ },
368
+ needsAuth: false,
369
+ },
370
+ ],
371
+ ]
372
+ }
373
+ case 'orderUpdates':
374
+ case 'fills':
375
+ case 'positions':
376
+ return [
377
+ [
378
+ ONDO_AUTH_CHANNEL[sub.channel],
379
+ {
380
+ frame: { channel: ONDO_AUTH_CHANNEL[sub.channel] },
381
+ needsAuth: true,
382
+ address: sub.address,
383
+ },
384
+ ],
385
+ ]
386
+ case 'accountSummary':
387
+ // Not a wire channel: derived from the positions feed (margin/uPnL)
388
+ // and the REST wallet balance, refreshed on fills.
389
+ return [
390
+ [
391
+ 'positionsPerps',
392
+ {
393
+ frame: { channel: 'positionsPerps' },
394
+ needsAuth: true,
395
+ address: sub.address,
396
+ },
397
+ ],
398
+ [
399
+ 'fillsPerps',
400
+ {
401
+ frame: { channel: 'fillsPerps' },
402
+ needsAuth: true,
403
+ address: sub.address,
404
+ },
405
+ ],
406
+ ]
407
+ case 'spotBalances':
408
+ return []
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Send the login op once per connection, before the first authenticated
414
+ * subscribe. Concurrent callers share one in-flight login; a failure
415
+ * clears the guard so a later subscribe (after a fresh SIWE login) retries.
416
+ */
417
+ private ensureLogin(address: Address): Promise<void> {
418
+ if (this.loginPromise === undefined) {
419
+ this.loginPromise = (async () => {
420
+ const token = await this.tokenStore.get(address)
421
+ if (token === null) {
422
+ throw new OndoSessionExpiredError(
423
+ `No Ondo session for ${address}. Run the SIWE login before subscribing to account channels.`
424
+ )
425
+ }
426
+ this.rws.send(
427
+ JSON.stringify({ op: 'login', args: { token: token.token } })
428
+ )
429
+ })()
430
+ this.loginPromise.catch(() => {
431
+ this.loginPromise = undefined
432
+ })
433
+ }
434
+ return this.loginPromise
435
+ }
436
+
437
+ private restClient(): OndoApiClient {
438
+ return new OndoApiClient(this.apiUrl, {
439
+ fetchImpl: this.client?.config.fetch,
440
+ })
441
+ }
442
+
443
+ /**
444
+ * Seed the account-summary inputs from the REST balance so the first emit is
445
+ * complete before any positions frame lands. Throws {@link
446
+ * OndoSessionExpiredError} when the SIWE session is missing, matching the
447
+ * other authenticated channels.
448
+ */
449
+ private async seedAccountSummary(address: Address): Promise<void> {
450
+ const token = await this.tokenStore.get(address)
451
+ if (token === null) {
452
+ throw new OndoSessionExpiredError(
453
+ `No Ondo session for ${address}. Run the SIWE login before subscribing to account channels.`
454
+ )
455
+ }
456
+ const balance = await this.restClient().get<OndoBalanceSummary>(
457
+ '/v1/perps/balance',
458
+ { authToken: token.token }
459
+ )
460
+ this.accountSummary = {
461
+ walletBalance: Number.parseFloat(balance.walletBalance) || 0,
462
+ marginUsed: Number.parseFloat(balance.usedMargin) || 0,
463
+ unrealizedPnl: Number.parseFloat(balance.unrealizedPnl) || 0,
464
+ }
465
+ this.emitAccountSummary()
466
+ }
467
+
468
+ /** Re-pull the wallet balance (moved by the fill) and re-emit the summary. */
469
+ private async refreshAccountSummaryBalance(address: Address): Promise<void> {
470
+ const token = await this.tokenStore.get(address)
471
+ if (token === null || this.accountSummary === undefined) {
472
+ return
473
+ }
474
+ const balance = await this.restClient().get<OndoBalanceSummary>(
475
+ '/v1/perps/balance',
476
+ { authToken: token.token }
477
+ )
478
+ if (this.accountSummary === undefined) {
479
+ return
480
+ }
481
+ this.accountSummary.walletBalance =
482
+ Number.parseFloat(balance.walletBalance) || 0
483
+ this.emitAccountSummary()
484
+ }
485
+
486
+ /**
487
+ * Emit the current account summary. Gross semantics: `walletBalance` is the
488
+ * collateral, the positions carry unrealized PnL, so it counts toward buying
489
+ * power (`availableMargin = walletBalance + unrealizedPnl − marginUsed`).
490
+ */
491
+ private emitAccountSummary(): void {
492
+ const summary = this.accountSummary
493
+ const address = this.accountAddress
494
+ if (summary === undefined || address === undefined) {
495
+ return
496
+ }
497
+ const portfolioValue = summary.walletBalance + summary.unrealizedPnl
498
+ this.emit(`accountSummary:${address}`, {
499
+ channel: 'accountSummary',
500
+ data: {
501
+ portfolioValue: portfolioValue.toString(),
502
+ availableMargin: (portfolioValue - summary.marginUsed).toString(),
503
+ marginUsed: summary.marginUsed.toString(),
504
+ unrealizedPnl: summary.unrealizedPnl.toString(),
505
+ },
506
+ })
507
+ }
508
+
509
+ protected handleMessage(raw: string): void {
510
+ let msg: OndoWsMessage
511
+ try {
512
+ msg = JSON.parse(raw) as OndoWsMessage
513
+ } catch {
514
+ wsLog.parseFailure(this.providerKey, raw)
515
+ return
516
+ }
517
+ if (msg.type === 'error') {
518
+ wsLog.serverError(this.providerKey, msg.msg ?? 'unknown error')
519
+ return
520
+ }
521
+ if (msg.type !== 'update') {
522
+ return
523
+ }
524
+ try {
525
+ switch (msg.channel) {
526
+ case 'depthBooksPerps':
527
+ this.handleBooks(msg.data as OndoBookSnapshot[])
528
+ break
529
+ case 'tradesPerps':
530
+ this.handleTrades(msg.data as OndoWsTrade[])
531
+ break
532
+ case 'kLinePerps':
533
+ this.handleKline(msg.data as OndoKline)
534
+ break
535
+ case 'markPricesPerps':
536
+ this.handleMarkPrices(msg.data as OndoMarkPrice[])
537
+ break
538
+ case 'fundingRatesPerps':
539
+ this.handleFundingRates(msg.data as OndoFundingRate[])
540
+ break
541
+ case 'ordersPerps':
542
+ this.handleOrders(msg.data as OndoOrder[])
543
+ break
544
+ case 'fillsPerps':
545
+ this.handleFills(msg.data as OndoFill[])
546
+ break
547
+ case 'positionsPerps':
548
+ this.handlePositions(msg.data as OndoPosition[])
549
+ break
550
+ }
551
+ } catch (err) {
552
+ wsLog.handlerFailure(this.providerKey, err)
553
+ }
554
+ }
555
+
556
+ private handleBooks(snapshots: OndoBookSnapshot[]): void {
557
+ for (const snap of snapshots) {
558
+ const toLevels = (levels: OndoBookSnapshot['bids'], direction: 1 | -1) =>
559
+ levels
560
+ .map(([price, size]) => ({ price, size, priceNum: Number(price) }))
561
+ .sort((a, b) => direction * (a.priceNum - b.priceNum))
562
+ .map(({ price, size }) => ({ price, size }))
563
+ this.emit(`orderbook:${snap.market}`, {
564
+ channel: 'orderbook',
565
+ data: {
566
+ provider: this.providerKey,
567
+ marketId: snap.market,
568
+ bids: toLevels(snap.bids, -1),
569
+ asks: toLevels(snap.asks, 1),
570
+ timestamp: Date.parse(snap.time),
571
+ },
572
+ })
573
+ }
574
+ }
575
+
576
+ private handleTrades(trades: OndoWsTrade[]): void {
577
+ const byMarket = new Map<string, OndoWsTrade[]>()
578
+ for (const trade of trades) {
579
+ const batch = byMarket.get(trade.market)
580
+ if (batch === undefined) {
581
+ byMarket.set(trade.market, [trade])
582
+ } else {
583
+ batch.push(trade)
584
+ }
585
+ }
586
+ for (const [market, batch] of byMarket) {
587
+ this.emit(`trades:${market}`, {
588
+ channel: 'trades',
589
+ data: batch.map((trade) => ({
590
+ provider: this.providerKey,
591
+ marketId: trade.market,
592
+ price: trade.price,
593
+ size: trade.size,
594
+ timestamp: Date.parse(trade.time),
595
+ side: trade.aggressor_side,
596
+ id: trade.id,
597
+ })),
598
+ })
599
+ }
600
+ }
601
+
602
+ private handleKline(kline: OndoKline): void {
603
+ const interval = intervalFromBarSpan(kline.e - kline.s)
604
+ if (interval === undefined) {
605
+ return
606
+ }
607
+ this.emit(`candle:${kline.m}:${interval}`, {
608
+ channel: 'candle',
609
+ data: {
610
+ t: kline.t * 1000,
611
+ o: String(kline.o),
612
+ h: String(kline.h),
613
+ l: String(kline.l),
614
+ c: String(kline.c),
615
+ v: String(kline.v),
616
+ },
617
+ })
618
+ }
619
+
620
+ private handleMarkPrices(prices: OndoMarkPrice[]): void {
621
+ for (const { market, markPrice } of prices) {
622
+ const context: MarketContext = {
623
+ ...this.contexts[market],
624
+ marketId: market,
625
+ // Ondo streams no mid price; the mark price stands in.
626
+ midPrice: markPrice,
627
+ markPrice,
628
+ }
629
+ const funding = this.pendingFunding.get(market)
630
+ if (funding !== undefined) {
631
+ context.funding = funding
632
+ this.pendingFunding.delete(market)
633
+ }
634
+ this.contexts[market] = context
635
+ }
636
+ this.emitContexts(prices.map((p) => p.market))
637
+ }
638
+
639
+ private handleFundingRates(rates: OndoFundingRate[]): void {
640
+ const touched: string[] = []
641
+ for (const { market, rate, intervalEnds } of rates) {
642
+ const funding = { rate, nextFundingTime: Date.parse(intervalEnds) }
643
+ const context = this.contexts[market]
644
+ if (context === undefined) {
645
+ // No mark price yet — a context without prices would violate the
646
+ // MarketContext contract, so hold the funding until one arrives.
647
+ this.pendingFunding.set(market, funding)
648
+ } else {
649
+ context.funding = funding
650
+ touched.push(market)
651
+ }
652
+ }
653
+ this.emitContexts(touched)
654
+ }
655
+
656
+ private emitContexts(markets: string[]): void {
657
+ this.emit('marketsContext', {
658
+ channel: 'marketsContext',
659
+ data: { ...this.contexts },
660
+ })
661
+ for (const market of markets) {
662
+ const context = this.contexts[market]
663
+ if (context !== undefined) {
664
+ this.emit(`marketContext:${market}`, {
665
+ channel: 'marketContext',
666
+ data: context,
667
+ })
668
+ }
669
+ }
670
+ }
671
+
672
+ /** Registry-backed market identity, or `undefined` off the tradable list. */
673
+ private resolveMarket = (market: string): MarketDisplay | undefined => {
674
+ const known = this.registry?.get(market)
675
+ return known && toMarketDisplay(known)
676
+ }
677
+
678
+ private handleOrders(orders: OndoOrder[]): void {
679
+ const address = this.accountAddress
680
+ if (address === undefined) {
681
+ return
682
+ }
683
+ this.emit(`orderUpdates:${address}`, {
684
+ channel: 'orderUpdates',
685
+ data: classifyAndMapOrders(orders, this.resolveMarket),
686
+ })
687
+ }
688
+
689
+ private handleFills(fills: OndoFill[]): void {
690
+ const address = this.accountAddress
691
+ if (address === undefined) {
692
+ return
693
+ }
694
+ const mapped = []
695
+ for (const fill of fills) {
696
+ const market = this.resolveMarket(fill.market)
697
+ if (market !== undefined) {
698
+ mapped.push(mapFill(fill, market))
699
+ }
700
+ }
701
+ this.emit(`fills:${address}`, { channel: 'fills', data: mapped })
702
+ if (this.accountSummary !== undefined) {
703
+ this.refreshAccountSummaryBalance(address as Address).catch((err) =>
704
+ wsLog.handlerFailure(this.providerKey, err)
705
+ )
706
+ }
707
+ }
708
+
709
+ private handlePositions(positions: OndoPosition[]): void {
710
+ const address = this.accountAddress
711
+ if (address === undefined) {
712
+ return
713
+ }
714
+ const known = positions.filter(
715
+ (position) => this.resolveMarket(position.market) !== undefined
716
+ )
717
+ this.emit(`positions:${address}`, {
718
+ channel: 'positions',
719
+ data: mapOpenPositions(
720
+ known,
721
+ (market) => this.resolveMarket(market) as MarketDisplay
722
+ ),
723
+ })
724
+ if (this.accountSummary !== undefined) {
725
+ let marginUsed = 0
726
+ let unrealizedPnl = 0
727
+ for (const position of positions) {
728
+ marginUsed += Number.parseFloat(position.usedMargin) || 0
729
+ unrealizedPnl += Number.parseFloat(position.unrealizedPnl) || 0
730
+ }
731
+ this.accountSummary.marginUsed = marginUsed
732
+ this.accountSummary.unrealizedPnl = unrealizedPnl
733
+ this.emitAccountSummary()
734
+ }
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Factory for registering the Ondo WS provider with `PerpsWsClient`:
740
+ * `new PerpsWsClient(client, { wsProviders: { ondo: ondoWsProvider() } })`.
741
+ *
742
+ * Closes over the per-instance options (storage, apiUrl override) so
743
+ * `PerpsWsClient` can call the returned factory with just
744
+ * `({ provider, wsUrl, client })` at subscribe time. `markets` is unused —
745
+ * Ondo advertises a single venue, no sub-DEX filtering.
746
+ *
747
+ * @public
748
+ */
749
+ export const ondoWsProvider =
750
+ (options?: OndoWsProviderOptions): WsProviderFactory =>
751
+ ({ provider, wsUrl, client }) =>
752
+ new OndoWsProvider(wsUrl, provider, options, client)