@lifi/perps-sdk 19.0.0 → 20.1.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 (209) hide show
  1. package/README.md +35 -5
  2. package/dist/cjs/client/PerpsClient.d.ts +1 -1
  3. package/dist/cjs/client/PerpsClient.d.ts.map +1 -1
  4. package/dist/cjs/client/PerpsClient.js +4 -1
  5. package/dist/cjs/client/PerpsClient.js.map +1 -1
  6. package/dist/cjs/client/createPerpsClient.d.ts +1 -6
  7. package/dist/cjs/client/createPerpsClient.d.ts.map +1 -1
  8. package/dist/cjs/client/createPerpsClient.js +1 -14
  9. package/dist/cjs/client/createPerpsClient.js.map +1 -1
  10. package/dist/cjs/index.d.ts +6 -3
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +15 -3
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/registry/assetRegistry.d.ts +2 -1
  15. package/dist/cjs/registry/assetRegistry.d.ts.map +1 -1
  16. package/dist/cjs/registry/assetRegistry.js +4 -2
  17. package/dist/cjs/registry/assetRegistry.js.map +1 -1
  18. package/dist/cjs/registry/marketRegistry.d.ts +2 -1
  19. package/dist/cjs/registry/marketRegistry.d.ts.map +1 -1
  20. package/dist/cjs/registry/marketRegistry.js +4 -2
  21. package/dist/cjs/registry/marketRegistry.js.map +1 -1
  22. package/dist/cjs/registry/referenceDataRegistry.d.ts +5 -1
  23. package/dist/cjs/registry/referenceDataRegistry.d.ts.map +1 -1
  24. package/dist/cjs/registry/referenceDataRegistry.js +65 -5
  25. package/dist/cjs/registry/referenceDataRegistry.js.map +1 -1
  26. package/dist/cjs/services/getOhlcv.d.ts.map +1 -1
  27. package/dist/cjs/services/getOhlcv.js.map +1 -1
  28. package/dist/cjs/transport/request.d.ts +1 -0
  29. package/dist/cjs/transport/request.d.ts.map +1 -1
  30. package/dist/cjs/transport/request.js +6 -2
  31. package/dist/cjs/transport/request.js.map +1 -1
  32. package/dist/cjs/types/api.d.ts +3 -4
  33. package/dist/cjs/types/api.d.ts.map +1 -1
  34. package/dist/cjs/types/config.d.ts +0 -9
  35. package/dist/cjs/types/config.d.ts.map +1 -1
  36. package/dist/cjs/types/provider.d.ts +1 -1
  37. package/dist/cjs/types/provider.d.ts.map +1 -1
  38. package/dist/cjs/types/withdrawal.d.ts +4 -2
  39. package/dist/cjs/types/withdrawal.d.ts.map +1 -1
  40. package/dist/cjs/utils/calculations.d.ts.map +1 -1
  41. package/dist/cjs/utils/calculations.js +107 -38
  42. package/dist/cjs/utils/calculations.js.map +1 -1
  43. package/dist/cjs/utils/decimal.d.ts +5 -0
  44. package/dist/cjs/utils/decimal.d.ts.map +1 -0
  45. package/dist/cjs/utils/decimal.js +17 -0
  46. package/dist/cjs/utils/decimal.js.map +1 -0
  47. package/dist/cjs/utils/math.d.ts +4 -0
  48. package/dist/cjs/utils/math.d.ts.map +1 -0
  49. package/dist/cjs/utils/math.js +8 -0
  50. package/dist/cjs/utils/math.js.map +1 -0
  51. package/dist/cjs/utils/orderEntry.d.ts +6 -0
  52. package/dist/cjs/utils/orderEntry.d.ts.map +1 -0
  53. package/dist/cjs/utils/orderEntry.js +20 -0
  54. package/dist/cjs/utils/orderEntry.js.map +1 -0
  55. package/dist/cjs/utils/parse.d.ts +1 -0
  56. package/dist/cjs/utils/parse.d.ts.map +1 -1
  57. package/dist/cjs/utils/parse.js +15 -0
  58. package/dist/cjs/utils/parse.js.map +1 -1
  59. package/dist/cjs/utils/positionMath.d.ts +5 -0
  60. package/dist/cjs/utils/positionMath.d.ts.map +1 -1
  61. package/dist/cjs/utils/positionMath.js +53 -6
  62. package/dist/cjs/utils/positionMath.js.map +1 -1
  63. package/dist/cjs/version.d.ts +1 -1
  64. package/dist/cjs/version.js +1 -1
  65. package/dist/cjs/websocket/PerpsWsClient.d.ts +0 -1
  66. package/dist/cjs/websocket/PerpsWsClient.d.ts.map +1 -1
  67. package/dist/cjs/websocket/PerpsWsClient.js +0 -7
  68. package/dist/cjs/websocket/PerpsWsClient.js.map +1 -1
  69. package/dist/cjs/websocket/WsProviderBase.d.ts.map +1 -1
  70. package/dist/cjs/websocket/WsProviderBase.js +11 -3
  71. package/dist/cjs/websocket/WsProviderBase.js.map +1 -1
  72. package/dist/esm/client/PerpsClient.d.ts +8 -7
  73. package/dist/esm/client/PerpsClient.d.ts.map +1 -1
  74. package/dist/esm/client/PerpsClient.js +11 -7
  75. package/dist/esm/client/PerpsClient.js.map +1 -1
  76. package/dist/esm/client/createPerpsClient.d.ts +1 -12
  77. package/dist/esm/client/createPerpsClient.d.ts.map +1 -1
  78. package/dist/esm/client/createPerpsClient.js +1 -19
  79. package/dist/esm/client/createPerpsClient.js.map +1 -1
  80. package/dist/esm/client/requireProvider.d.ts +1 -1
  81. package/dist/esm/client/requireProvider.js +1 -1
  82. package/dist/esm/index.d.ts +6 -3
  83. package/dist/esm/index.d.ts.map +1 -1
  84. package/dist/esm/index.js +5 -2
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/registry/assetRegistry.d.ts +2 -1
  87. package/dist/esm/registry/assetRegistry.d.ts.map +1 -1
  88. package/dist/esm/registry/assetRegistry.js +4 -2
  89. package/dist/esm/registry/assetRegistry.js.map +1 -1
  90. package/dist/esm/registry/marketRegistry.d.ts +2 -1
  91. package/dist/esm/registry/marketRegistry.d.ts.map +1 -1
  92. package/dist/esm/registry/marketRegistry.js +4 -2
  93. package/dist/esm/registry/marketRegistry.js.map +1 -1
  94. package/dist/esm/registry/referenceDataRegistry.d.ts +6 -6
  95. package/dist/esm/registry/referenceDataRegistry.d.ts.map +1 -1
  96. package/dist/esm/registry/referenceDataRegistry.js +68 -10
  97. package/dist/esm/registry/referenceDataRegistry.js.map +1 -1
  98. package/dist/esm/services/getOhlcv.d.ts +1 -0
  99. package/dist/esm/services/getOhlcv.d.ts.map +1 -1
  100. package/dist/esm/services/getOhlcv.js.map +1 -1
  101. package/dist/esm/transport/request.d.ts +2 -0
  102. package/dist/esm/transport/request.d.ts.map +1 -1
  103. package/dist/esm/transport/request.js +6 -2
  104. package/dist/esm/transport/request.js.map +1 -1
  105. package/dist/esm/types/api.d.ts +8 -22
  106. package/dist/esm/types/api.d.ts.map +1 -1
  107. package/dist/esm/types/config.d.ts +0 -27
  108. package/dist/esm/types/config.d.ts.map +1 -1
  109. package/dist/esm/types/provider.d.ts +7 -6
  110. package/dist/esm/types/provider.d.ts.map +1 -1
  111. package/dist/esm/types/withdrawal.d.ts +24 -15
  112. package/dist/esm/types/withdrawal.d.ts.map +1 -1
  113. package/dist/esm/utils/calculations.d.ts +3 -2
  114. package/dist/esm/utils/calculations.d.ts.map +1 -1
  115. package/dist/esm/utils/calculations.js +109 -39
  116. package/dist/esm/utils/calculations.js.map +1 -1
  117. package/dist/esm/utils/decimal.d.ts +14 -0
  118. package/dist/esm/utils/decimal.d.ts.map +1 -0
  119. package/dist/esm/utils/decimal.js +19 -0
  120. package/dist/esm/utils/decimal.js.map +1 -0
  121. package/dist/esm/utils/math.d.ts +6 -0
  122. package/dist/esm/utils/math.d.ts.map +1 -0
  123. package/dist/esm/utils/math.js +5 -0
  124. package/dist/esm/utils/math.js.map +1 -0
  125. package/dist/esm/utils/orderEntry.d.ts +26 -0
  126. package/dist/esm/utils/orderEntry.d.ts.map +1 -0
  127. package/dist/esm/utils/orderEntry.js +34 -0
  128. package/dist/esm/utils/orderEntry.js.map +1 -0
  129. package/dist/esm/utils/parse.d.ts +7 -0
  130. package/dist/esm/utils/parse.d.ts.map +1 -1
  131. package/dist/esm/utils/parse.js +20 -0
  132. package/dist/esm/utils/parse.js.map +1 -1
  133. package/dist/esm/utils/positionMath.d.ts +13 -0
  134. package/dist/esm/utils/positionMath.d.ts.map +1 -1
  135. package/dist/esm/utils/positionMath.js +60 -6
  136. package/dist/esm/utils/positionMath.js.map +1 -1
  137. package/dist/esm/version.d.ts +1 -1
  138. package/dist/esm/version.js +1 -1
  139. package/dist/esm/websocket/PerpsWsClient.d.ts +0 -7
  140. package/dist/esm/websocket/PerpsWsClient.d.ts.map +1 -1
  141. package/dist/esm/websocket/PerpsWsClient.js +0 -7
  142. package/dist/esm/websocket/PerpsWsClient.js.map +1 -1
  143. package/dist/esm/websocket/WsProviderBase.d.ts.map +1 -1
  144. package/dist/esm/websocket/WsProviderBase.js +11 -3
  145. package/dist/esm/websocket/WsProviderBase.js.map +1 -1
  146. package/dist/types/client/PerpsClient.d.ts +8 -7
  147. package/dist/types/client/PerpsClient.d.ts.map +1 -1
  148. package/dist/types/client/createPerpsClient.d.ts +1 -12
  149. package/dist/types/client/createPerpsClient.d.ts.map +1 -1
  150. package/dist/types/client/requireProvider.d.ts +1 -1
  151. package/dist/types/index.d.ts +6 -3
  152. package/dist/types/index.d.ts.map +1 -1
  153. package/dist/types/registry/assetRegistry.d.ts +2 -1
  154. package/dist/types/registry/assetRegistry.d.ts.map +1 -1
  155. package/dist/types/registry/marketRegistry.d.ts +2 -1
  156. package/dist/types/registry/marketRegistry.d.ts.map +1 -1
  157. package/dist/types/registry/referenceDataRegistry.d.ts +6 -6
  158. package/dist/types/registry/referenceDataRegistry.d.ts.map +1 -1
  159. package/dist/types/services/getOhlcv.d.ts +1 -0
  160. package/dist/types/services/getOhlcv.d.ts.map +1 -1
  161. package/dist/types/transport/request.d.ts +2 -0
  162. package/dist/types/transport/request.d.ts.map +1 -1
  163. package/dist/types/types/api.d.ts +8 -22
  164. package/dist/types/types/api.d.ts.map +1 -1
  165. package/dist/types/types/config.d.ts +0 -27
  166. package/dist/types/types/config.d.ts.map +1 -1
  167. package/dist/types/types/provider.d.ts +7 -6
  168. package/dist/types/types/provider.d.ts.map +1 -1
  169. package/dist/types/types/withdrawal.d.ts +24 -15
  170. package/dist/types/types/withdrawal.d.ts.map +1 -1
  171. package/dist/types/utils/calculations.d.ts +3 -2
  172. package/dist/types/utils/calculations.d.ts.map +1 -1
  173. package/dist/types/utils/decimal.d.ts +14 -0
  174. package/dist/types/utils/decimal.d.ts.map +1 -0
  175. package/dist/types/utils/math.d.ts +6 -0
  176. package/dist/types/utils/math.d.ts.map +1 -0
  177. package/dist/types/utils/orderEntry.d.ts +26 -0
  178. package/dist/types/utils/orderEntry.d.ts.map +1 -0
  179. package/dist/types/utils/parse.d.ts +7 -0
  180. package/dist/types/utils/parse.d.ts.map +1 -1
  181. package/dist/types/utils/positionMath.d.ts +13 -0
  182. package/dist/types/utils/positionMath.d.ts.map +1 -1
  183. package/dist/types/version.d.ts +1 -1
  184. package/dist/types/websocket/PerpsWsClient.d.ts +0 -7
  185. package/dist/types/websocket/PerpsWsClient.d.ts.map +1 -1
  186. package/dist/types/websocket/WsProviderBase.d.ts.map +1 -1
  187. package/package.json +2 -2
  188. package/src/client/PerpsClient.ts +12 -8
  189. package/src/client/createPerpsClient.ts +4 -32
  190. package/src/client/requireProvider.ts +1 -1
  191. package/src/index.ts +10 -4
  192. package/src/registry/assetRegistry.ts +7 -5
  193. package/src/registry/marketRegistry.ts +7 -5
  194. package/src/registry/referenceDataRegistry.ts +89 -11
  195. package/src/services/getOhlcv.ts +1 -0
  196. package/src/transport/request.ts +12 -2
  197. package/src/types/api.ts +8 -26
  198. package/src/types/config.ts +0 -30
  199. package/src/types/provider.ts +8 -7
  200. package/src/types/withdrawal.ts +24 -15
  201. package/src/utils/calculations.ts +114 -43
  202. package/src/utils/decimal.ts +21 -0
  203. package/src/utils/math.ts +7 -0
  204. package/src/utils/orderEntry.ts +38 -0
  205. package/src/utils/parse.ts +25 -0
  206. package/src/utils/positionMath.ts +66 -6
  207. package/src/version.ts +1 -1
  208. package/src/websocket/PerpsWsClient.ts +0 -19
  209. 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
  }
@@ -12,6 +12,7 @@ export interface GetOhlcvParams {
12
12
  provider: string
13
13
  /** Opaque provider `Market.id`, not a display symbol. */
14
14
  marketId: string
15
+ /** Pick from the provider's `Provider.supportedIntervals`; support varies per provider. */
15
16
  interval: OhlcvInterval
16
17
  /** Unix timestamp in milliseconds for the start bound. */
17
18
  startTime?: number
@@ -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
package/src/types/api.ts CHANGED
@@ -24,11 +24,7 @@ import type {
24
24
  } from '@lifi/perps-types'
25
25
  import type { Account, Address, WalletClient } from 'viem'
26
26
  import type { RetryConfig } from '../transport/retryPolicy.js'
27
- import type {
28
- ProviderConfigs,
29
- RequestInterceptor,
30
- SwitchChainHook,
31
- } from './config.js'
27
+ import type { RequestInterceptor, SwitchChainHook } from './config.js'
32
28
  import type { PerpsProviderPlugin } from './provider.js'
33
29
 
34
30
  /**
@@ -52,25 +48,15 @@ export interface PerpsConfig {
52
48
  apiKey?: string
53
49
  /** Perps API base URL; defaults to {@link DEFAULT_API_URL}. */
54
50
  apiUrl?: string
55
- /** Skip the SDK-version compatibility check when set. */
56
- disableVersionCheck?: boolean
57
51
  /** Hook that can rewrite URL/request options before transport. */
58
52
  requestInterceptor?: RequestInterceptor
59
53
  /**
60
- * Provider plugins or per-provider config. Two shapes are accepted:
61
- *
62
- * - `PerpsProviderPlugin[]` — plugin objects implementing the read surface
63
- * for one DEX each. Bound to the client at construction and looked up at
64
- * runtime as bound {@link PerpsProvider}s via `client.getProvider(key)`.
65
- * Modelled on `@lifi/sdk`'s `providers: SDKProvider[]`.
66
- * - `ProviderConfigs` — keyed config object (e.g.
67
- * `{ hyperliquid: { markets: [...] } }`). Used internally by
68
- * `PerpsWsClient` to filter which markets are subscribed to.
69
- *
70
- * Both may be supplied during the migration to provider packages;
71
- * the array form is preferred for new code.
54
+ * Provider plugins, one per DEX. Bound to the client at construction and
55
+ * looked up at runtime as bound {@link PerpsProvider}s via
56
+ * `client.getProvider(key)`. Modelled on `@lifi/sdk`'s
57
+ * `providers: SDKProvider[]`.
72
58
  */
73
- providers?: PerpsProviderPlugin[] | ProviderConfigs
59
+ providers?: PerpsProviderPlugin[]
74
60
  /**
75
61
  * The end-user's wallet, used whenever an action's descriptor names
76
62
  * `PerpsSigner.USER` as its `signer`. Accepts any viem-compatible WalletClient:
@@ -117,12 +103,8 @@ export interface PerpsClientOptions {
117
103
  apiKey?: string
118
104
  /** Perps API base URL; defaults to {@link DEFAULT_API_URL}. */
119
105
  apiUrl?: string
120
- /**
121
- * Provider plugins or per-provider config. Accepts the same two shapes as
122
- * {@link PerpsConfig.providers}: an array of `PerpsProviderPlugin` plugins or
123
- * a keyed `ProviderConfigs` map.
124
- */
125
- providers?: PerpsProviderPlugin[] | ProviderConfigs
106
+ /** Provider plugins; see {@link PerpsConfig.providers}. */
107
+ providers?: PerpsProviderPlugin[]
126
108
  /**
127
109
  * Hook invoked before a USER-signed EIP-712 action is signed, to switch the
128
110
  * user's wallet to the action's target chain. Also settable at runtime via
@@ -23,32 +23,6 @@ export type SwitchChainHook = (
23
23
  chainId: number
24
24
  ) => Promise<PerpsClientSigner | undefined>
25
25
 
26
- /**
27
- * Per-provider config — restricts which `markets` the WS client subscribes
28
- * to. Indexed by provider key.
29
- *
30
- * @public
31
- */
32
- export interface ProviderConfig {
33
- /** Optional provider category/market ids used to filter WS subscriptions. */
34
- markets?: string[]
35
- }
36
-
37
- /**
38
- * @deprecated Use {@link ProviderConfig}.
39
- * @public
40
- */
41
- export type HyperliquidConfig = ProviderConfig
42
-
43
- /**
44
- * Map of per-provider {@link ProviderConfig}, keyed by provider key.
45
- *
46
- * @public
47
- */
48
- export interface ProviderConfigs {
49
- [provider: string]: ProviderConfig | undefined
50
- }
51
-
52
26
  /**
53
27
  * Hook to rewrite each outgoing request's URL/`RequestInit` before it is sent.
54
28
  *
@@ -101,12 +75,8 @@ export interface PerpsBaseConfig {
101
75
  apiKey: string
102
76
  /** Resolved perps API base URL. */
103
77
  apiUrl: string
104
- /** Whether SDK version compatibility checks are disabled. */
105
- disableVersionCheck?: boolean
106
78
  /** Optional outgoing-request URL/options interceptor. */
107
79
  requestInterceptor?: RequestInterceptor
108
- /** Optional per-provider WS market filters. */
109
- providers?: ProviderConfigs
110
80
  /** Optional global or per-provider retry configuration. */
111
81
  retry?: RetryConfig
112
82
  /** Fetch implementation used by core HTTP transport. */
@@ -453,16 +453,17 @@ export interface PerpsProviderPlugin {
453
453
  ): Promise<PositionsResponse>
454
454
 
455
455
  /**
456
- * The user's current venue-side settings for one market — the margin mode
457
- * and leverage the next order on it will use. Optional because venues
458
- * expose this unevenly: Hyperliquid reads it directly (`activeAssetData`),
459
- * Lighter only reports it on an account's position row. `undefined` means
460
- * the venue has nothing to read for this market.
456
+ * The user's current venue-side settings for one perps market — the margin
457
+ * mode and leverage the next order on it will use. Resolves the venue value
458
+ * or throws.
459
+ *
460
+ * @throws {PerpsError} `ValidationError` for a spot market; any other
461
+ * `PerpsError` when the venue read fails or carries no usable value.
461
462
  */
462
- getMarketSettings?(
463
+ getMarketSettings(
463
464
  params: ProviderGetMarketSettingsParams,
464
465
  options?: SDKRequestOptions
465
- ): Promise<MarketSettings | undefined>
466
+ ): Promise<MarketSettings>
466
467
 
467
468
  /**
468
469
  * The amounts the account can still buy and sell on one market, in that
@@ -1,15 +1,7 @@
1
- import type { ActionType, Asset } from '@lifi/perps-types'
1
+ import type { ActionType, Asset, WithdrawalRoute } from '@lifi/perps-types'
2
2
  import type { Address } from 'viem'
3
3
 
4
- /**
5
- * Which of a venue's two balance routes a withdrawal is drawn from. Lighter's
6
- * own vocabulary, carried in the signed tx as `AssetRouteType` (`perps` = 0,
7
- * `spot` = 1); an account's mode changes what the two balances mean, never
8
- * which route the tx names.
9
- *
10
- * @public
11
- */
12
- export type WithdrawalRoute = 'perps' | 'spot'
4
+ export type { WithdrawalRoute }
13
5
 
14
6
  /**
15
7
  * One withdrawable `(asset, route)` selection as the provider reports it, keyed
@@ -25,10 +17,16 @@ export interface ProviderWithdrawableBalance {
25
17
  available: string
26
18
  /**
27
19
  * Flat venue fee on a withdrawal from this row, in the asset's own units.
28
- * Absent when the venue publishes no fee for the asset. The client rejects a
29
- * value that is not a non-negative decimal.
20
+ * Absent when the venue publishes no fee for the asset. The client rejects
21
+ * a value that is not a non-negative decimal.
30
22
  */
31
23
  withdrawalFee?: string
24
+ /**
25
+ * `true` when the venue takes `withdrawalFee` out of the requested amount,
26
+ * `false` when it charges the fee in addition. Set together with
27
+ * `withdrawalFee`; the client drops it from a row without a fee.
28
+ */
29
+ isFeeDeducted?: boolean
32
30
  }
33
31
 
34
32
  /**
@@ -44,11 +42,22 @@ export interface WithdrawableBalance {
44
42
  /** Withdrawable amount in the asset's own units. */
45
43
  available: string
46
44
  /**
47
- * Flat venue fee on a withdrawal from this row, in the asset's own units. An
48
- * amount at or below it delivers nothing. Absent when the venue publishes no
49
- * fee for the asset.
45
+ * Flat venue fee on a withdrawal from this row, in the asset's own units.
46
+ * Absent when the venue publishes no fee for the asset. With
47
+ * `isFeeDeducted` `true`, the venue takes the fee out of the requested
48
+ * amount, and an amount at or below the fee delivers nothing. With `false`,
49
+ * the venue charges the fee in addition to the requested amount, so the
50
+ * largest amount the row can fund is `available` minus the fee. That result
51
+ * is zero or negative when the fee is at or above `available`, and the row
52
+ * then funds no withdrawal.
50
53
  */
51
54
  withdrawalFee?: string
55
+ /**
56
+ * Present only together with `withdrawalFee`. Absent on a row from a
57
+ * provider plugin that does not report it; absence means unknown, not
58
+ * `true`.
59
+ */
60
+ isFeeDeducted?: boolean
52
61
  }
53
62
 
54
63
  /**
@@ -16,7 +16,9 @@ import {
16
16
  type QuoteSide,
17
17
  type TradeType,
18
18
  } from '@lifi/perps-types'
19
+ import type Big from 'big.js'
19
20
  import { PerpsError } from '../errors/PerpsError.js'
21
+ import { areFinite, DivBig } from './decimal.js'
20
22
 
21
23
  /**
22
24
  * Calculate position size in asset units from margin.
@@ -36,7 +38,13 @@ export function calculatePositionSize(
36
38
  leverage: number,
37
39
  price: number
38
40
  ): number {
39
- return (marginUsd * leverage) / price
41
+ if (price === 0) {
42
+ return (marginUsd * leverage) / price
43
+ }
44
+ if (!areFinite(marginUsd, leverage, price)) {
45
+ return Number.NaN
46
+ }
47
+ return new DivBig(marginUsd).times(leverage).div(price).toNumber()
40
48
  }
41
49
 
42
50
  /**
@@ -48,7 +56,10 @@ export function calculatePositionSize(
48
56
  * @public
49
57
  */
50
58
  export function calculateNotionalValue(size: number, price: number): number {
51
- return Math.abs(size) * price
59
+ if (!areFinite(size, price)) {
60
+ return Number.NaN
61
+ }
62
+ return new DivBig(size).abs().times(price).toNumber()
52
63
  }
53
64
 
54
65
  /**
@@ -65,7 +76,10 @@ export function calculateUnrealizedPnl(
65
76
  currentPrice: number,
66
77
  size: number
67
78
  ): number {
68
- return (currentPrice - entryPrice) * size
79
+ if (!areFinite(entryPrice, currentPrice, size)) {
80
+ return Number.NaN
81
+ }
82
+ return new DivBig(currentPrice).minus(entryPrice).times(size).toNumber()
69
83
  }
70
84
 
71
85
  /**
@@ -80,7 +94,10 @@ export function calculateRoe(pnl: number, margin: number): number {
80
94
  if (margin === 0) {
81
95
  return 0
82
96
  }
83
- return (pnl / margin) * 100
97
+ if (!areFinite(pnl, margin)) {
98
+ return Number.NaN
99
+ }
100
+ return new DivBig(pnl).div(margin).times(100).toNumber()
84
101
  }
85
102
 
86
103
  /**
@@ -95,7 +112,13 @@ export function calculateRequiredMargin(
95
112
  notionalValue: number,
96
113
  leverage: number
97
114
  ): number {
98
- return notionalValue / leverage
115
+ if (leverage === 0) {
116
+ return notionalValue / leverage
117
+ }
118
+ if (!areFinite(notionalValue, leverage)) {
119
+ return Number.NaN
120
+ }
121
+ return new DivBig(notionalValue).div(leverage).toNumber()
99
122
  }
100
123
 
101
124
  /**
@@ -107,7 +130,10 @@ export function calculateRequiredMargin(
107
130
  * @public
108
131
  */
109
132
  export function estimateFees(sizeUsd: number, feeRate: number): number {
110
- return sizeUsd * feeRate
133
+ if (!areFinite(sizeUsd, feeRate)) {
134
+ return Number.NaN
135
+ }
136
+ return new DivBig(sizeUsd).times(feeRate).toNumber()
111
137
  }
112
138
 
113
139
  /**
@@ -124,8 +150,17 @@ export function applySlippage(
124
150
  slippagePercent: number,
125
151
  isBuy: boolean
126
152
  ): number {
127
- const multiplier = 1 + slippagePercent / 100
128
- return isBuy ? price * multiplier : price / multiplier
153
+ if (!areFinite(price, slippagePercent)) {
154
+ return Number.NaN
155
+ }
156
+ const multiplier = new DivBig(slippagePercent).div(100).plus(1)
157
+ if (isBuy) {
158
+ return new DivBig(price).times(multiplier).toNumber()
159
+ }
160
+ if (multiplier.eq(0)) {
161
+ return price / multiplier.toNumber()
162
+ }
163
+ return new DivBig(price).div(multiplier).toNumber()
129
164
  }
130
165
 
131
166
  /**
@@ -166,7 +201,10 @@ export function effectiveLeverage(params: {
166
201
  if (marginUsd === 0) {
167
202
  return 0
168
203
  }
169
- return positionValueUsd / marginUsd
204
+ if (!areFinite(positionValueUsd, marginUsd)) {
205
+ return Number.NaN
206
+ }
207
+ return new DivBig(positionValueUsd).div(marginUsd).toNumber()
170
208
  }
171
209
 
172
210
  /**
@@ -202,12 +240,15 @@ export function calculateExpectedPnl(
202
240
  if (!triggerPrice || entryPrice === 0 || margin === 0) {
203
241
  return null
204
242
  }
243
+ if (!areFinite(triggerPrice, entryPrice, leverage, margin)) {
244
+ return { amount: Number.NaN, percent: Number.NaN }
245
+ }
205
246
  const priceDiff = isLong
206
- ? triggerPrice - entryPrice
207
- : entryPrice - triggerPrice
208
- const percent = (priceDiff / entryPrice) * leverage * 100
209
- const amount = margin * (percent / 100)
210
- return { amount, percent }
247
+ ? new DivBig(triggerPrice).minus(entryPrice)
248
+ : new DivBig(entryPrice).minus(triggerPrice)
249
+ const percent = priceDiff.div(entryPrice).times(leverage).times(100)
250
+ const amount = percent.times(margin).div(100)
251
+ return { amount: amount.toNumber(), percent: percent.toNumber() }
211
252
  }
212
253
 
213
254
  /**
@@ -228,8 +269,14 @@ export function priceFromPercent(
228
269
  if (entryPrice === 0 || leverage === 0) {
229
270
  return 0
230
271
  }
231
- const priceDelta = (percent / 100 / leverage) * entryPrice
232
- return isLong ? entryPrice + priceDelta : entryPrice - priceDelta
272
+ if (!areFinite(percent, entryPrice, leverage)) {
273
+ return Number.NaN
274
+ }
275
+ const priceDelta = new DivBig(percent)
276
+ .times(entryPrice)
277
+ .div(new DivBig(leverage).times(100))
278
+ const entry = new DivBig(entryPrice)
279
+ return (isLong ? entry.plus(priceDelta) : entry.minus(priceDelta)).toNumber()
233
280
  }
234
281
 
235
282
  /**
@@ -250,8 +297,13 @@ export function percentFromPrice(
250
297
  if (entryPrice === 0 || leverage === 0) {
251
298
  return 0
252
299
  }
253
- const priceDiff = isLong ? price - entryPrice : entryPrice - price
254
- return (priceDiff / entryPrice) * leverage * 100
300
+ if (!areFinite(price, entryPrice, leverage)) {
301
+ return Number.NaN
302
+ }
303
+ const priceDiff = isLong
304
+ ? new DivBig(price).minus(entryPrice)
305
+ : new DivBig(entryPrice).minus(price)
306
+ return priceDiff.div(entryPrice).times(leverage).times(100).toNumber()
255
307
  }
256
308
 
257
309
  /**
@@ -268,11 +320,14 @@ export function calculateRealizedPnlPercent(
268
320
  size: number,
269
321
  price: number
270
322
  ): number {
271
- const positionValue = Math.abs(size) * price
272
- if (positionValue === 0) {
323
+ if (!areFinite(realizedPnl, size, price)) {
324
+ return Number.NaN
325
+ }
326
+ const positionValue = new DivBig(size).abs().times(price)
327
+ if (positionValue.eq(0)) {
273
328
  return 0
274
329
  }
275
- return (realizedPnl / positionValue) * 100
330
+ return new DivBig(realizedPnl).div(positionValue).times(100).toNumber()
276
331
  }
277
332
 
278
333
  /** Result of {@link walkOrderbook} — the fill obtained for a USD-notional walk. */
@@ -283,7 +338,7 @@ interface BookWalk {
283
338
  filledNotional: number
284
339
  /** Volume-weighted average fill price, or 0 when the book is empty. */
285
340
  vwap: number
286
- /** True when the levels could not absorb the requested notional. */
341
+ /** True when the levels could not absorb the requested notional, and for a non-finite notional. */
287
342
  insufficientLiquidity: boolean
288
343
  }
289
344
 
@@ -293,7 +348,8 @@ interface BookWalk {
293
348
  * in array order — the caller passes asks for a buy and bids for a sell, each
294
349
  * already ordered best-price-first. When the book cannot absorb the full
295
350
  * notional, the walk stops at the last level and flags `insufficientLiquidity`,
296
- * returning the best obtainable fill.
351
+ * returning the best obtainable fill. A non-finite `sizeUsd` gives NaN fill
352
+ * fields with `insufficientLiquidity` set.
297
353
  *
298
354
  * @throws {PerpsError} `ValidationError` when a level's `price` or `size`
299
355
  * does not parse to a finite number.
@@ -303,32 +359,47 @@ export function walkOrderbook(
303
359
  levels: OrderbookLevel[],
304
360
  sizeUsd: number
305
361
  ): BookWalk {
306
- let remaining = sizeUsd
307
- let baseSize = 0
308
- let filledNotional = 0
362
+ if (!Number.isFinite(sizeUsd)) {
363
+ return {
364
+ baseSize: Number.NaN,
365
+ filledNotional: Number.NaN,
366
+ vwap: Number.NaN,
367
+ insufficientLiquidity: true,
368
+ }
369
+ }
370
+ let remaining = new DivBig(sizeUsd)
371
+ let baseSize = new DivBig(0)
372
+ let filledNotional = new DivBig(0)
309
373
  for (const level of levels) {
310
- if (remaining <= 0) {
374
+ if (remaining.lte(0)) {
311
375
  break
312
376
  }
313
- const price = Number.parseFloat(level.price)
314
- const size = Number.parseFloat(level.size)
315
- if (!Number.isFinite(price) || !Number.isFinite(size)) {
316
- throw new PerpsError(
317
- PerpsErrorCode.ValidationError,
318
- `Malformed orderbook level: price='${level.price}', size='${level.size}'`
319
- )
377
+ const { price, size } = parseLevel(level)
378
+ const levelNotional = size.times(price)
379
+ const take = remaining.lt(levelNotional) ? remaining : levelNotional
380
+ if (take.eq(0)) {
381
+ continue
320
382
  }
321
- const levelNotional = size * price
322
- const take = Math.min(remaining, levelNotional)
323
- filledNotional += take
324
- baseSize += take / price
325
- remaining -= take
383
+ filledNotional = filledNotional.plus(take)
384
+ baseSize = baseSize.plus(take.div(price))
385
+ remaining = remaining.minus(take)
326
386
  }
327
387
  return {
328
- baseSize,
329
- filledNotional,
330
- vwap: baseSize === 0 ? 0 : filledNotional / baseSize,
331
- insufficientLiquidity: remaining > 0,
388
+ baseSize: baseSize.toNumber(),
389
+ filledNotional: filledNotional.toNumber(),
390
+ vwap: baseSize.eq(0) ? 0 : filledNotional.div(baseSize).toNumber(),
391
+ insufficientLiquidity: remaining.gt(0),
392
+ }
393
+ }
394
+
395
+ function parseLevel(level: OrderbookLevel): { price: Big; size: Big } {
396
+ try {
397
+ return { price: new DivBig(level.price), size: new DivBig(level.size) }
398
+ } catch {
399
+ throw new PerpsError(
400
+ PerpsErrorCode.ValidationError,
401
+ `Malformed orderbook level: price='${level.price}', size='${level.size}'`
402
+ )
332
403
  }
333
404
  }
334
405
 
@@ -0,0 +1,21 @@
1
+ import Big from 'big.js'
2
+
3
+ /**
4
+ * Scoped big.js constructor with 40 decimal places for division, so the
5
+ * shared global `Big.DP` and `Big.RM` stay untouched.
6
+ */
7
+ export const DivBig = Big()
8
+ DivBig.DP = 40
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
+
18
+ /** True when every value is a finite number, so `new DivBig(value)` cannot throw. */
19
+ export function areFinite(...values: number[]): boolean {
20
+ return values.every(Number.isFinite)
21
+ }