@pythnetwork/pyth-lazer-sdk 6.0.0 → 6.2.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.
@@ -17,43 +17,69 @@ const UINT16_NUM_BYTES = 2;
17
17
  const UINT32_NUM_BYTES = 4;
18
18
  const UINT64_NUM_BYTES = 8;
19
19
  class PythLazerClient {
20
- token;
20
+ logger;
21
21
  metadataServiceUrl;
22
22
  priceServiceUrl;
23
- logger;
23
+ token;
24
24
  wsp;
25
- constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
26
- this.token = token;
25
+ constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
26
+ this.logger = logger;
27
27
  this.metadataServiceUrl = metadataServiceUrl;
28
28
  this.priceServiceUrl = priceServiceUrl;
29
- this.logger = logger;
29
+ this.token = token;
30
30
  this.wsp = wsp;
31
31
  }
32
32
  /**
33
- * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
34
- * @throws Error if WebSocket pool is not configured
35
- * @returns The WebSocket pool
36
- */ getWebSocketPool() {
37
- if (!this.wsp) {
38
- throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
39
- }
40
- return this.wsp;
41
- }
42
- /**
43
33
  * Creates a new PythLazerClient instance.
44
34
  * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
45
35
  */ static async create(config) {
36
+ let wsp = undefined;
37
+ let client = undefined;
38
+ const { abortSignal } = config;
39
+ const handleAbort = ()=>{
40
+ // we need to EXPLICITLY shutdown both items,
41
+ // because there is a possibility the client was undefined
42
+ // when the abort signal was called
43
+ client?.shutdown();
44
+ wsp?.shutdown();
45
+ abortSignal?.removeEventListener("abort", handleAbort);
46
+ };
47
+ const throwIfAborted = ()=>{
48
+ if (abortSignal?.aborted) {
49
+ throw new DOMException("PythLazerClient.create() was aborted", "AbortError");
50
+ }
51
+ };
52
+ abortSignal?.addEventListener("abort", handleAbort);
46
53
  const token = config.token;
47
54
  // Collect and remove trailing slash from URLs
48
55
  const metadataServiceUrl = (config.metadataServiceUrl ?? _constants.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
49
56
  const priceServiceUrl = (config.priceServiceUrl ?? _constants.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
50
57
  const logger = config.logger ?? _tslog.dummyLogger;
51
- // If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
52
- let wsp;
53
- if (config.webSocketPoolConfig) {
54
- wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, logger);
58
+ try {
59
+ throwIfAborted();
60
+ // the prior API was mismatched, in that it marked a websocket pool as optional,
61
+ // yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
62
+ // now, the typings indicate it's no longer optional and we don't sanity check
63
+ // if it's set
64
+ wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, abortSignal, logger);
65
+ throwIfAborted();
66
+ client = new PythLazerClient({
67
+ logger,
68
+ metadataServiceUrl,
69
+ priceServiceUrl,
70
+ token,
71
+ wsp
72
+ });
73
+ throwIfAborted();
74
+ // detach the abortSignal handler here, because we've made it!
75
+ abortSignal?.removeEventListener("abort", handleAbort);
76
+ return client;
77
+ } catch (error) {
78
+ client?.shutdown();
79
+ wsp?.shutdown();
80
+ abortSignal?.removeEventListener("abort", handleAbort);
81
+ throw error;
55
82
  }
56
- return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
57
83
  }
58
84
  /**
59
85
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -61,9 +87,8 @@ class PythLazerClient {
61
87
  * @param handler - Callback function that receives the parsed message. The message can be either a JSON response
62
88
  * or a binary response containing EVM, Solana, or parsed payload data.
63
89
  */ addMessageListener(handler) {
64
- const wsp = this.getWebSocketPool();
65
- wsp.addMessageListener(async (data)=>{
66
- if (typeof data == "string") {
90
+ this.wsp.addMessageListener(async (data)=>{
91
+ if (typeof data === "string") {
67
92
  handler({
68
93
  type: "json",
69
94
  value: JSON.parse(data)
@@ -74,7 +99,7 @@ class PythLazerClient {
74
99
  let pos = 0;
75
100
  const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
76
101
  pos += UINT32_NUM_BYTES;
77
- if (magic != _protocol.BINARY_UPDATE_FORMAT_MAGIC_LE) {
102
+ if (magic !== _protocol.BINARY_UPDATE_FORMAT_MAGIC_LE) {
78
103
  throw new Error("binary update format magic mismatch");
79
104
  }
80
105
  // TODO: some uint64 values may not be representable as Number.
@@ -87,15 +112,15 @@ class PythLazerClient {
87
112
  const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
88
113
  pos += UINT16_NUM_BYTES;
89
114
  const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
90
- if (magic == _protocol.FORMAT_MAGICS_LE.EVM) {
115
+ if (magic === _protocol.FORMAT_MAGICS_LE.EVM) {
91
116
  value.evm = buffData.subarray(pos, pos + len);
92
- } else if (magic == _protocol.FORMAT_MAGICS_LE.SOLANA) {
117
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.SOLANA) {
93
118
  value.solana = buffData.subarray(pos, pos + len);
94
- } else if (magic == _protocol.FORMAT_MAGICS_LE.LE_ECDSA) {
119
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.LE_ECDSA) {
95
120
  value.leEcdsa = buffData.subarray(pos, pos + len);
96
- } else if (magic == _protocol.FORMAT_MAGICS_LE.LE_UNSIGNED) {
121
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.LE_UNSIGNED) {
97
122
  value.leUnsigned = buffData.subarray(pos, pos + len);
98
- } else if (magic == _protocol.FORMAT_MAGICS_LE.JSON) {
123
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.JSON) {
99
124
  value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
100
125
  } else {
101
126
  throw new Error(`unknown magic: ${magic.toString()}`);
@@ -109,42 +134,54 @@ class PythLazerClient {
109
134
  });
110
135
  }
111
136
  subscribe(request) {
112
- const wsp = this.getWebSocketPool();
113
137
  if (request.type !== "subscribe") {
114
138
  throw new Error("Request must be a subscribe request");
115
139
  }
116
- wsp.addSubscription(request);
140
+ this.wsp.addSubscription(request);
117
141
  }
118
142
  unsubscribe(subscriptionId) {
119
- const wsp = this.getWebSocketPool();
120
- wsp.removeSubscription(subscriptionId);
143
+ this.wsp.removeSubscription(subscriptionId);
121
144
  }
122
145
  send(request) {
123
- const wsp = this.getWebSocketPool();
124
- wsp.sendRequest(request);
146
+ this.wsp.sendRequest(request);
125
147
  }
126
148
  /**
127
149
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
128
150
  * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
129
151
  * @param handler - Function to be called when all connections are down
130
152
  */ addAllConnectionsDownListener(handler) {
131
- const wsp = this.getWebSocketPool();
132
- wsp.addAllConnectionsDownListener(handler);
153
+ this.wsp.addAllConnectionsDownListener(handler);
154
+ }
155
+ /**
156
+ * Registers a handler function that will be called when at least one connection is restored after all were down.
157
+ * @param handler - Function to be called when connection is restored
158
+ */ addConnectionRestoredListener(handler) {
159
+ this.wsp.addConnectionRestoredListener(handler);
160
+ }
161
+ /**
162
+ * Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
163
+ * @param handler - Function to be called with connection index and endpoint URL
164
+ */ addConnectionTimeoutListener(handler) {
165
+ this.wsp.addConnectionTimeoutListener(handler);
166
+ }
167
+ /**
168
+ * Registers a handler function that will be called when an individual connection reconnects.
169
+ * @param handler - Function to be called with connection index and endpoint URL
170
+ */ addConnectionReconnectListener(handler) {
171
+ this.wsp.addConnectionReconnectListener(handler);
133
172
  }
134
173
  shutdown() {
135
- const wsp = this.getWebSocketPool();
136
- wsp.shutdown();
174
+ this.wsp.shutdown();
137
175
  }
138
176
  /**
139
177
  * Private helper method to make authenticated HTTP requests with Bearer token
140
178
  * @param url - The URL to fetch
141
179
  * @param options - Additional fetch options
142
180
  * @returns Promise resolving to the fetch Response
143
- */ async authenticatedFetch(url, options = {}) {
144
- const headers = {
145
- Authorization: `Bearer ${this.token}`,
146
- ...options.headers
147
- };
181
+ */ authenticatedFetch(url, options = {}) {
182
+ // Handle all possible types of headers (Headers object, array, or plain object)
183
+ const headers = new Headers(options.headers);
184
+ headers.set("Authorization", headers.get("authorization") ?? `Bearer ${this.token}`);
148
185
  return fetch(url, {
149
186
  ...options,
150
187
  headers
@@ -181,15 +218,15 @@ class PythLazerClient {
181
218
  try {
182
219
  const body = JSON.stringify(params);
183
220
  this.logger.debug("getLatestPrice", {
184
- url,
185
- body
221
+ body,
222
+ url
186
223
  });
187
224
  const response = await this.authenticatedFetch(url, {
188
- method: "POST",
225
+ body: body,
189
226
  headers: {
190
227
  "Content-Type": "application/json"
191
228
  },
192
- body: body
229
+ method: "POST"
193
230
  });
194
231
  if (!response.ok) {
195
232
  throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
@@ -208,15 +245,15 @@ class PythLazerClient {
208
245
  try {
209
246
  const body = JSON.stringify(params);
210
247
  this.logger.debug("getPrice", {
211
- url,
212
- body
248
+ body,
249
+ url
213
250
  });
214
251
  const response = await this.authenticatedFetch(url, {
215
- method: "POST",
252
+ body: body,
216
253
  headers: {
217
254
  "Content-Type": "application/json"
218
255
  },
219
- body: body
256
+ method: "POST"
220
257
  });
221
258
  if (!response.ok) {
222
259
  throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
@@ -1,5 +1,5 @@
1
1
  import type { Logger } from "ts-log";
2
- import type { ParsedPayload, Request, Response, SymbolResponse, SymbolsQueryParams, LatestPriceRequest, PriceRequest, JsonUpdate } from "./protocol.js";
2
+ import type { JsonUpdate, LatestPriceRequest, ParsedPayload, PriceRequest, Request, Response, SymbolResponse, SymbolsQueryParams } from "./protocol.js";
3
3
  import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
4
4
  export type BinaryResponse = {
5
5
  subscriptionId: number;
@@ -17,25 +17,25 @@ export type JsonOrBinaryResponse = {
17
17
  value: BinaryResponse;
18
18
  };
19
19
  export type LazerClientConfig = {
20
+ /**
21
+ * If provided, allows aborting the client creation process.
22
+ * Once the client is successfully created, this signal has no effect on the client's lifecycle.
23
+ * When aborted, the promise will reject with a DOMException with name 'AbortError'.
24
+ */
25
+ abortSignal?: AbortSignal;
20
26
  token: string;
21
27
  metadataServiceUrl?: string;
22
28
  priceServiceUrl?: string;
23
29
  logger?: Logger;
24
- webSocketPoolConfig?: WebSocketPoolConfig;
30
+ webSocketPoolConfig: WebSocketPoolConfig;
25
31
  };
26
32
  export declare class PythLazerClient {
27
- private readonly token;
28
- private readonly metadataServiceUrl;
29
- private readonly priceServiceUrl;
30
- private readonly logger;
31
- private readonly wsp?;
33
+ private logger;
34
+ private metadataServiceUrl;
35
+ private priceServiceUrl;
36
+ private token;
37
+ private wsp;
32
38
  private constructor();
33
- /**
34
- * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
35
- * @throws Error if WebSocket pool is not configured
36
- * @returns The WebSocket pool
37
- */
38
- private getWebSocketPool;
39
39
  /**
40
40
  * Creates a new PythLazerClient instance.
41
41
  * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
@@ -57,6 +57,21 @@ export declare class PythLazerClient {
57
57
  * @param handler - Function to be called when all connections are down
58
58
  */
59
59
  addAllConnectionsDownListener(handler: () => void): void;
60
+ /**
61
+ * Registers a handler function that will be called when at least one connection is restored after all were down.
62
+ * @param handler - Function to be called when connection is restored
63
+ */
64
+ addConnectionRestoredListener(handler: () => void): void;
65
+ /**
66
+ * Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
67
+ * @param handler - Function to be called with connection index and endpoint URL
68
+ */
69
+ addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
70
+ /**
71
+ * Registers a handler function that will be called when an individual connection reconnects.
72
+ * @param handler - Function to be called with connection index and endpoint URL
73
+ */
74
+ addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
60
75
  shutdown(): void;
61
76
  /**
62
77
  * Private helper method to make authenticated HTTP requests with Bearer token
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
5
  _export_star(require("./client.cjs"), exports);
6
- _export_star(require("./protocol.cjs"), exports);
7
6
  _export_star(require("./constants.cjs"), exports);
7
+ _export_star(require("./protocol.cjs"), exports);
8
8
  function _export_star(from, to) {
9
9
  Object.keys(from).forEach(function(k) {
10
10
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
@@ -1,3 +1,3 @@
1
1
  export * from "./client.js";
2
- export * from "./protocol.js";
3
2
  export * from "./constants.js";
3
+ export * from "./protocol.js";
@@ -21,11 +21,11 @@ _export(exports, {
21
21
  });
22
22
  const BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
23
23
  const FORMAT_MAGICS_LE = {
24
- JSON: 3_302_625_434,
25
24
  EVM: 2_593_727_018,
26
- SOLANA: 2_182_742_457,
25
+ JSON: 3_302_625_434,
27
26
  LE_ECDSA: 1_296_547_300,
28
- LE_UNSIGNED: 1_499_680_012
27
+ LE_UNSIGNED: 1_499_680_012,
28
+ SOLANA: 2_182_742_457
29
29
  };
30
30
  var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
31
31
  CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
@@ -2,7 +2,7 @@ export type Format = "evm" | "solana" | "leEcdsa" | "leUnsigned";
2
2
  export type DeliveryFormat = "json" | "binary";
3
3
  export type JsonBinaryEncoding = "base64" | "hex";
4
4
  export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval" | "marketSession" | "emaPrice" | "emaConfidence" | "feedUpdateTimestamp";
5
- export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms";
5
+ export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms" | "fixed_rate@1000ms";
6
6
  export type Request = {
7
7
  type: "subscribe";
8
8
  subscriptionId: number;
@@ -78,11 +78,11 @@ export type Response = {
78
78
  };
79
79
  export declare const BINARY_UPDATE_FORMAT_MAGIC_LE = 461928307;
80
80
  export declare const FORMAT_MAGICS_LE: {
81
- JSON: number;
82
81
  EVM: number;
83
- SOLANA: number;
82
+ JSON: number;
84
83
  LE_ECDSA: number;
85
84
  LE_UNSIGNED: number;
85
+ SOLANA: number;
86
86
  };
87
87
  export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate" | "eco" | "kalshi";
88
88
  export type SymbolsQueryParams = {
@@ -45,6 +45,7 @@ class ResilientWebSocket {
45
45
  onError;
46
46
  onMessage;
47
47
  onReconnect;
48
+ onTimeout;
48
49
  constructor(config){
49
50
  this.endpoint = config.endpoint;
50
51
  this.wsOptions = config.wsOptions;
@@ -62,6 +63,9 @@ class ResilientWebSocket {
62
63
  this.onReconnect = ()=>{
63
64
  // Empty function, can be set by the user.
64
65
  };
66
+ this.onTimeout = ()=>{
67
+ // Empty function, can be set by the user.
68
+ };
65
69
  }
66
70
  send(data) {
67
71
  this.logger.debug(`Sending message`);
@@ -129,6 +133,7 @@ class ResilientWebSocket {
129
133
  this.heartbeatTimeout = setTimeout(()=>{
130
134
  const warnMsg = "Connection timed out. Reconnecting...";
131
135
  this.logger.warn(warnMsg);
136
+ this.onTimeout();
132
137
  if (this.wsClient) {
133
138
  if (typeof this.wsClient.terminate === "function") {
134
139
  this.wsClient.terminate();
@@ -44,6 +44,7 @@ export declare class ResilientWebSocket {
44
44
  onError: (error: ErrorEvent) => void;
45
45
  onMessage: (data: WebSocket.Data) => void;
46
46
  onReconnect: () => void;
47
+ onTimeout: () => void;
47
48
  constructor(config: ResilientWebSocketConfig);
48
49
  send(data: string | Buffer): void;
49
50
  startWebSocket(): void;