@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.
- package/LICENSE +201 -0
- package/README.md +38 -0
- package/dist/cjs/OndoProvider.d.ts +8 -0
- package/dist/cjs/OndoProvider.d.ts.map +1 -0
- package/dist/cjs/OndoProvider.js +338 -0
- package/dist/cjs/OndoProvider.js.map +1 -0
- package/dist/cjs/accountConfig.d.ts +3 -0
- package/dist/cjs/accountConfig.d.ts.map +1 -0
- package/dist/cjs/accountConfig.js +59 -0
- package/dist/cjs/accountConfig.js.map +1 -0
- package/dist/cjs/accountSummary.d.ts +3 -0
- package/dist/cjs/accountSummary.d.ts.map +1 -0
- package/dist/cjs/accountSummary.js +8 -0
- package/dist/cjs/accountSummary.js.map +1 -0
- package/dist/cjs/auth/OndoTokenStore.d.ts +13 -0
- package/dist/cjs/auth/OndoTokenStore.d.ts.map +1 -0
- package/dist/cjs/auth/OndoTokenStore.js +53 -0
- package/dist/cjs/auth/OndoTokenStore.js.map +1 -0
- package/dist/cjs/auth/completeSiweLogin.d.ts +13 -0
- package/dist/cjs/auth/completeSiweLogin.d.ts.map +1 -0
- package/dist/cjs/auth/completeSiweLogin.js +15 -0
- package/dist/cjs/auth/completeSiweLogin.js.map +1 -0
- package/dist/cjs/auth/index.d.ts +4 -0
- package/dist/cjs/auth/index.d.ts.map +1 -0
- package/dist/cjs/auth/index.js +11 -0
- package/dist/cjs/auth/index.js.map +1 -0
- package/dist/cjs/auth/signActions.d.ts +13 -0
- package/dist/cjs/auth/signActions.d.ts.map +1 -0
- package/dist/cjs/auth/signActions.js +86 -0
- package/dist/cjs/auth/signActions.js.map +1 -0
- package/dist/cjs/constants.d.ts +9 -0
- package/dist/cjs/constants.d.ts.map +1 -0
- package/dist/cjs/constants.js +12 -0
- package/dist/cjs/constants.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/auth.d.ts +16 -0
- package/dist/cjs/types/auth.d.ts.map +1 -0
- package/dist/cjs/types/auth.js +3 -0
- package/dist/cjs/types/auth.js.map +1 -0
- package/dist/cjs/types/index.d.ts +4 -0
- package/dist/cjs/types/index.d.ts.map +1 -0
- package/dist/cjs/types/index.js +3 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/types/wire.d.ts +154 -0
- package/dist/cjs/types/wire.d.ts.map +1 -0
- package/dist/cjs/types/wire.js +3 -0
- package/dist/cjs/types/wire.js.map +1 -0
- package/dist/cjs/types/ws.d.ts +92 -0
- package/dist/cjs/types/ws.d.ts.map +1 -0
- package/dist/cjs/types/ws.js +3 -0
- package/dist/cjs/types/ws.js.map +1 -0
- package/dist/cjs/utils/activityCursor.d.ts +9 -0
- package/dist/cjs/utils/activityCursor.d.ts.map +1 -0
- package/dist/cjs/utils/activityCursor.js +79 -0
- package/dist/cjs/utils/activityCursor.js.map +1 -0
- package/dist/cjs/utils/apiClient.d.ts +43 -0
- package/dist/cjs/utils/apiClient.d.ts.map +1 -0
- package/dist/cjs/utils/apiClient.js +134 -0
- package/dist/cjs/utils/apiClient.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +11 -0
- package/dist/cjs/utils/index.d.ts.map +1 -0
- package/dist/cjs/utils/index.js +39 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/cjs/utils/liquidation.d.ts +4 -0
- package/dist/cjs/utils/liquidation.d.ts.map +1 -0
- package/dist/cjs/utils/liquidation.js +16 -0
- package/dist/cjs/utils/liquidation.js.map +1 -0
- package/dist/cjs/utils/mapActivity.d.ts +5 -0
- package/dist/cjs/utils/mapActivity.d.ts.map +1 -0
- package/dist/cjs/utils/mapActivity.js +34 -0
- package/dist/cjs/utils/mapActivity.js.map +1 -0
- package/dist/cjs/utils/mapFill.d.ts +4 -0
- package/dist/cjs/utils/mapFill.d.ts.map +1 -0
- package/dist/cjs/utils/mapFill.js +36 -0
- package/dist/cjs/utils/mapFill.js.map +1 -0
- package/dist/cjs/utils/mapOrder.d.ts +16 -0
- package/dist/cjs/utils/mapOrder.d.ts.map +1 -0
- package/dist/cjs/utils/mapOrder.js +135 -0
- package/dist/cjs/utils/mapOrder.js.map +1 -0
- package/dist/cjs/utils/mapPosition.d.ts +5 -0
- package/dist/cjs/utils/mapPosition.d.ts.map +1 -0
- package/dist/cjs/utils/mapPosition.js +26 -0
- package/dist/cjs/utils/mapPosition.js.map +1 -0
- package/dist/cjs/utils/ohlcvInterval.d.ts +4 -0
- package/dist/cjs/utils/ohlcvInterval.d.ts.map +1 -0
- package/dist/cjs/utils/ohlcvInterval.js +49 -0
- package/dist/cjs/utils/ohlcvInterval.js.map +1 -0
- package/dist/cjs/utils/ondoAsset.d.ts +3 -0
- package/dist/cjs/utils/ondoAsset.d.ts.map +1 -0
- package/dist/cjs/utils/ondoAsset.js +12 -0
- package/dist/cjs/utils/ondoAsset.js.map +1 -0
- package/dist/cjs/utils/orderFormatting.d.ts +4 -0
- package/dist/cjs/utils/orderFormatting.d.ts.map +1 -0
- package/dist/cjs/utils/orderFormatting.js +40 -0
- package/dist/cjs/utils/orderFormatting.js.map +1 -0
- package/dist/cjs/websocket/OndoWsProvider.d.ts +56 -0
- package/dist/cjs/websocket/OndoWsProvider.d.ts.map +1 -0
- package/dist/cjs/websocket/OndoWsProvider.js +515 -0
- package/dist/cjs/websocket/OndoWsProvider.js.map +1 -0
- package/dist/esm/OndoProvider.d.ts +39 -0
- package/dist/esm/OndoProvider.d.ts.map +1 -0
- package/dist/esm/OndoProvider.js +366 -0
- package/dist/esm/OndoProvider.js.map +1 -0
- package/dist/esm/accountConfig.d.ts +13 -0
- package/dist/esm/accountConfig.d.ts.map +1 -0
- package/dist/esm/accountConfig.js +77 -0
- package/dist/esm/accountConfig.js.map +1 -0
- package/dist/esm/accountSummary.d.ts +12 -0
- package/dist/esm/accountSummary.d.ts.map +1 -0
- package/dist/esm/accountSummary.js +14 -0
- package/dist/esm/accountSummary.js.map +1 -0
- package/dist/esm/auth/OndoTokenStore.d.ts +20 -0
- package/dist/esm/auth/OndoTokenStore.d.ts.map +1 -0
- package/dist/esm/auth/OndoTokenStore.js +61 -0
- package/dist/esm/auth/OndoTokenStore.js.map +1 -0
- package/dist/esm/auth/completeSiweLogin.d.ts +28 -0
- package/dist/esm/auth/completeSiweLogin.d.ts.map +1 -0
- package/dist/esm/auth/completeSiweLogin.js +21 -0
- package/dist/esm/auth/completeSiweLogin.js.map +1 -0
- package/dist/esm/auth/index.d.ts +4 -0
- package/dist/esm/auth/index.d.ts.map +1 -0
- package/dist/esm/auth/index.js +5 -0
- package/dist/esm/auth/index.js.map +1 -0
- package/dist/esm/auth/signActions.d.ts +35 -0
- package/dist/esm/auth/signActions.d.ts.map +1 -0
- package/dist/esm/auth/signActions.js +103 -0
- package/dist/esm/auth/signActions.js.map +1 -0
- package/dist/esm/constants.d.ts +35 -0
- package/dist/esm/constants.d.ts.map +1 -0
- package/dist/esm/constants.js +35 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/types/auth.d.ts +32 -0
- package/dist/esm/types/auth.d.ts.map +1 -0
- package/dist/esm/types/auth.js +2 -0
- package/dist/esm/types/auth.js.map +1 -0
- package/dist/esm/types/index.d.ts +4 -0
- package/dist/esm/types/index.d.ts.map +1 -0
- package/dist/esm/types/index.js +3 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/types/wire.d.ts +195 -0
- package/dist/esm/types/wire.d.ts.map +1 -0
- package/dist/esm/types/wire.js +7 -0
- package/dist/esm/types/wire.js.map +1 -0
- package/dist/esm/types/ws.d.ts +108 -0
- package/dist/esm/types/ws.d.ts.map +1 -0
- package/dist/esm/types/ws.js +7 -0
- package/dist/esm/types/ws.js.map +1 -0
- package/dist/esm/utils/activityCursor.d.ts +47 -0
- package/dist/esm/utils/activityCursor.d.ts.map +1 -0
- package/dist/esm/utils/activityCursor.js +93 -0
- package/dist/esm/utils/activityCursor.js.map +1 -0
- package/dist/esm/utils/apiClient.d.ts +90 -0
- package/dist/esm/utils/apiClient.d.ts.map +1 -0
- package/dist/esm/utils/apiClient.js +169 -0
- package/dist/esm/utils/apiClient.js.map +1 -0
- package/dist/esm/utils/index.d.ts +11 -0
- package/dist/esm/utils/index.d.ts.map +1 -0
- package/dist/esm/utils/index.js +12 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/esm/utils/liquidation.d.ts +20 -0
- package/dist/esm/utils/liquidation.d.ts.map +1 -0
- package/dist/esm/utils/liquidation.js +29 -0
- package/dist/esm/utils/liquidation.js.map +1 -0
- package/dist/esm/utils/mapActivity.d.ts +21 -0
- package/dist/esm/utils/mapActivity.d.ts.map +1 -0
- package/dist/esm/utils/mapActivity.js +45 -0
- package/dist/esm/utils/mapActivity.js.map +1 -0
- package/dist/esm/utils/mapFill.d.ts +12 -0
- package/dist/esm/utils/mapFill.d.ts.map +1 -0
- package/dist/esm/utils/mapFill.js +37 -0
- package/dist/esm/utils/mapFill.js.map +1 -0
- package/dist/esm/utils/mapOrder.d.ts +50 -0
- package/dist/esm/utils/mapOrder.d.ts.map +1 -0
- package/dist/esm/utils/mapOrder.js +155 -0
- package/dist/esm/utils/mapOrder.js.map +1 -0
- package/dist/esm/utils/mapPosition.d.ts +19 -0
- package/dist/esm/utils/mapPosition.d.ts.map +1 -0
- package/dist/esm/utils/mapPosition.js +32 -0
- package/dist/esm/utils/mapPosition.js.map +1 -0
- package/dist/esm/utils/ohlcvInterval.d.ts +11 -0
- package/dist/esm/utils/ohlcvInterval.d.ts.map +1 -0
- package/dist/esm/utils/ohlcvInterval.js +59 -0
- package/dist/esm/utils/ohlcvInterval.js.map +1 -0
- package/dist/esm/utils/ondoAsset.d.ts +4 -0
- package/dist/esm/utils/ondoAsset.d.ts.map +1 -0
- package/dist/esm/utils/ondoAsset.js +9 -0
- package/dist/esm/utils/ondoAsset.js.map +1 -0
- package/dist/esm/utils/orderFormatting.d.ts +33 -0
- package/dist/esm/utils/orderFormatting.d.ts.map +1 -0
- package/dist/esm/utils/orderFormatting.js +62 -0
- package/dist/esm/utils/orderFormatting.js.map +1 -0
- package/dist/esm/websocket/OndoWsProvider.d.ts +122 -0
- package/dist/esm/websocket/OndoWsProvider.d.ts.map +1 -0
- package/dist/esm/websocket/OndoWsProvider.js +591 -0
- package/dist/esm/websocket/OndoWsProvider.js.map +1 -0
- package/dist/types/OndoProvider.d.ts +39 -0
- package/dist/types/OndoProvider.d.ts.map +1 -0
- package/dist/types/accountConfig.d.ts +13 -0
- package/dist/types/accountConfig.d.ts.map +1 -0
- package/dist/types/accountSummary.d.ts +12 -0
- package/dist/types/accountSummary.d.ts.map +1 -0
- package/dist/types/auth/OndoTokenStore.d.ts +20 -0
- package/dist/types/auth/OndoTokenStore.d.ts.map +1 -0
- package/dist/types/auth/completeSiweLogin.d.ts +28 -0
- package/dist/types/auth/completeSiweLogin.d.ts.map +1 -0
- package/dist/types/auth/index.d.ts +4 -0
- package/dist/types/auth/index.d.ts.map +1 -0
- package/dist/types/auth/signActions.d.ts +35 -0
- package/dist/types/auth/signActions.d.ts.map +1 -0
- package/dist/types/constants.d.ts +35 -0
- package/dist/types/constants.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/types/auth.d.ts +32 -0
- package/dist/types/types/auth.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +4 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/types/wire.d.ts +195 -0
- package/dist/types/types/wire.d.ts.map +1 -0
- package/dist/types/types/ws.d.ts +108 -0
- package/dist/types/types/ws.d.ts.map +1 -0
- package/dist/types/utils/activityCursor.d.ts +47 -0
- package/dist/types/utils/activityCursor.d.ts.map +1 -0
- package/dist/types/utils/apiClient.d.ts +90 -0
- package/dist/types/utils/apiClient.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +11 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/liquidation.d.ts +20 -0
- package/dist/types/utils/liquidation.d.ts.map +1 -0
- package/dist/types/utils/mapActivity.d.ts +21 -0
- package/dist/types/utils/mapActivity.d.ts.map +1 -0
- package/dist/types/utils/mapFill.d.ts +12 -0
- package/dist/types/utils/mapFill.d.ts.map +1 -0
- package/dist/types/utils/mapOrder.d.ts +50 -0
- package/dist/types/utils/mapOrder.d.ts.map +1 -0
- package/dist/types/utils/mapPosition.d.ts +19 -0
- package/dist/types/utils/mapPosition.d.ts.map +1 -0
- package/dist/types/utils/ohlcvInterval.d.ts +11 -0
- package/dist/types/utils/ohlcvInterval.d.ts.map +1 -0
- package/dist/types/utils/ondoAsset.d.ts +4 -0
- package/dist/types/utils/ondoAsset.d.ts.map +1 -0
- package/dist/types/utils/orderFormatting.d.ts +33 -0
- package/dist/types/utils/orderFormatting.d.ts.map +1 -0
- package/dist/types/websocket/OndoWsProvider.d.ts +122 -0
- package/dist/types/websocket/OndoWsProvider.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/OndoProvider.ts +635 -0
- package/src/accountConfig.ts +105 -0
- package/src/accountSummary.ts +22 -0
- package/src/auth/OndoTokenStore.ts +83 -0
- package/src/auth/completeSiweLogin.ts +43 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/signActions.ts +160 -0
- package/src/constants.ts +38 -0
- package/src/index.ts +36 -0
- package/src/types/auth.ts +32 -0
- package/src/types/index.ts +5 -0
- package/src/types/wire.ts +228 -0
- package/src/types/ws.ts +79 -0
- package/src/utils/activityCursor.ts +140 -0
- package/src/utils/apiClient.ts +257 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/liquidation.ts +37 -0
- package/src/utils/mapActivity.ts +61 -0
- package/src/utils/mapFill.ts +47 -0
- package/src/utils/mapOrder.ts +201 -0
- package/src/utils/mapPosition.ts +44 -0
- package/src/utils/ohlcvInterval.ts +69 -0
- package/src/utils/ondoAsset.ts +10 -0
- package/src/utils/orderFormatting.ts +70 -0
- 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)
|