@lifi/perps-sdk 20.0.0 → 20.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +20 -0
  2. package/dist/cjs/index.d.ts +1 -0
  3. package/dist/cjs/index.d.ts.map +1 -1
  4. package/dist/cjs/index.js +7 -2
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/registry/assetRegistry.d.ts +2 -1
  7. package/dist/cjs/registry/assetRegistry.d.ts.map +1 -1
  8. package/dist/cjs/registry/assetRegistry.js +4 -2
  9. package/dist/cjs/registry/assetRegistry.js.map +1 -1
  10. package/dist/cjs/registry/marketRegistry.d.ts +2 -1
  11. package/dist/cjs/registry/marketRegistry.d.ts.map +1 -1
  12. package/dist/cjs/registry/marketRegistry.js +4 -2
  13. package/dist/cjs/registry/marketRegistry.js.map +1 -1
  14. package/dist/cjs/registry/referenceDataRegistry.d.ts +5 -1
  15. package/dist/cjs/registry/referenceDataRegistry.d.ts.map +1 -1
  16. package/dist/cjs/registry/referenceDataRegistry.js +65 -5
  17. package/dist/cjs/registry/referenceDataRegistry.js.map +1 -1
  18. package/dist/cjs/transport/request.d.ts +1 -0
  19. package/dist/cjs/transport/request.d.ts.map +1 -1
  20. package/dist/cjs/transport/request.js +6 -2
  21. package/dist/cjs/transport/request.js.map +1 -1
  22. package/dist/cjs/types/withdrawal.d.ts +5 -5
  23. package/dist/cjs/types/withdrawal.d.ts.map +1 -1
  24. package/dist/cjs/utils/decimal.d.ts +1 -0
  25. package/dist/cjs/utils/decimal.d.ts.map +1 -1
  26. package/dist/cjs/utils/decimal.js +4 -1
  27. package/dist/cjs/utils/decimal.js.map +1 -1
  28. package/dist/cjs/utils/orderEntry.d.ts +6 -0
  29. package/dist/cjs/utils/orderEntry.d.ts.map +1 -0
  30. package/dist/cjs/utils/orderEntry.js +20 -0
  31. package/dist/cjs/utils/orderEntry.js.map +1 -0
  32. package/dist/cjs/version.d.ts +1 -1
  33. package/dist/cjs/version.js +1 -1
  34. package/dist/cjs/websocket/WsProviderBase.d.ts.map +1 -1
  35. package/dist/cjs/websocket/WsProviderBase.js +11 -3
  36. package/dist/cjs/websocket/WsProviderBase.js.map +1 -1
  37. package/dist/esm/index.d.ts +1 -0
  38. package/dist/esm/index.d.ts.map +1 -1
  39. package/dist/esm/index.js +1 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/registry/assetRegistry.d.ts +2 -1
  42. package/dist/esm/registry/assetRegistry.d.ts.map +1 -1
  43. package/dist/esm/registry/assetRegistry.js +4 -2
  44. package/dist/esm/registry/assetRegistry.js.map +1 -1
  45. package/dist/esm/registry/marketRegistry.d.ts +2 -1
  46. package/dist/esm/registry/marketRegistry.d.ts.map +1 -1
  47. package/dist/esm/registry/marketRegistry.js +4 -2
  48. package/dist/esm/registry/marketRegistry.js.map +1 -1
  49. package/dist/esm/registry/referenceDataRegistry.d.ts +6 -6
  50. package/dist/esm/registry/referenceDataRegistry.d.ts.map +1 -1
  51. package/dist/esm/registry/referenceDataRegistry.js +68 -10
  52. package/dist/esm/registry/referenceDataRegistry.js.map +1 -1
  53. package/dist/esm/transport/request.d.ts +2 -0
  54. package/dist/esm/transport/request.d.ts.map +1 -1
  55. package/dist/esm/transport/request.js +6 -2
  56. package/dist/esm/transport/request.js.map +1 -1
  57. package/dist/esm/types/withdrawal.d.ts +5 -5
  58. package/dist/esm/types/withdrawal.d.ts.map +1 -1
  59. package/dist/esm/utils/decimal.d.ts +5 -0
  60. package/dist/esm/utils/decimal.d.ts.map +1 -1
  61. package/dist/esm/utils/decimal.js +7 -0
  62. package/dist/esm/utils/decimal.js.map +1 -1
  63. package/dist/esm/utils/orderEntry.d.ts +26 -0
  64. package/dist/esm/utils/orderEntry.d.ts.map +1 -0
  65. package/dist/esm/utils/orderEntry.js +34 -0
  66. package/dist/esm/utils/orderEntry.js.map +1 -0
  67. package/dist/esm/version.d.ts +1 -1
  68. package/dist/esm/version.js +1 -1
  69. package/dist/esm/websocket/WsProviderBase.d.ts.map +1 -1
  70. package/dist/esm/websocket/WsProviderBase.js +11 -3
  71. package/dist/esm/websocket/WsProviderBase.js.map +1 -1
  72. package/dist/types/index.d.ts +1 -0
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/dist/types/registry/assetRegistry.d.ts +2 -1
  75. package/dist/types/registry/assetRegistry.d.ts.map +1 -1
  76. package/dist/types/registry/marketRegistry.d.ts +2 -1
  77. package/dist/types/registry/marketRegistry.d.ts.map +1 -1
  78. package/dist/types/registry/referenceDataRegistry.d.ts +6 -6
  79. package/dist/types/registry/referenceDataRegistry.d.ts.map +1 -1
  80. package/dist/types/transport/request.d.ts +2 -0
  81. package/dist/types/transport/request.d.ts.map +1 -1
  82. package/dist/types/types/withdrawal.d.ts +5 -5
  83. package/dist/types/types/withdrawal.d.ts.map +1 -1
  84. package/dist/types/utils/decimal.d.ts +5 -0
  85. package/dist/types/utils/decimal.d.ts.map +1 -1
  86. package/dist/types/utils/orderEntry.d.ts +26 -0
  87. package/dist/types/utils/orderEntry.d.ts.map +1 -0
  88. package/dist/types/version.d.ts +1 -1
  89. package/dist/types/websocket/WsProviderBase.d.ts.map +1 -1
  90. package/package.json +2 -2
  91. package/src/index.ts +6 -0
  92. package/src/registry/assetRegistry.ts +7 -5
  93. package/src/registry/marketRegistry.ts +7 -5
  94. package/src/registry/referenceDataRegistry.ts +89 -11
  95. package/src/transport/request.ts +12 -2
  96. package/src/types/withdrawal.ts +10 -5
  97. package/src/utils/decimal.ts +8 -0
  98. package/src/utils/orderEntry.ts +38 -0
  99. package/src/version.ts +1 -1
  100. package/src/websocket/WsProviderBase.ts +11 -3
@@ -1,14 +1,64 @@
1
1
  import { PerpsErrorCode } from '@lifi/perps-types'
2
2
  import { PerpsError } from '../errors/PerpsError.js'
3
+ import type { RequestOptions } from '../transport/request.js'
3
4
  import type { PerpsSDKClient } from '../types/provider.js'
4
5
  import { createWarnOnce } from '../utils/warnOnce.js'
5
6
 
7
+ function freshUntil(
8
+ headers: Headers,
9
+ requestStartedAt: number,
10
+ responseReceivedAt: number
11
+ ): number {
12
+ const directives = (headers.get('cache-control') ?? '')
13
+ .toLowerCase()
14
+ .split(',')
15
+ .map((directive) => directive.trim())
16
+ if (
17
+ directives.some((directive) =>
18
+ /^(no-store|no-cache)(?:\s*=|$)/.test(directive)
19
+ ) ||
20
+ headers
21
+ .get('vary')
22
+ ?.split(',')
23
+ .some((name) => name.trim() === '*')
24
+ ) {
25
+ return 0
26
+ }
27
+ const maxAges = directives.filter((directive) =>
28
+ /^max-age(?:\s*=|$)/.test(directive)
29
+ )
30
+ const maxAge =
31
+ maxAges.length === 1
32
+ ? /^max-age\s*=\s*(?:"(\d+)"|(\d+))$/.exec(maxAges[0])
33
+ : null
34
+ if (!maxAge) {
35
+ return 0
36
+ }
37
+ const age = headers.get('age') ?? '0'
38
+ const date = headers.get('date')
39
+ const dateTime = date === null ? responseReceivedAt : Date.parse(date)
40
+ if (!/^\d+$/.test(age) || !Number.isFinite(dateTime)) {
41
+ return 0
42
+ }
43
+ const lifetime = Number(maxAge[1] ?? maxAge[2]) * 1000
44
+ const currentAge = Math.max(
45
+ responseReceivedAt - dateTime,
46
+ Number(age) * 1000 + Math.max(0, responseReceivedAt - requestStartedAt)
47
+ )
48
+ return Number.isFinite(lifetime)
49
+ ? responseReceivedAt + lifetime - currentAge
50
+ : 0
51
+ }
52
+
6
53
  /** Atomic provider reference indexes; HTTP cache headers govern freshness. @internal */
7
54
  export abstract class ReferenceDataRegistry<T> {
8
55
  private index = new Map<string, T>()
9
56
  private secondaryIndexes = new Map<string, Map<string, T>>()
10
57
  private current: readonly T[] = []
11
58
  private inflight: Promise<readonly T[]> | undefined
59
+ private inflightIdentity: string | undefined
60
+ private cacheIdentity: string | undefined
61
+ private freshUntil = 0
12
62
  private readonly warnOnce = createWarnOnce()
13
63
 
14
64
  protected constructor(
@@ -21,21 +71,37 @@ export abstract class ReferenceDataRegistry<T> {
21
71
  ) {}
22
72
 
23
73
  /** Fetch the provider's full list through the HTTP layer. */
24
- protected abstract fetchItems(): Promise<T[]>
74
+ protected abstract fetchItems(
75
+ onResponse: RequestOptions['onResponse']
76
+ ): Promise<T[]>
25
77
 
26
78
  /** The item's primary key — what {@link get} is keyed by. */
27
79
  protected abstract keyOf(item: T): string
28
80
 
29
- /**
30
- * Fetch the list through the HTTP cache and rebuild the index. Concurrent
31
- * callers share one in-flight fetch; a settled fetch is never reused, so
32
- * HTTP `cache-control` alone governs freshness.
33
- */
81
+ /** Sync reference indexes, reusing only explicitly fresh Node responses. */
34
82
  sync(): Promise<readonly T[]> {
35
- if (!this.inflight) {
36
- this.inflight = this.load().finally(() => {
37
- this.inflight = undefined
83
+ const { config } = this.client
84
+ // Hooks can change credentials or cache policy on every call.
85
+ if (config.requestInterceptor || config.fetch) {
86
+ return this.load()
87
+ }
88
+ const identity = JSON.stringify([
89
+ config.apiUrl,
90
+ config.apiKey,
91
+ config.integrator,
92
+ ])
93
+ if (identity === this.cacheIdentity && Date.now() < this.freshUntil) {
94
+ return Promise.resolve(this.current)
95
+ }
96
+ if (!this.inflight || this.inflightIdentity !== identity) {
97
+ const inflight = this.load(identity).finally(() => {
98
+ if (this.inflight === inflight) {
99
+ this.inflight = undefined
100
+ this.inflightIdentity = undefined
101
+ }
38
102
  })
103
+ this.inflight = inflight
104
+ this.inflightIdentity = identity
39
105
  }
40
106
  return this.inflight
41
107
  }
@@ -59,8 +125,18 @@ export abstract class ReferenceDataRegistry<T> {
59
125
  return this.secondaryIndexes.get(key)?.get(id)
60
126
  }
61
127
 
62
- private async load(): Promise<readonly T[]> {
63
- const items = await this.fetchItems()
128
+ private async load(identity?: string): Promise<readonly T[]> {
129
+ let expiresAt = 0
130
+ const items = await this.fetchItems((headers, startedAt, receivedAt) => {
131
+ // Browsers retain their native HTTP cache and its resident response age.
132
+ if (
133
+ identity !== undefined &&
134
+ typeof process !== 'undefined' &&
135
+ process.versions?.node
136
+ ) {
137
+ expiresAt = freshUntil(headers, startedAt, receivedAt)
138
+ }
139
+ })
64
140
  const index = new Map<string, T>()
65
141
  for (const item of items) {
66
142
  const key = this.keyOf(item)
@@ -93,6 +169,8 @@ export abstract class ReferenceDataRegistry<T> {
93
169
  this.index = index
94
170
  this.secondaryIndexes = secondaryIndexes
95
171
  this.current = items
172
+ this.cacheIdentity = identity
173
+ this.freshUntil = expiresAt
96
174
  return items
97
175
  }
98
176
  }
@@ -20,6 +20,12 @@ import {
20
20
  export interface RequestOptions extends RequestInit {
21
21
  /** Per-call retry override. Falls back to the resolved client-level policy. */
22
22
  retry?: RetryPolicy | false
23
+ /** Successful response metadata, delivered only after JSON decoding. */
24
+ onResponse?: (
25
+ headers: Headers,
26
+ requestStartedAt: number,
27
+ responseReceivedAt: number
28
+ ) => void
23
29
  }
24
30
 
25
31
  /**
@@ -34,7 +40,7 @@ export async function request<T>(
34
40
  options: RequestOptions = {},
35
41
  sdkOptions?: SDKRequestOptions
36
42
  ): Promise<T> {
37
- const { retry, ...fetchInit } = options
43
+ const { retry, onResponse, ...fetchInit } = options
38
44
  const policy =
39
45
  retry !== undefined
40
46
  ? resolveRetryPolicy(LIFI_RETRY_DEFAULTS, retry, LIFI_REQUEST_KEY)
@@ -64,11 +70,13 @@ export async function request<T>(
64
70
  }
65
71
 
66
72
  try {
73
+ const requestStartedAt = onResponse ? Date.now() : 0
67
74
  const response = await fetchWithRetry(url, finalOptions, {
68
75
  policy,
69
76
  fetchImpl: config.fetch,
70
77
  signal: sdkOptions?.signal ?? finalOptions.signal ?? undefined,
71
78
  })
79
+ const responseReceivedAt = onResponse ? Date.now() : 0
72
80
 
73
81
  if (!response.ok) {
74
82
  let body: PerpsErrorBody | undefined
@@ -94,7 +102,9 @@ export async function request<T>(
94
102
  throw error
95
103
  }
96
104
 
97
- return (await response.json()) as T
105
+ const data = (await response.json()) as T
106
+ onResponse?.(response.headers, requestStartedAt, responseReceivedAt)
107
+ return data
98
108
  } catch (error) {
99
109
  if (error instanceof PerpsError) {
100
110
  throw error
@@ -1,4 +1,9 @@
1
- import type { ActionType, Asset, WithdrawalRoute } from '@lifi/perps-types'
1
+ import type {
2
+ ActionType,
3
+ Asset,
4
+ DecimalString,
5
+ WithdrawalRoute,
6
+ } from '@lifi/perps-types'
2
7
  import type { Address } from 'viem'
3
8
 
4
9
  export type { WithdrawalRoute }
@@ -14,13 +19,13 @@ export interface ProviderWithdrawableBalance {
14
19
  assetId: string
15
20
  route: WithdrawalRoute
16
21
  /** Withdrawable amount in the asset's own units. Always greater than zero. */
17
- available: string
22
+ available: DecimalString
18
23
  /**
19
24
  * Flat venue fee on a withdrawal from this row, in the asset's own units.
20
25
  * Absent when the venue publishes no fee for the asset. The client rejects
21
26
  * a value that is not a non-negative decimal.
22
27
  */
23
- withdrawalFee?: string
28
+ withdrawalFee?: DecimalString
24
29
  /**
25
30
  * `true` when the venue takes `withdrawalFee` out of the requested amount,
26
31
  * `false` when it charges the fee in addition. Set together with
@@ -40,7 +45,7 @@ export interface WithdrawableBalance {
40
45
  asset: Asset
41
46
  route: WithdrawalRoute
42
47
  /** Withdrawable amount in the asset's own units. */
43
- available: string
48
+ available: DecimalString
44
49
  /**
45
50
  * Flat venue fee on a withdrawal from this row, in the asset's own units.
46
51
  * Absent when the venue publishes no fee for the asset. With
@@ -51,7 +56,7 @@ export interface WithdrawableBalance {
51
56
  * is zero or negative when the fee is at or above `available`, and the row
52
57
  * then funds no withdrawal.
53
58
  */
54
- withdrawalFee?: string
59
+ withdrawalFee?: DecimalString
55
60
  /**
56
61
  * Present only together with `withdrawalFee`. Absent on a row from a
57
62
  * provider plugin that does not report it; absence means unknown, not
@@ -7,6 +7,14 @@ import Big from 'big.js'
7
7
  export const DivBig = Big()
8
8
  DivBig.DP = 40
9
9
 
10
+ /**
11
+ * As `DivBig`, but truncating, so a derived amount never rounds up past the
12
+ * exact quotient.
13
+ */
14
+ export const TruncBig = Big()
15
+ TruncBig.DP = 40
16
+ TruncBig.RM = Big.roundDown
17
+
10
18
  /** True when every value is a finite number, so `new DivBig(value)` cannot throw. */
11
19
  export function areFinite(...values: number[]): boolean {
12
20
  return values.every(Number.isFinite)
@@ -0,0 +1,38 @@
1
+ import type Big from 'big.js'
2
+ import { TruncBig } from './decimal.js'
3
+
4
+ /**
5
+ * Base size the given margin buys at `leverage`: size = margin × leverage ÷ price.
6
+ * Truncates, so `size × price` never exceeds `margin × leverage`.
7
+ * @public
8
+ */
9
+ export function sizeFromMargin(margin: Big, leverage: number, price: Big): Big {
10
+ return new TruncBig(margin).times(leverage).div(price)
11
+ }
12
+
13
+ /**
14
+ * Margin that holds the given base size at `leverage`: margin = size × price ÷ leverage.
15
+ * Truncates, so `margin × leverage` never exceeds `size × price`.
16
+ * @public
17
+ */
18
+ export function marginFromSize(size: Big, leverage: number, price: Big): Big {
19
+ return new TruncBig(size).times(price).div(leverage)
20
+ }
21
+
22
+ /**
23
+ * Base size for a notional: size = notional ÷ price.
24
+ * Truncates, so `size × price` never exceeds the notional.
25
+ * @public
26
+ */
27
+ export function sizeFromNotional(notionalUsd: Big, price: Big): Big {
28
+ return new TruncBig(notionalUsd).div(price)
29
+ }
30
+
31
+ /**
32
+ * Margin for a notional at `leverage`: margin = notional ÷ leverage.
33
+ * Truncates, so `margin × leverage` never exceeds the notional.
34
+ * @public
35
+ */
36
+ export function marginFromNotional(notionalUsd: Big, leverage: number): Big {
37
+ return new TruncBig(notionalUsd).div(leverage)
38
+ }
package/src/version.ts CHANGED
@@ -10,4 +10,4 @@ export const name = '@lifi/perps-sdk'
10
10
  *
11
11
  * @public
12
12
  */
13
- export const version = '20.0.0'
13
+ export const version = '20.1.1'
@@ -199,7 +199,9 @@ export abstract class WsProviderBase<TSub = unknown> implements WsProvider {
199
199
  try {
200
200
  await this.sendSubscribe(state)
201
201
  } catch (err) {
202
- this.wireSubs.delete(key)
202
+ if (this.wireSubs.get(key) === state) {
203
+ this.wireSubs.delete(key)
204
+ }
203
205
  throw err
204
206
  }
205
207
  }
@@ -304,7 +306,11 @@ export abstract class WsProviderBase<TSub = unknown> implements WsProvider {
304
306
  try {
305
307
  const teardown = await opening
306
308
  const current = this.channels.get(key)
307
- if (current === undefined || current.listeners.size === 0) {
309
+ if (current !== entry) {
310
+ teardown()
311
+ return
312
+ }
313
+ if (current.listeners.size === 0) {
308
314
  // Every listener released while opening — unwind immediately.
309
315
  teardown()
310
316
  this.channels.delete(key)
@@ -315,7 +321,9 @@ export abstract class WsProviderBase<TSub = unknown> implements WsProvider {
315
321
  } catch (err) {
316
322
  // Open failed: drop the channel so all (concurrent) subscribers reject
317
323
  // cleanly and a later subscribe re-opens.
318
- this.channels.delete(key)
324
+ if (this.channels.get(key) === entry) {
325
+ this.channels.delete(key)
326
+ }
319
327
  throw err
320
328
  }
321
329
  }