@pythnetwork/pyth-lazer-sdk 4.0.0 → 5.2.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 (45) hide show
  1. package/README.md +57 -0
  2. package/dist/cjs/client.cjs +229 -0
  3. package/dist/cjs/constants.cjs +36 -0
  4. package/dist/cjs/index.cjs +20 -0
  5. package/dist/cjs/package.json +1 -1
  6. package/dist/cjs/protocol.cjs +33 -0
  7. package/dist/cjs/protocol.d.ts +4 -1
  8. package/dist/cjs/socket/{resilient-websocket.js → resilient-websocket.cjs} +60 -45
  9. package/dist/cjs/socket/resilient-websocket.d.ts +15 -1
  10. package/dist/cjs/socket/{websocket-pool.js → websocket-pool.cjs} +77 -64
  11. package/dist/cjs/socket/websocket-pool.d.ts +5 -2
  12. package/dist/cjs/util/buffer-util.cjs +33 -0
  13. package/dist/cjs/util/buffer-util.d.ts +7 -0
  14. package/dist/cjs/util/env-util.cjs +33 -0
  15. package/dist/cjs/util/env-util.d.ts +17 -0
  16. package/dist/cjs/util/index.cjs +20 -0
  17. package/dist/cjs/util/index.d.ts +3 -0
  18. package/dist/cjs/util/url-util.cjs +17 -0
  19. package/dist/cjs/util/url-util.d.ts +8 -0
  20. package/dist/esm/client.mjs +219 -0
  21. package/dist/esm/index.mjs +3 -0
  22. package/dist/esm/package.json +1 -1
  23. package/dist/esm/protocol.d.ts +4 -1
  24. package/dist/esm/protocol.mjs +12 -0
  25. package/dist/esm/socket/resilient-websocket.d.ts +15 -1
  26. package/dist/esm/socket/{resilient-websocket.js → resilient-websocket.mjs} +42 -35
  27. package/dist/esm/socket/websocket-pool.d.ts +5 -2
  28. package/dist/esm/socket/{websocket-pool.js → websocket-pool.mjs} +58 -54
  29. package/dist/esm/util/buffer-util.d.ts +7 -0
  30. package/dist/esm/util/buffer-util.mjs +27 -0
  31. package/dist/esm/util/env-util.d.ts +17 -0
  32. package/dist/esm/util/env-util.mjs +23 -0
  33. package/dist/esm/util/index.d.ts +3 -0
  34. package/dist/esm/util/index.mjs +3 -0
  35. package/dist/esm/util/url-util.d.ts +8 -0
  36. package/dist/esm/util/url-util.mjs +13 -0
  37. package/package.json +111 -15
  38. package/dist/cjs/client.js +0 -236
  39. package/dist/cjs/constants.js +0 -9
  40. package/dist/cjs/index.js +0 -19
  41. package/dist/cjs/protocol.js +0 -11
  42. package/dist/esm/client.js +0 -230
  43. package/dist/esm/index.js +0 -3
  44. package/dist/esm/protocol.js +0 -8
  45. /package/dist/esm/{constants.js → constants.mjs} +0 -0
@@ -1,79 +1,91 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WebSocketPool = void 0;
7
- const ttlcache_1 = __importDefault(require("@isaacs/ttlcache"));
8
- const ts_log_1 = require("ts-log");
9
- const resilient_websocket_js_1 = require("./resilient-websocket.js");
10
- const constants_js_1 = require("../constants.js");
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "WebSocketPool", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return WebSocketPool;
9
+ }
10
+ });
11
+ const _ttlcache = /*#__PURE__*/ _interop_require_default(require("@isaacs/ttlcache"));
12
+ const _tslog = require("ts-log");
13
+ const _resilientwebsocket = require("./resilient-websocket.cjs");
14
+ const _constants = require("../constants.cjs");
15
+ const _index = require("../util/index.cjs");
16
+ function _interop_require_default(obj) {
17
+ return obj && obj.__esModule ? obj : {
18
+ default: obj
19
+ };
20
+ }
11
21
  const DEFAULT_NUM_CONNECTIONS = 4;
12
22
  class WebSocketPool {
13
23
  logger;
14
24
  rwsPool;
15
25
  cache;
16
- subscriptions; // id -> subscription Request
26
+ subscriptions;
17
27
  messageListeners;
18
28
  allConnectionsDownListeners;
19
29
  wasAllDown = true;
20
30
  checkConnectionStatesInterval;
21
- constructor(logger) {
31
+ constructor(logger){
22
32
  this.logger = logger;
23
33
  this.rwsPool = [];
24
- this.cache = new ttlcache_1.default({ ttl: 1000 * 10 }); // TTL of 10 seconds
34
+ this.cache = new _ttlcache.default({
35
+ ttl: 1000 * 10
36
+ }); // TTL of 10 seconds
25
37
  this.subscriptions = new Map();
26
38
  this.messageListeners = [];
27
39
  this.allConnectionsDownListeners = [];
28
40
  // Start monitoring connection states
29
- this.checkConnectionStatesInterval = setInterval(() => {
41
+ this.checkConnectionStatesInterval = setInterval(()=>{
30
42
  this.checkConnectionStates();
31
43
  }, 100);
32
44
  }
33
45
  /**
34
- * Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
35
- * Usage semantics are similar to using a regular WebSocket client.
36
- * @param urls - List of WebSocket URLs to connect to
37
- * @param token - Authentication token to use for the connections
38
- * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
39
- * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
40
- */
41
- static async create(config, token, logger) {
46
+ * Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
47
+ * Usage semantics are similar to using a regular WebSocket client.
48
+ * @param urls - List of WebSocket URLs to connect to
49
+ * @param token - Authentication token to use for the connections
50
+ * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
51
+ * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
52
+ */ static async create(config, token, logger) {
42
53
  const urls = config.urls ?? [
43
- constants_js_1.DEFAULT_STREAM_SERVICE_0_URL,
44
- constants_js_1.DEFAULT_STREAM_SERVICE_1_URL,
54
+ _constants.DEFAULT_STREAM_SERVICE_0_URL,
55
+ _constants.DEFAULT_STREAM_SERVICE_1_URL
45
56
  ];
46
- const log = logger ?? ts_log_1.dummyLogger;
57
+ const log = logger ?? _tslog.dummyLogger;
47
58
  const pool = new WebSocketPool(log);
48
59
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
49
- for (let i = 0; i < numConnections; i++) {
50
- const url = urls[i % urls.length];
60
+ for(let i = 0; i < numConnections; i++){
61
+ const baseUrl = urls[i % urls.length];
62
+ const isBrowser = (0, _index.envIsBrowserOrWorker)();
63
+ const url = isBrowser ? (0, _index.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
51
64
  if (!url) {
52
65
  throw new Error(`URLs must not be null or empty`);
53
66
  }
54
67
  const wsOptions = {
55
68
  ...config.rwsConfig?.wsOptions,
56
- headers: {
57
- Authorization: `Bearer ${token}`,
58
- },
69
+ headers: isBrowser ? undefined : {
70
+ Authorization: `Bearer ${token}`
71
+ }
59
72
  };
60
- const rws = new resilient_websocket_js_1.ResilientWebSocket({
73
+ const rws = new _resilientwebsocket.ResilientWebSocket({
61
74
  ...config.rwsConfig,
62
75
  endpoint: url,
63
76
  wsOptions,
64
- logger: log,
77
+ logger: log
65
78
  });
66
79
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
67
80
  // the connection and call the onReconnect callback.
68
- rws.onReconnect = () => {
81
+ rws.onReconnect = ()=>{
69
82
  if (rws.wsUserClosed) {
70
83
  return;
71
84
  }
72
- for (const [, request] of pool.subscriptions) {
85
+ for (const [, request] of pool.subscriptions){
73
86
  try {
74
87
  rws.send(JSON.stringify(request));
75
- }
76
- catch (error) {
88
+ } catch (error) {
77
89
  pool.logger.error("Failed to resend subscription on reconnect:", error);
78
90
  }
79
91
  }
@@ -82,37 +94,42 @@ class WebSocketPool {
82
94
  rws.onError = config.onError;
83
95
  }
84
96
  // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
85
- rws.onMessage = pool.dedupeHandler;
97
+ rws.onMessage = (data)=>{
98
+ pool.dedupeHandler(data).catch((error)=>{
99
+ const errMsg = `An error occurred in the WebSocket pool's dedupeHandler: ${error instanceof Error ? error.message : String(error)}`;
100
+ throw new Error(errMsg);
101
+ });
102
+ };
86
103
  pool.rwsPool.push(rws);
87
104
  rws.startWebSocket();
88
105
  }
89
106
  pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
90
- while (!pool.isAnyConnectionEstablished()) {
91
- await new Promise((resolve) => setTimeout(resolve, 100));
107
+ while(!pool.isAnyConnectionEstablished()){
108
+ await new Promise((resolve)=>setTimeout(resolve, 100));
92
109
  }
93
110
  pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
94
111
  return pool;
95
112
  }
96
113
  /**
97
- * Checks for error responses in JSON messages and throws appropriate errors
98
- */
99
- handleErrorMessages(data) {
114
+ * Checks for error responses in JSON messages and throws appropriate errors
115
+ */ handleErrorMessages(data) {
100
116
  const message = JSON.parse(data);
101
117
  if (message.type === "subscriptionError") {
102
118
  throw new Error(`Error occurred for subscription ID ${String(message.subscriptionId)}: ${message.error}`);
103
- }
104
- else if (message.type === "error") {
119
+ } else if (message.type === "error") {
105
120
  throw new Error(`Error: ${message.error}`);
106
121
  }
107
122
  }
123
+ async constructCacheKeyFromWebsocketData(data) {
124
+ if (typeof data === "string") return data;
125
+ const buff = await (0, _index.bufferFromWebsocketData)(data);
126
+ return buff.toString("hex");
127
+ }
108
128
  /**
109
- * Handles incoming websocket messages by deduplicating identical messages received across
110
- * multiple connections before forwarding to registered handlers
111
- */
112
- dedupeHandler = (data) => {
113
- const cacheKey = typeof data === "string"
114
- ? data
115
- : Buffer.from(data).toString("hex");
129
+ * Handles incoming websocket messages by deduplicating identical messages received across
130
+ * multiple connections before forwarding to registered handlers
131
+ */ dedupeHandler = async (data)=>{
132
+ const cacheKey = await this.constructCacheKeyFromWebsocketData(data);
116
133
  if (this.cache.has(cacheKey)) {
117
134
  this.logger.debug("Dropping duplicate message");
118
135
  return;
@@ -121,12 +138,10 @@ class WebSocketPool {
121
138
  if (typeof data === "string") {
122
139
  this.handleErrorMessages(data);
123
140
  }
124
- for (const handler of this.messageListeners) {
125
- handler(data);
126
- }
141
+ await Promise.all(this.messageListeners.map((handler)=>handler(data)));
127
142
  };
128
143
  sendRequest(request) {
129
- for (const rws of this.rwsPool) {
144
+ for (const rws of this.rwsPool){
130
145
  rws.send(JSON.stringify(request));
131
146
  }
132
147
  }
@@ -141,7 +156,7 @@ class WebSocketPool {
141
156
  this.subscriptions.delete(subscriptionId);
142
157
  const request = {
143
158
  type: "unsubscribe",
144
- subscriptionId,
159
+ subscriptionId
145
160
  };
146
161
  this.sendRequest(request);
147
162
  }
@@ -149,17 +164,16 @@ class WebSocketPool {
149
164
  this.messageListeners.push(handler);
150
165
  }
151
166
  /**
152
- * Calls the handler if all websocket connections are currently down or in reconnecting state.
153
- * The connections may still try to reconnect in the background.
154
- */
155
- addAllConnectionsDownListener(handler) {
167
+ * Calls the handler if all websocket connections are currently down or in reconnecting state.
168
+ * The connections may still try to reconnect in the background.
169
+ */ addAllConnectionsDownListener(handler) {
156
170
  this.allConnectionsDownListeners.push(handler);
157
171
  }
158
172
  areAllConnectionsDown() {
159
- return this.rwsPool.every((ws) => !ws.isConnected() || ws.isReconnecting());
173
+ return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
160
174
  }
161
175
  isAnyConnectionEstablished() {
162
- return this.rwsPool.some((ws) => ws.isConnected());
176
+ return this.rwsPool.some((ws)=>ws.isConnected());
163
177
  }
164
178
  checkConnectionStates() {
165
179
  const allDown = this.areAllConnectionsDown();
@@ -168,7 +182,7 @@ class WebSocketPool {
168
182
  this.wasAllDown = true;
169
183
  this.logger.error("All WebSocket connections are down or reconnecting");
170
184
  // Notify all listeners
171
- for (const listener of this.allConnectionsDownListeners) {
185
+ for (const listener of this.allConnectionsDownListeners){
172
186
  listener();
173
187
  }
174
188
  }
@@ -178,7 +192,7 @@ class WebSocketPool {
178
192
  }
179
193
  }
180
194
  shutdown() {
181
- for (const rws of this.rwsPool) {
195
+ for (const rws of this.rwsPool){
182
196
  rws.closeWebSocket();
183
197
  }
184
198
  this.rwsPool = [];
@@ -188,4 +202,3 @@ class WebSocketPool {
188
202
  clearInterval(this.checkConnectionStatesInterval);
189
203
  }
190
204
  }
191
- exports.WebSocketPool = WebSocketPool;
@@ -4,6 +4,7 @@ import type { Logger } from "ts-log";
4
4
  import type { Request } from "../protocol.js";
5
5
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ type WebSocketOnMessageCallback = (data: WebSocket.Data) => void | Promise<void>;
7
8
  export type WebSocketPoolConfig = {
8
9
  urls?: string[];
9
10
  numConnections?: number;
@@ -33,15 +34,16 @@ export declare class WebSocketPool {
33
34
  * Checks for error responses in JSON messages and throws appropriate errors
34
35
  */
35
36
  private handleErrorMessages;
37
+ private constructCacheKeyFromWebsocketData;
36
38
  /**
37
39
  * Handles incoming websocket messages by deduplicating identical messages received across
38
40
  * multiple connections before forwarding to registered handlers
39
41
  */
40
- dedupeHandler: (data: WebSocket.Data) => void;
42
+ dedupeHandler: (data: WebSocket.Data) => Promise<void>;
41
43
  sendRequest(request: Request): void;
42
44
  addSubscription(request: Request): void;
43
45
  removeSubscription(subscriptionId: number): void;
44
- addMessageListener(handler: (data: WebSocket.Data) => void): void;
46
+ addMessageListener(handler: WebSocketOnMessageCallback): void;
45
47
  /**
46
48
  * Calls the handler if all websocket connections are currently down or in reconnecting state.
47
49
  * The connections may still try to reconnect in the background.
@@ -52,3 +54,4 @@ export declare class WebSocketPool {
52
54
  private checkConnectionStates;
53
55
  shutdown(): void;
54
56
  }
57
+ export {};
@@ -0,0 +1,33 @@
1
+ // the linting rules don't allow importing anything that might clash with
2
+ // a global, top-level import. we disable this rule because we need this
3
+ // imported from our installed dependency
4
+ // eslint-disable-next-line unicorn/prefer-node-protocol
5
+ "use strict";
6
+ Object.defineProperty(exports, "__esModule", {
7
+ value: true
8
+ });
9
+ Object.defineProperty(exports, "bufferFromWebsocketData", {
10
+ enumerable: true,
11
+ get: function() {
12
+ return bufferFromWebsocketData;
13
+ }
14
+ });
15
+ const _buffer = require("buffer");
16
+ const BufferClassToUse = "Buffer" in globalThis ? globalThis.Buffer : _buffer.Buffer;
17
+ async function bufferFromWebsocketData(data) {
18
+ if (typeof data === "string") {
19
+ return BufferClassToUse.from(new TextEncoder().encode(data).buffer);
20
+ }
21
+ if (data instanceof BufferClassToUse) return data;
22
+ if (data instanceof Blob) {
23
+ // let the uncaught promise exception bubble up if there's an issue
24
+ return BufferClassToUse.from(await data.arrayBuffer());
25
+ }
26
+ if (data instanceof ArrayBuffer) return BufferClassToUse.from(data);
27
+ if (Array.isArray(data)) {
28
+ // an array of buffers is highly unlikely, but it is a possibility
29
+ // indicated by the WebSocket Data interface
30
+ return BufferClassToUse.concat(data);
31
+ }
32
+ return data;
33
+ }
@@ -0,0 +1,7 @@
1
+ import type { Data } from "isomorphic-ws";
2
+ /**
3
+ * given a relatively unknown websocket frame data object,
4
+ * returns a valid Buffer instance that is safe to use
5
+ * isomorphically in any JS runtime environment
6
+ */
7
+ export declare function bufferFromWebsocketData(data: Data): Promise<Buffer>;
@@ -0,0 +1,33 @@
1
+ // we create this local-only type, which has assertions made to indicate
2
+ // that we do not know and cannot guarantee which JS environment we are in
3
+ "use strict";
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ function _export(target, all) {
8
+ for(var name in all)Object.defineProperty(target, name, {
9
+ enumerable: true,
10
+ get: Object.getOwnPropertyDescriptor(all, name).get
11
+ });
12
+ }
13
+ _export(exports, {
14
+ get envIsBrowser () {
15
+ return envIsBrowser;
16
+ },
17
+ get envIsBrowserOrWorker () {
18
+ return envIsBrowserOrWorker;
19
+ },
20
+ get envIsServiceOrWebWorker () {
21
+ return envIsServiceOrWebWorker;
22
+ }
23
+ });
24
+ const g = globalThis;
25
+ function envIsServiceOrWebWorker() {
26
+ return typeof WorkerGlobalScope !== "undefined" && g.self instanceof WorkerGlobalScope;
27
+ }
28
+ function envIsBrowser() {
29
+ return g.window !== undefined;
30
+ }
31
+ function envIsBrowserOrWorker() {
32
+ return envIsServiceOrWebWorker() || envIsBrowser();
33
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Detects if this code is running within any Service or WebWorker context.
3
+ * @returns true if in a worker of some kind, false if otherwise
4
+ */
5
+ export declare function envIsServiceOrWebWorker(): boolean;
6
+ /**
7
+ * Detects if the code is running in a regular DOM or Web Worker context.
8
+ * @returns true if running in a DOM or Web Worker context, false if running in Node.js
9
+ */
10
+ export declare function envIsBrowser(): boolean;
11
+ /**
12
+ * a convenience method that returns whether or not
13
+ * this code is executing in some type of browser-centric environment
14
+ *
15
+ * @returns true if in the browser's main UI thread or in a worker, false if otherwise
16
+ */
17
+ export declare function envIsBrowserOrWorker(): boolean;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ _export_star(require("./buffer-util.cjs"), exports);
6
+ _export_star(require("./env-util.cjs"), exports);
7
+ _export_star(require("./url-util.cjs"), exports);
8
+ function _export_star(from, to) {
9
+ Object.keys(from).forEach(function(k) {
10
+ if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
11
+ Object.defineProperty(to, k, {
12
+ enumerable: true,
13
+ get: function() {
14
+ return from[k];
15
+ }
16
+ });
17
+ }
18
+ });
19
+ return from;
20
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./buffer-util.js";
2
+ export * from "./env-util.js";
3
+ export * from "./url-util.js";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "addAuthTokenToWebSocketUrl", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return addAuthTokenToWebSocketUrl;
9
+ }
10
+ });
11
+ const ACCESS_TOKEN_QUERY_PARAM_KEY = "ACCESS_TOKEN";
12
+ function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
13
+ if (!baseUrl || !authToken) return baseUrl;
14
+ const parsedUrl = new URL(baseUrl);
15
+ parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
16
+ return parsedUrl.toString();
17
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Given a URL to a hosted lazer stream service and a possible auth token,
3
+ * appends the auth token as a query parameter and returns the URL with the token
4
+ * contained within.
5
+ * If the URL provided is nullish, it is returned as-is (in the same nullish format).
6
+ * If the token is nullish, the baseUrl given is returned, instead.
7
+ */
8
+ export declare function addAuthTokenToWebSocketUrl(baseUrl: string | null | undefined, authToken: string | null | undefined): string | null | undefined;
@@ -0,0 +1,219 @@
1
+ import { dummyLogger } from "ts-log";
2
+ import { DEFAULT_METADATA_SERVICE_URL, DEFAULT_PRICE_SERVICE_URL } from "./constants.mjs";
3
+ import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.mjs";
4
+ import { WebSocketPool } from "./socket/websocket-pool.mjs";
5
+ import { bufferFromWebsocketData } from "./util/buffer-util.mjs";
6
+ const UINT16_NUM_BYTES = 2;
7
+ const UINT32_NUM_BYTES = 4;
8
+ const UINT64_NUM_BYTES = 8;
9
+ export class PythLazerClient {
10
+ token;
11
+ metadataServiceUrl;
12
+ priceServiceUrl;
13
+ logger;
14
+ wsp;
15
+ constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
16
+ this.token = token;
17
+ this.metadataServiceUrl = metadataServiceUrl;
18
+ this.priceServiceUrl = priceServiceUrl;
19
+ this.logger = logger;
20
+ this.wsp = wsp;
21
+ }
22
+ /**
23
+ * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
24
+ * @throws Error if WebSocket pool is not configured
25
+ * @returns The WebSocket pool
26
+ */ getWebSocketPool() {
27
+ if (!this.wsp) {
28
+ throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
29
+ }
30
+ return this.wsp;
31
+ }
32
+ /**
33
+ * Creates a new PythLazerClient instance.
34
+ * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
35
+ */ static async create(config) {
36
+ const token = config.token;
37
+ // Collect and remove trailing slash from URLs
38
+ const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
39
+ const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
40
+ const logger = config.logger ?? dummyLogger;
41
+ // If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
42
+ let wsp;
43
+ if (config.webSocketPoolConfig) {
44
+ wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, logger);
45
+ }
46
+ return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
47
+ }
48
+ /**
49
+ * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
50
+ * The listener will be called for each message received, with deduplication across redundant connections.
51
+ * @param handler - Callback function that receives the parsed message. The message can be either a JSON response
52
+ * or a binary response containing EVM, Solana, or parsed payload data.
53
+ */ addMessageListener(handler) {
54
+ const wsp = this.getWebSocketPool();
55
+ wsp.addMessageListener(async (data)=>{
56
+ if (typeof data == "string") {
57
+ handler({
58
+ type: "json",
59
+ value: JSON.parse(data)
60
+ });
61
+ return;
62
+ }
63
+ const buffData = await bufferFromWebsocketData(data);
64
+ let pos = 0;
65
+ const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
66
+ pos += UINT32_NUM_BYTES;
67
+ if (magic != BINARY_UPDATE_FORMAT_MAGIC_LE) {
68
+ throw new Error("binary update format magic mismatch");
69
+ }
70
+ // TODO: some uint64 values may not be representable as Number.
71
+ const subscriptionId = Number(buffData.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
72
+ pos += UINT64_NUM_BYTES;
73
+ const value = {
74
+ subscriptionId
75
+ };
76
+ while(pos < buffData.length){
77
+ const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
78
+ pos += UINT16_NUM_BYTES;
79
+ const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
80
+ if (magic == FORMAT_MAGICS_LE.EVM) {
81
+ value.evm = buffData.subarray(pos, pos + len);
82
+ } else if (magic == FORMAT_MAGICS_LE.SOLANA) {
83
+ value.solana = buffData.subarray(pos, pos + len);
84
+ } else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
85
+ value.leEcdsa = buffData.subarray(pos, pos + len);
86
+ } else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
87
+ value.leUnsigned = buffData.subarray(pos, pos + len);
88
+ } else if (magic == FORMAT_MAGICS_LE.JSON) {
89
+ value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
90
+ } else {
91
+ throw new Error(`unknown magic: ${magic.toString()}`);
92
+ }
93
+ pos += len;
94
+ }
95
+ handler({
96
+ type: "binary",
97
+ value
98
+ });
99
+ });
100
+ }
101
+ subscribe(request) {
102
+ const wsp = this.getWebSocketPool();
103
+ if (request.type !== "subscribe") {
104
+ throw new Error("Request must be a subscribe request");
105
+ }
106
+ wsp.addSubscription(request);
107
+ }
108
+ unsubscribe(subscriptionId) {
109
+ const wsp = this.getWebSocketPool();
110
+ wsp.removeSubscription(subscriptionId);
111
+ }
112
+ send(request) {
113
+ const wsp = this.getWebSocketPool();
114
+ wsp.sendRequest(request);
115
+ }
116
+ /**
117
+ * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
118
+ * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
119
+ * @param handler - Function to be called when all connections are down
120
+ */ addAllConnectionsDownListener(handler) {
121
+ const wsp = this.getWebSocketPool();
122
+ wsp.addAllConnectionsDownListener(handler);
123
+ }
124
+ shutdown() {
125
+ const wsp = this.getWebSocketPool();
126
+ wsp.shutdown();
127
+ }
128
+ /**
129
+ * Private helper method to make authenticated HTTP requests with Bearer token
130
+ * @param url - The URL to fetch
131
+ * @param options - Additional fetch options
132
+ * @returns Promise resolving to the fetch Response
133
+ */ async authenticatedFetch(url, options = {}) {
134
+ const headers = {
135
+ Authorization: `Bearer ${this.token}`,
136
+ ...options.headers
137
+ };
138
+ return fetch(url, {
139
+ ...options,
140
+ headers
141
+ });
142
+ }
143
+ /**
144
+ * Queries the symbols endpoint to get available price feed symbols.
145
+ * @param params - Optional query parameters to filter symbols
146
+ * @returns Promise resolving to array of symbol information
147
+ */ async getSymbols(params) {
148
+ const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
149
+ if (params?.query) {
150
+ url.searchParams.set("query", params.query);
151
+ }
152
+ if (params?.asset_type) {
153
+ url.searchParams.set("asset_type", params.asset_type);
154
+ }
155
+ try {
156
+ const response = await this.authenticatedFetch(url.toString());
157
+ if (!response.ok) {
158
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
159
+ }
160
+ return await response.json();
161
+ } catch (error) {
162
+ throw new Error(`Failed to fetch symbols: ${error instanceof Error ? error.message : String(error)}`);
163
+ }
164
+ }
165
+ /**
166
+ * Queries the latest price endpoint to get current price data.
167
+ * @param params - Parameters for the latest price request
168
+ * @returns Promise resolving to JsonUpdate with current price data
169
+ */ async getLatestPrice(params) {
170
+ const url = `${this.priceServiceUrl}/v1/latest_price`;
171
+ try {
172
+ const body = JSON.stringify(params);
173
+ this.logger.debug("getLatestPrice", {
174
+ url,
175
+ body
176
+ });
177
+ const response = await this.authenticatedFetch(url, {
178
+ method: "POST",
179
+ headers: {
180
+ "Content-Type": "application/json"
181
+ },
182
+ body: body
183
+ });
184
+ if (!response.ok) {
185
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
186
+ }
187
+ return await response.json();
188
+ } catch (error) {
189
+ throw new Error(`Failed to fetch latest price: ${error instanceof Error ? error.message : String(error)}`);
190
+ }
191
+ }
192
+ /**
193
+ * Queries the price endpoint to get historical price data at a specific timestamp.
194
+ * @param params - Parameters for the price request including timestamp
195
+ * @returns Promise resolving to JsonUpdate with price data at the specified time
196
+ */ async getPrice(params) {
197
+ const url = `${this.priceServiceUrl}/v1/price`;
198
+ try {
199
+ const body = JSON.stringify(params);
200
+ this.logger.debug("getPrice", {
201
+ url,
202
+ body
203
+ });
204
+ const response = await this.authenticatedFetch(url, {
205
+ method: "POST",
206
+ headers: {
207
+ "Content-Type": "application/json"
208
+ },
209
+ body: body
210
+ });
211
+ if (!response.ok) {
212
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
213
+ }
214
+ return await response.json();
215
+ } catch (error) {
216
+ throw new Error(`Failed to fetch price: ${error instanceof Error ? error.message : String(error)}`);
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./client.mjs";
2
+ export * from "./protocol.mjs";
3
+ export * from "./constants.mjs";
@@ -1 +1 @@
1
- {"type":"module"}
1
+ { "type": "module" }