@lifi/perps-sdk-provider-ondo 27.0.0 → 29.0.0

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 (81) hide show
  1. package/dist/cjs/OndoProvider.d.ts.map +1 -1
  2. package/dist/cjs/OndoProvider.js +5 -2
  3. package/dist/cjs/OndoProvider.js.map +1 -1
  4. package/dist/cjs/auth/signActions.d.ts +3 -3
  5. package/dist/cjs/auth/signActions.d.ts.map +1 -1
  6. package/dist/cjs/auth/signActions.js +67 -46
  7. package/dist/cjs/auth/signActions.js.map +1 -1
  8. package/dist/cjs/types/ws.d.ts +4 -0
  9. package/dist/cjs/types/ws.d.ts.map +1 -1
  10. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  11. package/dist/cjs/utils/mapFill.js +1 -1
  12. package/dist/cjs/utils/mapFill.js.map +1 -1
  13. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  14. package/dist/cjs/utils/mapOrder.js +9 -9
  15. package/dist/cjs/utils/mapOrder.js.map +1 -1
  16. package/dist/cjs/utils/mapPortfolioHistory.d.ts.map +1 -1
  17. package/dist/cjs/utils/mapPortfolioHistory.js +3 -1
  18. package/dist/cjs/utils/mapPortfolioHistory.js.map +1 -1
  19. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  20. package/dist/cjs/utils/mapPosition.js +1 -1
  21. package/dist/cjs/utils/mapPosition.js.map +1 -1
  22. package/dist/cjs/utils/venueValues.d.ts +1 -1
  23. package/dist/cjs/utils/venueValues.d.ts.map +1 -1
  24. package/dist/cjs/utils/venueValues.js +5 -2
  25. package/dist/cjs/utils/venueValues.js.map +1 -1
  26. package/dist/cjs/websocket/OndoWsProvider.d.ts +3 -0
  27. package/dist/cjs/websocket/OndoWsProvider.d.ts.map +1 -1
  28. package/dist/cjs/websocket/OndoWsProvider.js +58 -16
  29. package/dist/cjs/websocket/OndoWsProvider.js.map +1 -1
  30. package/dist/esm/OndoProvider.d.ts.map +1 -1
  31. package/dist/esm/OndoProvider.js +10 -6
  32. package/dist/esm/OndoProvider.js.map +1 -1
  33. package/dist/esm/auth/signActions.d.ts +11 -6
  34. package/dist/esm/auth/signActions.d.ts.map +1 -1
  35. package/dist/esm/auth/signActions.js +79 -53
  36. package/dist/esm/auth/signActions.js.map +1 -1
  37. package/dist/esm/types/ws.d.ts +4 -0
  38. package/dist/esm/types/ws.d.ts.map +1 -1
  39. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  40. package/dist/esm/utils/mapFill.js +1 -1
  41. package/dist/esm/utils/mapFill.js.map +1 -1
  42. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  43. package/dist/esm/utils/mapOrder.js +9 -9
  44. package/dist/esm/utils/mapOrder.js.map +1 -1
  45. package/dist/esm/utils/mapPortfolioHistory.d.ts.map +1 -1
  46. package/dist/esm/utils/mapPortfolioHistory.js +3 -1
  47. package/dist/esm/utils/mapPortfolioHistory.js.map +1 -1
  48. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  49. package/dist/esm/utils/mapPosition.js +1 -1
  50. package/dist/esm/utils/mapPosition.js.map +1 -1
  51. package/dist/esm/utils/venueValues.d.ts +1 -1
  52. package/dist/esm/utils/venueValues.d.ts.map +1 -1
  53. package/dist/esm/utils/venueValues.js +5 -2
  54. package/dist/esm/utils/venueValues.js.map +1 -1
  55. package/dist/esm/websocket/OndoWsProvider.d.ts +5 -0
  56. package/dist/esm/websocket/OndoWsProvider.d.ts.map +1 -1
  57. package/dist/esm/websocket/OndoWsProvider.js +66 -21
  58. package/dist/esm/websocket/OndoWsProvider.js.map +1 -1
  59. package/dist/types/OndoProvider.d.ts.map +1 -1
  60. package/dist/types/auth/signActions.d.ts +11 -6
  61. package/dist/types/auth/signActions.d.ts.map +1 -1
  62. package/dist/types/types/ws.d.ts +4 -0
  63. package/dist/types/types/ws.d.ts.map +1 -1
  64. package/dist/types/utils/mapFill.d.ts.map +1 -1
  65. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  66. package/dist/types/utils/mapPortfolioHistory.d.ts.map +1 -1
  67. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  68. package/dist/types/utils/venueValues.d.ts +1 -1
  69. package/dist/types/utils/venueValues.d.ts.map +1 -1
  70. package/dist/types/websocket/OndoWsProvider.d.ts +5 -0
  71. package/dist/types/websocket/OndoWsProvider.d.ts.map +1 -1
  72. package/package.json +3 -3
  73. package/src/OndoProvider.ts +12 -6
  74. package/src/auth/signActions.ts +116 -81
  75. package/src/types/ws.ts +1 -0
  76. package/src/utils/mapFill.ts +6 -1
  77. package/src/utils/mapOrder.ts +16 -10
  78. package/src/utils/mapPortfolioHistory.ts +3 -7
  79. package/src/utils/mapPosition.ts +2 -1
  80. package/src/utils/venueValues.ts +6 -2
  81. package/src/websocket/OndoWsProvider.ts +93 -24
@@ -1,13 +1,23 @@
1
- import { PerpsError, type SignActionsContext } from '@lifi/perps-sdk'
1
+ import {
2
+ PerpsError,
3
+ rethrowWithKnownResults,
4
+ type SignActionsContext,
5
+ type SignActionsResult,
6
+ } from '@lifi/perps-sdk'
2
7
  import type {
8
+ ActionResult,
3
9
  ActionStep,
4
10
  HmacActionStep,
5
11
  HmacSignedActionStep,
6
12
  SessionActionStep,
7
- SignedActionStep,
8
13
  SiweActionStep,
9
14
  } from '@lifi/perps-types'
10
- import { ActionType, PerpsErrorCode, SigningMethod } from '@lifi/perps-types'
15
+ import {
16
+ ActionType,
17
+ PerpsErrorCode,
18
+ SigningMethod,
19
+ UserActionStatus,
20
+ } from '@lifi/perps-types'
11
21
  import { type Address, getAddress } from 'viem'
12
22
  import {
13
23
  ONDO_API_KEY_NAME,
@@ -249,12 +259,17 @@ async function executeSessionRequest(
249
259
  * `SIWE` signs the backend-built ERC-4361 challenge with the user's wallet and
250
260
  * completes the login directly against Ondo. The session JWT is persisted in
251
261
  * the token store and no signed step is returned, so neither the JWT nor the
252
- * wallet signature transits the LI.FI backend.
262
+ * wallet signature transits the LI.FI backend. Each completed login is an
263
+ * `Accepted` executed result.
253
264
  *
254
265
  * `SESSION` executes client-only setup steps directly against the venue with
255
266
  * the stored session token. A backend-authored request-bearing step is
256
- * dispatched as-is; a bare marker step is keyed on its action. Returns no
257
- * signed steps, so `executeAction` is skipped.
267
+ * dispatched as-is; a bare marker step is keyed on its action. Each step the
268
+ * venue answered is an `Accepted` executed result, and no signed step is
269
+ * returned, so `executeAction` is skipped.
270
+ *
271
+ * A step that throws after an earlier step executed throws a
272
+ * `PerpsExecutionError` that holds the earlier results.
258
273
  *
259
274
  * `HMAC` computes a per-request HMAC-SHA256 signature over each request step
260
275
  * from the client-held API key (creating one on first use), attaching the
@@ -269,29 +284,35 @@ export async function ondoSignActions(
269
284
  steps: ActionStep[],
270
285
  address: Address,
271
286
  ctx?: SignActionsContext
272
- ): Promise<SignedActionStep[]> {
287
+ ): Promise<SignActionsResult> {
273
288
  switch (method) {
274
289
  case SigningMethod.SIWE: {
275
290
  const userWallet = requireUserWallet(ctx)
276
- for (const step of steps) {
277
- if (!isSiweStep(step)) {
278
- throw new PerpsError(
279
- PerpsErrorCode.SDKError,
280
- `Ondo received a non-SIWE step ('${step.action}') under the SIWE signing method.`
281
- )
291
+ const executed: ActionResult[] = []
292
+ try {
293
+ for (const step of steps) {
294
+ if (!isSiweStep(step)) {
295
+ throw new PerpsError(
296
+ PerpsErrorCode.SDKError,
297
+ `Ondo received a non-SIWE step ('${step.action}') under the SIWE signing method.`
298
+ )
299
+ }
300
+ const token = await completeSiweLogin(deps.client, userWallet, {
301
+ id: step.siwe.challengeId,
302
+ message: step.siwe.message,
303
+ })
304
+ executed.push(acceptedResult(step))
305
+ await deps.tokenStore.set(address, token)
282
306
  }
283
- const token = await completeSiweLogin(deps.client, userWallet, {
284
- id: step.siwe.challengeId,
285
- message: step.siwe.message,
286
- })
287
- await deps.tokenStore.set(address, token)
307
+ } catch (error) {
308
+ rethrowWithKnownResults(executed, error)
288
309
  }
289
- return []
310
+ return { signed: [], executed }
290
311
  }
291
312
 
292
313
  case SigningMethod.HMAC: {
293
314
  const apiKey = await ensureApiKey(deps, address)
294
- return Promise.all(
315
+ const signed = await Promise.all(
295
316
  steps.map(async (step): Promise<HmacSignedActionStep> => {
296
317
  if (!hasRequest(step)) {
297
318
  throw new PerpsError(
@@ -318,72 +339,20 @@ export async function ondoSignActions(
318
339
  }
319
340
  })
320
341
  )
342
+ return { signed, executed: [] }
321
343
  }
322
344
 
323
345
  case SigningMethod.SESSION: {
324
- for (const step of steps) {
325
- if (hasRequest(step)) {
326
- await executeSessionRequest(deps, address, step.request)
327
- continue
328
- }
329
- if (!isSessionStep(step)) {
330
- throw new PerpsError(
331
- PerpsErrorCode.SDKError,
332
- `Ondo received a non-session step ('${step.action}') under the session signing method.`
333
- )
334
- }
335
- switch (step.action) {
336
- case ActionType.CREATE_DEPOSIT_ADDRESS: {
337
- const token = await requireOndoSessionToken(
338
- deps.tokenStore,
339
- address
340
- )
341
- const account = await deps.client.get<{ accountID?: unknown }>(
342
- '/v1/account',
343
- { authToken: token.token }
344
- )
345
- const payload = buildOndoProvisionPayload(
346
- step.session,
347
- typeof account.accountID === 'string' ? account.accountID : ''
348
- )
349
- await deps.client.post('/v1/provision_address', payload, {
350
- authToken: token.token,
351
- })
352
- // Re-query to validate that Ondo can read the provisioned address;
353
- // getAccount performs the authoritative state refresh afterward.
354
- await listOndoDepositAddress(deps.client, token.token)
355
- break
356
- }
357
-
358
- case ActionType.ACCEPT_PROVIDER_TERMS: {
359
- const token = await requireOndoSessionToken(
360
- deps.tokenStore,
361
- address
362
- )
363
- await deps.client.post(
364
- '/v1/agreement',
365
- {
366
- termsVersion: ONDO_TERMS_VERSION,
367
- privacyVersion: ONDO_PRIVACY_VERSION,
368
- },
369
- { authToken: token.token }
370
- )
371
- break
372
- }
373
- case ActionType.REGISTER_API_KEY:
374
- await ensureApiKey(deps, address)
375
- break
376
- case ActionType.ADD_WITHDRAWAL_ADDRESS:
377
- await addWithdrawalAddress(deps, address, ctx)
378
- break
379
- default:
380
- throw new PerpsError(
381
- PerpsErrorCode.SDKError,
382
- `Ondo has no session-step executor for action '${step.action}'.`
383
- )
346
+ const executed: ActionResult[] = []
347
+ try {
348
+ for (const step of steps) {
349
+ await executeSessionStep(deps, address, step, ctx)
350
+ executed.push(acceptedResult(step))
384
351
  }
352
+ } catch (error) {
353
+ rethrowWithKnownResults(executed, error)
385
354
  }
386
- return []
355
+ return { signed: [], executed }
387
356
  }
388
357
 
389
358
  default:
@@ -393,3 +362,69 @@ export async function ondoSignActions(
393
362
  )
394
363
  }
395
364
  }
365
+
366
+ function acceptedResult(step: ActionStep): ActionResult {
367
+ return { action: step.action, status: UserActionStatus.Accepted }
368
+ }
369
+
370
+ async function executeSessionStep(
371
+ deps: OndoSignActionsDeps,
372
+ address: Address,
373
+ step: ActionStep,
374
+ ctx: SignActionsContext | undefined
375
+ ): Promise<void> {
376
+ if (hasRequest(step)) {
377
+ await executeSessionRequest(deps, address, step.request)
378
+ return
379
+ }
380
+ if (!isSessionStep(step)) {
381
+ throw new PerpsError(
382
+ PerpsErrorCode.SDKError,
383
+ `Ondo received a non-session step ('${step.action}') under the session signing method.`
384
+ )
385
+ }
386
+ switch (step.action) {
387
+ case ActionType.CREATE_DEPOSIT_ADDRESS: {
388
+ const token = await requireOndoSessionToken(deps.tokenStore, address)
389
+ const account = await deps.client.get<{ accountID?: unknown }>(
390
+ '/v1/account',
391
+ { authToken: token.token }
392
+ )
393
+ const payload = buildOndoProvisionPayload(
394
+ step.session,
395
+ typeof account.accountID === 'string' ? account.accountID : ''
396
+ )
397
+ await deps.client.post('/v1/provision_address', payload, {
398
+ authToken: token.token,
399
+ })
400
+ // Re-query to validate that Ondo can read the provisioned address;
401
+ // getAccount performs the authoritative state refresh afterward.
402
+ await listOndoDepositAddress(deps.client, token.token)
403
+ break
404
+ }
405
+
406
+ case ActionType.ACCEPT_PROVIDER_TERMS: {
407
+ const token = await requireOndoSessionToken(deps.tokenStore, address)
408
+ await deps.client.post(
409
+ '/v1/agreement',
410
+ {
411
+ termsVersion: ONDO_TERMS_VERSION,
412
+ privacyVersion: ONDO_PRIVACY_VERSION,
413
+ },
414
+ { authToken: token.token }
415
+ )
416
+ break
417
+ }
418
+ case ActionType.REGISTER_API_KEY:
419
+ await ensureApiKey(deps, address)
420
+ break
421
+ case ActionType.ADD_WITHDRAWAL_ADDRESS:
422
+ await addWithdrawalAddress(deps, address, ctx)
423
+ break
424
+ default:
425
+ throw new PerpsError(
426
+ PerpsErrorCode.SDKError,
427
+ `Ondo has no session-step executor for action '${step.action}'.`
428
+ )
429
+ }
430
+ }
package/src/types/ws.ts CHANGED
@@ -65,6 +65,7 @@ export interface OndoFundingRate {
65
65
  /** Server frame envelope. Non-`update` types are acks or errors. @public */
66
66
  export type OndoWsMessage =
67
67
  | { type: 'update'; channel: 'depthBooksPerps'; data: OndoBookSnapshot[] }
68
+ | { type: 'update'; channel: 'topOfBooksPerps'; data: OndoBookSnapshot[] }
68
69
  | { type: 'update'; channel: 'tradesPerps'; data: OndoWsTrade[] }
69
70
  | { type: 'update'; channel: 'kLinePerps'; data: OndoKline }
70
71
  | { type: 'update'; channel: 'markPricesPerps'; data: OndoMarkPrice[] }
@@ -31,7 +31,12 @@ export const mapFill = (
31
31
  fill: OndoFill,
32
32
  market: MarketDisplay
33
33
  ): Fill | undefined => {
34
- const createdAt = rowTimestampToIsoStringOrWarn('fill', 'time', fill.time)
34
+ const createdAt = rowTimestampToIsoStringOrWarn(
35
+ 'fill',
36
+ 'time',
37
+ fill.time,
38
+ market.id
39
+ )
35
40
  if (createdAt === undefined) {
36
41
  return undefined
37
42
  }
@@ -24,19 +24,23 @@ import type { OndoOrder, OndoTwapOrder } from '../types/wire.js'
24
24
  import { rowTimestampToIsoStringOrWarn } from './venueValues.js'
25
25
 
26
26
  const skipOrder = (
27
+ marketId: string,
27
28
  field: string,
28
- value: unknown,
29
- expected?: string
29
+ value: unknown
30
30
  ): undefined => {
31
- warnSkippedVenueRow(ONDO_PROVIDER_KEY, 'order', field, value, expected)
31
+ warnSkippedVenueRow(ONDO_PROVIDER_KEY, 'order', field, value, { marketId })
32
32
  return undefined
33
33
  }
34
34
 
35
- const orderSizeOrSkip = (field: string, value: unknown): string | undefined => {
35
+ const orderSizeOrSkip = (
36
+ marketId: string,
37
+ field: string,
38
+ value: unknown
39
+ ): string | undefined => {
36
40
  try {
37
41
  return unknownToDecimalString(value, field, ONDO_PROVIDER_KEY)
38
42
  } catch {
39
- return skipOrder(field, value)
43
+ return skipOrder(marketId, field, value)
40
44
  }
41
45
  }
42
46
 
@@ -101,13 +105,13 @@ export const mapOrder = (
101
105
  parentOrderId?: string
102
106
  ): Order | undefined => {
103
107
  const twap = 'twapId' in order
104
- const filledSize = orderSizeOrSkip('filledSize', order.filledSize)
108
+ const filledSize = orderSizeOrSkip(market.id, 'filledSize', order.filledSize)
105
109
  if (filledSize === undefined) {
106
110
  return undefined
107
111
  }
108
112
  const sizeValue = twap
109
- ? orderSizeOrSkip('totalSize', order.totalSize)
110
- : orderSizeOrSkip('size', order.size)
113
+ ? orderSizeOrSkip(market.id, 'totalSize', order.totalSize)
114
+ : orderSizeOrSkip(market.id, 'size', order.size)
111
115
  if (sizeValue === undefined) {
112
116
  return undefined
113
117
  }
@@ -121,7 +125,8 @@ export const mapOrder = (
121
125
  const createdAt = rowTimestampToIsoStringOrWarn(
122
126
  'order',
123
127
  createdField,
124
- createdValue
128
+ createdValue,
129
+ market.id
125
130
  )
126
131
  if (createdAt === undefined) {
127
132
  return undefined
@@ -132,7 +137,8 @@ export const mapOrder = (
132
137
  const updatedAt = rowTimestampToIsoStringOrWarn(
133
138
  'order',
134
139
  'updatedAt',
135
- updatedValue
140
+ updatedValue,
141
+ market.id
136
142
  )
137
143
  if (updatedAt === undefined) {
138
144
  return undefined
@@ -33,13 +33,9 @@ const mapPoint = (
33
33
  ): PortfolioHistoryPoint | undefined => {
34
34
  const timestamp = Date.parse(point.time)
35
35
  if (Number.isNaN(timestamp)) {
36
- warnSkippedVenueRow(
37
- ONDO_PROVIDER_KEY,
38
- POINT_ROW,
39
- 'time',
40
- point.time,
41
- 'timestamp'
42
- )
36
+ warnSkippedVenueRow(ONDO_PROVIDER_KEY, POINT_ROW, 'time', point.time, {
37
+ expected: 'timestamp',
38
+ })
43
39
  return undefined
44
40
  }
45
41
  return { timestamp, accountValue: point.marginBalance, pnl: point.totalPnL }
@@ -31,7 +31,8 @@ export const mapPosition = (
31
31
  ONDO_PROVIDER_KEY,
32
32
  'position',
33
33
  'netQuantity',
34
- pos.netQuantity
34
+ pos.netQuantity,
35
+ { marketId: market.id }
35
36
  )
36
37
  return undefined
37
38
  }
@@ -8,12 +8,16 @@ import { ONDO_PROVIDER_KEY } from '../constants.js'
8
8
  export const rowTimestampToIsoStringOrWarn = (
9
9
  row: string,
10
10
  field: string,
11
- value: string
11
+ value: string,
12
+ marketId?: string
12
13
  ): string | undefined => {
13
14
  try {
14
15
  return timestampToIsoString(value)
15
16
  } catch {
16
- warnSkippedVenueRow(ONDO_PROVIDER_KEY, row, field, value, 'timestamp')
17
+ warnSkippedVenueRow(ONDO_PROVIDER_KEY, row, field, value, {
18
+ marketId,
19
+ expected: 'timestamp',
20
+ })
17
21
  return undefined
18
22
  }
19
23
  }
@@ -1,5 +1,7 @@
1
1
  import {
2
+ addDecimalString,
2
3
  DecodeChain,
4
+ divideDecimalString,
3
5
  getMarketRegistry,
4
6
  isDecimalString,
5
7
  localStorageAdapter,
@@ -40,6 +42,7 @@ import {
40
42
  import type {
41
43
  OndoAuthToken,
42
44
  OndoBalanceSummary,
45
+ OndoBookLevel,
43
46
  OndoBookSnapshot,
44
47
  OndoFill,
45
48
  OndoFundingRate,
@@ -62,9 +65,10 @@ import { intervalFromBarSpan, mapInterval } from '../utils/ohlcvInterval.js'
62
65
 
63
66
  // Public channels (per market, demuxed from the frame's `market` field):
64
67
  // orderbook → depthBooksPerps, trades → tradesPerps, candle → kLinePerps.
65
- // `marketsContext`/`marketContext` merge two wire channels — markPricesPerps
66
- // and fundingRatesPerps — into the aggregated context emit; Ondo publishes no
67
- // mid price on either, so `markPrice` stands in for `midPrice`.
68
+ // `marketsContext`/`marketContext` merge three wire channels — markPricesPerps,
69
+ // fundingRatesPerps and topOfBooksPerps — into the aggregated context emit.
70
+ // `midPrice` is the top-of-book mid; the mark price stands in until a
71
+ // two-sided book arrives for the market.
68
72
  // Authenticated channels (account-wide, no market filter):
69
73
  // orderUpdates → ordersPerps, fills → fillsPerps, positions → positionsPerps.
70
74
  //
@@ -166,6 +170,8 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
166
170
  string,
167
171
  { rate: string; nextFundingTime: number }
168
172
  >()
173
+ /** Latest two-sided top-of-book mid, keyed by venue market symbol. */
174
+ private bookMids = new Map<string, string>()
169
175
 
170
176
  /** The single address this connection's login is bound to. */
171
177
  private accountAddress: string | undefined
@@ -472,6 +478,7 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
472
478
  protected override onClose(): void {
473
479
  this.contexts = {}
474
480
  this.pendingFunding.clear()
481
+ this.bookMids.clear()
475
482
  this.loginPromise = undefined
476
483
  this.accountAddress = undefined
477
484
  this.authGeneration += 1
@@ -494,6 +501,10 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
494
501
  'fundingRatesPerps',
495
502
  { frame: { channel: 'fundingRatesPerps' }, needsAuth: false },
496
503
  ],
504
+ [
505
+ 'topOfBooksPerps',
506
+ { frame: { channel: 'topOfBooksPerps' }, needsAuth: false },
507
+ ],
497
508
  ]
498
509
  case 'marketContext':
499
510
  return [
@@ -511,6 +522,13 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
511
522
  needsAuth: false,
512
523
  },
513
524
  ],
525
+ [
526
+ `topOfBooksPerps:${sub.marketId}`,
527
+ {
528
+ frame: { channel: 'topOfBooksPerps', markets: [sub.marketId] },
529
+ needsAuth: false,
530
+ },
531
+ ],
514
532
  ]
515
533
  case 'orderbook':
516
534
  return [
@@ -758,6 +776,9 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
758
776
  case 'depthBooksPerps':
759
777
  this.handleBooks(msg.data as OndoBookSnapshot[])
760
778
  break
779
+ case 'topOfBooksPerps':
780
+ this.handleTopOfBooks(msg.data as OndoBookSnapshot[])
781
+ break
761
782
  case 'tradesPerps':
762
783
  this.handleTrades(msg.data as OndoWsTrade[])
763
784
  break
@@ -785,38 +806,86 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
785
806
  }
786
807
  }
787
808
 
809
+ /** Valid levels of one book side, best first (`-1` bids, `1` asks). */
810
+ private toBookLevels(
811
+ market: string,
812
+ levels: OndoBookLevel[],
813
+ direction: 1 | -1
814
+ ) {
815
+ return levels
816
+ .flatMap(([price, size]) => {
817
+ if (!isDecimalString(price)) {
818
+ wsLog.skippedRow(
819
+ this.providerKey,
820
+ BOOK_LEVEL_ROW,
821
+ 'price',
822
+ price,
823
+ market
824
+ )
825
+ return []
826
+ }
827
+ if (!isDecimalString(size)) {
828
+ wsLog.skippedRow(
829
+ this.providerKey,
830
+ BOOK_LEVEL_ROW,
831
+ 'size',
832
+ size,
833
+ market
834
+ )
835
+ return []
836
+ }
837
+ return [{ price, size }]
838
+ })
839
+ .sort(
840
+ (a, b) => direction * (safeCompareDecimalStrings(a.price, b.price) ?? 0)
841
+ )
842
+ }
843
+
788
844
  private handleBooks(snapshots: OndoBookSnapshot[]): void {
789
845
  for (const snap of snapshots) {
790
- const toLevels = (levels: OndoBookSnapshot['bids'], direction: 1 | -1) =>
791
- levels
792
- .flatMap(([price, size]) => {
793
- if (!isDecimalString(price)) {
794
- wsLog.skippedRow(this.providerKey, BOOK_LEVEL_ROW, 'price', price)
795
- return []
796
- }
797
- if (!isDecimalString(size)) {
798
- wsLog.skippedRow(this.providerKey, BOOK_LEVEL_ROW, 'size', size)
799
- return []
800
- }
801
- return [{ price, size }]
802
- })
803
- .sort(
804
- (a, b) =>
805
- direction * (safeCompareDecimalStrings(a.price, b.price) ?? 0)
806
- )
807
846
  this.emit(`orderbook:${snap.market}`, {
808
847
  channel: 'orderbook',
809
848
  data: {
810
849
  provider: this.providerKey,
811
850
  marketId: snap.market,
812
- bids: toLevels(snap.bids, -1),
813
- asks: toLevels(snap.asks, 1),
851
+ bids: this.toBookLevels(snap.market, snap.bids, -1),
852
+ asks: this.toBookLevels(snap.market, snap.asks, 1),
814
853
  timestamp: Date.parse(snap.time),
815
854
  },
816
855
  })
817
856
  }
818
857
  }
819
858
 
859
+ private handleTopOfBooks(snapshots: OndoBookSnapshot[]): void {
860
+ const touched: string[] = []
861
+ for (const { market, bids, asks } of snapshots) {
862
+ const bestBid = this.toBookLevels(market, bids, -1)[0]
863
+ const bestAsk = this.toBookLevels(market, asks, 1)[0]
864
+ if (bestBid === undefined || bestAsk === undefined) {
865
+ this.bookMids.delete(market)
866
+ } else {
867
+ this.bookMids.set(
868
+ market,
869
+ divideDecimalString(
870
+ addDecimalString(bestBid.price, bestAsk.price),
871
+ '2'
872
+ )
873
+ )
874
+ }
875
+ const context = this.contexts[market]
876
+ if (context !== undefined) {
877
+ this.contexts[market] = {
878
+ ...context,
879
+ midPrice: this.bookMids.get(market) ?? context.markPrice,
880
+ }
881
+ touched.push(market)
882
+ }
883
+ }
884
+ if (touched.length > 0) {
885
+ this.emitContexts(touched)
886
+ }
887
+ }
888
+
820
889
  private handleTrades(trades: OndoWsTrade[]): void {
821
890
  const byMarket = new Map<string, OndoWsTrade[]>()
822
891
  for (const trade of trades) {
@@ -866,8 +935,8 @@ export class OndoWsProvider extends WsProviderBase<SubState> {
866
935
  const context: MarketContext = {
867
936
  ...this.contexts[market],
868
937
  marketId: market,
869
- // Ondo streams no mid price; the mark price stands in.
870
- midPrice: markPrice,
938
+ // The mark price stands in until a two-sided top-of-book arrives.
939
+ midPrice: this.bookMids.get(market) ?? markPrice,
871
940
  markPrice,
872
941
  oraclePrice,
873
942
  }