@pythnetwork/pyth-lazer-sdk 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
- import type { ParsedPayload, Request, Response } from "./protocol.js";
1
+ import type { Logger } from "ts-log";
2
+ import type { ParsedPayload, Request, Response, SymbolResponse, SymbolsQueryParams, LatestPriceRequest, PriceRequest, JsonUpdate } from "./protocol.js";
2
3
  import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
3
4
  export type BinaryResponse = {
4
5
  subscriptionId: number;
@@ -15,17 +16,31 @@ export type JsonOrBinaryResponse = {
15
16
  type: "binary";
16
17
  value: BinaryResponse;
17
18
  };
19
+ export type LazerClientConfig = {
20
+ token: string;
21
+ metadataServiceUrl?: string;
22
+ priceServiceUrl?: string;
23
+ logger?: Logger;
24
+ webSocketPoolConfig?: WebSocketPoolConfig;
25
+ };
18
26
  export declare class PythLazerClient {
19
- private readonly wsp;
27
+ private readonly token;
28
+ private readonly metadataServiceUrl;
29
+ private readonly priceServiceUrl;
30
+ private readonly logger;
31
+ private readonly wsp?;
20
32
  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;
21
39
  /**
22
40
  * Creates a new PythLazerClient instance.
23
- * @param urls - List of WebSocket URLs of the Pyth Lazer service
24
- * @param token - The access token for authentication
25
- * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
26
- * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
41
+ * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
27
42
  */
28
- static create(config: WebSocketPoolConfig): Promise<PythLazerClient>;
43
+ static create(config: LazerClientConfig): Promise<PythLazerClient>;
29
44
  /**
30
45
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
31
46
  * The listener will be called for each message received, with deduplication across redundant connections.
@@ -43,4 +58,29 @@ export declare class PythLazerClient {
43
58
  */
44
59
  addAllConnectionsDownListener(handler: () => void): void;
45
60
  shutdown(): void;
61
+ /**
62
+ * Private helper method to make authenticated HTTP requests with Bearer token
63
+ * @param url - The URL to fetch
64
+ * @param options - Additional fetch options
65
+ * @returns Promise resolving to the fetch Response
66
+ */
67
+ private authenticatedFetch;
68
+ /**
69
+ * Queries the symbols endpoint to get available price feed symbols.
70
+ * @param params - Optional query parameters to filter symbols
71
+ * @returns Promise resolving to array of symbol information
72
+ */
73
+ getSymbols(params?: SymbolsQueryParams): Promise<SymbolResponse[]>;
74
+ /**
75
+ * Queries the latest price endpoint to get current price data.
76
+ * @param params - Parameters for the latest price request
77
+ * @returns Promise resolving to JsonUpdate with current price data
78
+ */
79
+ getLatestPrice(params: LatestPriceRequest): Promise<JsonUpdate>;
80
+ /**
81
+ * Queries the price endpoint to get historical price data at a specific timestamp.
82
+ * @param params - Parameters for the price request including timestamp
83
+ * @returns Promise resolving to JsonUpdate with price data at the specified time
84
+ */
85
+ getPrice(params: PriceRequest): Promise<JsonUpdate>;
46
86
  }
@@ -1,26 +1,57 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.PythLazerClient = void 0;
7
+ const cross_fetch_1 = __importDefault(require("cross-fetch"));
8
+ const ts_log_1 = require("ts-log");
9
+ const constants_js_1 = require("./constants.js");
4
10
  const protocol_js_1 = require("./protocol.js");
5
11
  const websocket_pool_js_1 = require("./socket/websocket-pool.js");
6
12
  const UINT16_NUM_BYTES = 2;
7
13
  const UINT32_NUM_BYTES = 4;
8
14
  const UINT64_NUM_BYTES = 8;
9
15
  class PythLazerClient {
16
+ token;
17
+ metadataServiceUrl;
18
+ priceServiceUrl;
19
+ logger;
10
20
  wsp;
11
- constructor(wsp) {
21
+ constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp) {
22
+ this.token = token;
23
+ this.metadataServiceUrl = metadataServiceUrl;
24
+ this.priceServiceUrl = priceServiceUrl;
25
+ this.logger = logger;
12
26
  this.wsp = wsp;
13
27
  }
28
+ /**
29
+ * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
30
+ * @throws Error if WebSocket pool is not configured
31
+ * @returns The WebSocket pool
32
+ */
33
+ getWebSocketPool() {
34
+ if (!this.wsp) {
35
+ throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
36
+ }
37
+ return this.wsp;
38
+ }
14
39
  /**
15
40
  * Creates a new PythLazerClient instance.
16
- * @param urls - List of WebSocket URLs of the Pyth Lazer service
17
- * @param token - The access token for authentication
18
- * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
19
- * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
41
+ * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
20
42
  */
21
43
  static async create(config) {
22
- const wsp = await websocket_pool_js_1.WebSocketPool.create(config);
23
- return new PythLazerClient(wsp);
44
+ const token = config.token;
45
+ // Collect and remove trailing slash from URLs
46
+ const metadataServiceUrl = (config.metadataServiceUrl ?? constants_js_1.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
47
+ const priceServiceUrl = (config.priceServiceUrl ?? constants_js_1.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
48
+ const logger = config.logger ?? ts_log_1.dummyLogger;
49
+ // If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
50
+ let wsp;
51
+ if (config.webSocketPoolConfig) {
52
+ wsp = await websocket_pool_js_1.WebSocketPool.create(config.webSocketPoolConfig, token, logger);
53
+ }
54
+ return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
24
55
  }
25
56
  /**
26
57
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -29,7 +60,8 @@ class PythLazerClient {
29
60
  * or a binary response containing EVM, Solana, or parsed payload data.
30
61
  */
31
62
  addMessageListener(handler) {
32
- this.wsp.addMessageListener((data) => {
63
+ const wsp = this.getWebSocketPool();
64
+ wsp.addMessageListener((data) => {
33
65
  if (typeof data == "string") {
34
66
  handler({
35
67
  type: "json",
@@ -81,16 +113,19 @@ class PythLazerClient {
81
113
  });
82
114
  }
83
115
  subscribe(request) {
116
+ const wsp = this.getWebSocketPool();
84
117
  if (request.type !== "subscribe") {
85
118
  throw new Error("Request must be a subscribe request");
86
119
  }
87
- this.wsp.addSubscription(request);
120
+ wsp.addSubscription(request);
88
121
  }
89
122
  unsubscribe(subscriptionId) {
90
- this.wsp.removeSubscription(subscriptionId);
123
+ const wsp = this.getWebSocketPool();
124
+ wsp.removeSubscription(subscriptionId);
91
125
  }
92
126
  send(request) {
93
- this.wsp.sendRequest(request);
127
+ const wsp = this.getWebSocketPool();
128
+ wsp.sendRequest(request);
94
129
  }
95
130
  /**
96
131
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
@@ -98,10 +133,104 @@ class PythLazerClient {
98
133
  * @param handler - Function to be called when all connections are down
99
134
  */
100
135
  addAllConnectionsDownListener(handler) {
101
- this.wsp.addAllConnectionsDownListener(handler);
136
+ const wsp = this.getWebSocketPool();
137
+ wsp.addAllConnectionsDownListener(handler);
102
138
  }
103
139
  shutdown() {
104
- this.wsp.shutdown();
140
+ const wsp = this.getWebSocketPool();
141
+ wsp.shutdown();
142
+ }
143
+ /**
144
+ * Private helper method to make authenticated HTTP requests with Bearer token
145
+ * @param url - The URL to fetch
146
+ * @param options - Additional fetch options
147
+ * @returns Promise resolving to the fetch Response
148
+ */
149
+ async authenticatedFetch(url, options = {}) {
150
+ const headers = {
151
+ Authorization: `Bearer ${this.token}`,
152
+ ...options.headers,
153
+ };
154
+ return (0, cross_fetch_1.default)(url, {
155
+ ...options,
156
+ headers,
157
+ });
158
+ }
159
+ /**
160
+ * Queries the symbols endpoint to get available price feed symbols.
161
+ * @param params - Optional query parameters to filter symbols
162
+ * @returns Promise resolving to array of symbol information
163
+ */
164
+ async getSymbols(params) {
165
+ const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
166
+ if (params?.query) {
167
+ url.searchParams.set("query", params.query);
168
+ }
169
+ if (params?.asset_type) {
170
+ url.searchParams.set("asset_type", params.asset_type);
171
+ }
172
+ try {
173
+ const response = await this.authenticatedFetch(url.toString());
174
+ if (!response.ok) {
175
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
176
+ }
177
+ return (await response.json());
178
+ }
179
+ catch (error) {
180
+ throw new Error(`Failed to fetch symbols: ${error instanceof Error ? error.message : String(error)}`);
181
+ }
182
+ }
183
+ /**
184
+ * Queries the latest price endpoint to get current price data.
185
+ * @param params - Parameters for the latest price request
186
+ * @returns Promise resolving to JsonUpdate with current price data
187
+ */
188
+ async getLatestPrice(params) {
189
+ const url = `${this.priceServiceUrl}/v1/latest_price`;
190
+ try {
191
+ const body = JSON.stringify(params);
192
+ this.logger.debug("getLatestPrice", { url, body });
193
+ const response = await this.authenticatedFetch(url, {
194
+ method: "POST",
195
+ headers: {
196
+ "Content-Type": "application/json",
197
+ },
198
+ body: body,
199
+ });
200
+ if (!response.ok) {
201
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
202
+ }
203
+ return (await response.json());
204
+ }
205
+ catch (error) {
206
+ throw new Error(`Failed to fetch latest price: ${error instanceof Error ? error.message : String(error)}`);
207
+ }
208
+ }
209
+ /**
210
+ * Queries the price endpoint to get historical price data at a specific timestamp.
211
+ * @param params - Parameters for the price request including timestamp
212
+ * @returns Promise resolving to JsonUpdate with price data at the specified time
213
+ */
214
+ async getPrice(params) {
215
+ const url = `${this.priceServiceUrl}/v1/price`;
216
+ try {
217
+ const body = JSON.stringify(params);
218
+ this.logger.debug("getPrice", { url, body });
219
+ const response = await this.authenticatedFetch(url, {
220
+ method: "POST",
221
+ headers: {
222
+ "Content-Type": "application/json",
223
+ },
224
+ body: body,
225
+ });
226
+ if (!response.ok) {
227
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
228
+ }
229
+ return (await response.json());
230
+ }
231
+ catch (error) {
232
+ throw new Error(`Failed to fetch price: ${error instanceof Error ? error.message : String(error)}`);
233
+ }
105
234
  }
106
235
  }
107
236
  exports.PythLazerClient = PythLazerClient;
@@ -1,2 +1,6 @@
1
1
  export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
2
2
  export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
+ export declare const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
4
+ export declare const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
5
+ export declare const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
+ export declare const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SOLANA_LAZER_STORAGE_ID = exports.SOLANA_LAZER_PROGRAM_ID = void 0;
3
+ exports.DEFAULT_STREAM_SERVICE_1_URL = exports.DEFAULT_STREAM_SERVICE_0_URL = exports.DEFAULT_PRICE_SERVICE_URL = exports.DEFAULT_METADATA_SERVICE_URL = exports.SOLANA_LAZER_STORAGE_ID = exports.SOLANA_LAZER_PROGRAM_ID = void 0;
4
4
  exports.SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
5
5
  exports.SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
6
+ exports.DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
7
+ exports.DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
8
+ exports.DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
9
+ exports.DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -37,6 +37,7 @@ export type JsonBinaryData = {
37
37
  };
38
38
  export type InvalidFeedSubscriptionDetails = {
39
39
  unknownIds: number[];
40
+ unknownSymbols: string[];
40
41
  unsupportedChannels: number[];
41
42
  unstable: number[];
42
43
  };
@@ -75,3 +76,49 @@ export declare const FORMAT_MAGICS_LE: {
75
76
  LE_ECDSA: number;
76
77
  LE_UNSIGNED: number;
77
78
  };
79
+ export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate";
80
+ export type SymbolsQueryParams = {
81
+ query?: string;
82
+ asset_type?: AssetType;
83
+ };
84
+ export type SymbolResponse = {
85
+ pyth_lazer_id: number;
86
+ name: string;
87
+ symbol: string;
88
+ description: string;
89
+ asset_type: string;
90
+ exponent: number;
91
+ min_publishers: number;
92
+ min_channel: string;
93
+ state: string;
94
+ schedule: string;
95
+ cmc_id?: number | null;
96
+ hermes_id?: string | null;
97
+ interval?: string | null;
98
+ };
99
+ export type LatestPriceRequest = {
100
+ priceFeedIds?: number[];
101
+ symbols?: string[];
102
+ properties: PriceFeedProperty[];
103
+ formats: Format[];
104
+ jsonBinaryEncoding?: JsonBinaryEncoding;
105
+ parsed?: boolean;
106
+ channel: Channel;
107
+ };
108
+ export type PriceRequest = {
109
+ timestamp: number;
110
+ priceFeedIds?: number[];
111
+ symbols?: string[];
112
+ properties: PriceFeedProperty[];
113
+ formats: Format[];
114
+ jsonBinaryEncoding?: JsonBinaryEncoding;
115
+ parsed?: boolean;
116
+ channel: Channel;
117
+ };
118
+ export type JsonUpdate = {
119
+ parsed?: ParsedPayload;
120
+ evm?: JsonBinaryData;
121
+ solana?: JsonBinaryData;
122
+ leEcdsa?: JsonBinaryData;
123
+ leUnsigned?: JsonBinaryData;
124
+ };
@@ -5,10 +5,8 @@ import type { Request } from "../protocol.js";
5
5
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
7
  export type WebSocketPoolConfig = {
8
- urls: string[];
9
- token: string;
8
+ urls?: string[];
10
9
  numConnections?: number;
11
- logger?: Logger;
12
10
  rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
13
11
  onError?: (error: ErrorEvent) => void;
14
12
  };
@@ -30,7 +28,7 @@ export declare class WebSocketPool {
30
28
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
31
29
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
32
30
  */
33
- static create(config: WebSocketPoolConfig): Promise<WebSocketPool>;
31
+ static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
34
32
  /**
35
33
  * Checks for error responses in JSON messages and throws appropriate errors
36
34
  */
@@ -7,6 +7,7 @@ exports.WebSocketPool = void 0;
7
7
  const ttlcache_1 = __importDefault(require("@isaacs/ttlcache"));
8
8
  const ts_log_1 = require("ts-log");
9
9
  const resilient_websocket_js_1 = require("./resilient-websocket.js");
10
+ const constants_js_1 = require("../constants.js");
10
11
  const DEFAULT_NUM_CONNECTIONS = 4;
11
12
  class WebSocketPool {
12
13
  logger;
@@ -37,29 +38,30 @@ class WebSocketPool {
37
38
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
38
39
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
39
40
  */
40
- static async create(config) {
41
- if (config.urls.length === 0) {
42
- throw new Error("No URLs provided");
43
- }
44
- const logger = config.logger ?? ts_log_1.dummyLogger;
45
- const pool = new WebSocketPool(logger);
41
+ static async create(config, token, logger) {
42
+ const urls = config.urls ?? [
43
+ constants_js_1.DEFAULT_STREAM_SERVICE_0_URL,
44
+ constants_js_1.DEFAULT_STREAM_SERVICE_1_URL,
45
+ ];
46
+ const log = logger ?? ts_log_1.dummyLogger;
47
+ const pool = new WebSocketPool(log);
46
48
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
47
49
  for (let i = 0; i < numConnections; i++) {
48
- const url = config.urls[i % config.urls.length];
50
+ const url = urls[i % urls.length];
49
51
  if (!url) {
50
52
  throw new Error(`URLs must not be null or empty`);
51
53
  }
52
54
  const wsOptions = {
53
55
  ...config.rwsConfig?.wsOptions,
54
56
  headers: {
55
- Authorization: `Bearer ${config.token}`,
57
+ Authorization: `Bearer ${token}`,
56
58
  },
57
59
  };
58
60
  const rws = new resilient_websocket_js_1.ResilientWebSocket({
59
61
  ...config.rwsConfig,
60
62
  endpoint: url,
61
63
  wsOptions,
62
- logger,
64
+ logger: log,
63
65
  });
64
66
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
65
67
  // the connection and call the onReconnect callback.
@@ -1,4 +1,5 @@
1
- import type { ParsedPayload, Request, Response } from "./protocol.js";
1
+ import type { Logger } from "ts-log";
2
+ import type { ParsedPayload, Request, Response, SymbolResponse, SymbolsQueryParams, LatestPriceRequest, PriceRequest, JsonUpdate } from "./protocol.js";
2
3
  import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
3
4
  export type BinaryResponse = {
4
5
  subscriptionId: number;
@@ -15,17 +16,31 @@ export type JsonOrBinaryResponse = {
15
16
  type: "binary";
16
17
  value: BinaryResponse;
17
18
  };
19
+ export type LazerClientConfig = {
20
+ token: string;
21
+ metadataServiceUrl?: string;
22
+ priceServiceUrl?: string;
23
+ logger?: Logger;
24
+ webSocketPoolConfig?: WebSocketPoolConfig;
25
+ };
18
26
  export declare class PythLazerClient {
19
- private readonly wsp;
27
+ private readonly token;
28
+ private readonly metadataServiceUrl;
29
+ private readonly priceServiceUrl;
30
+ private readonly logger;
31
+ private readonly wsp?;
20
32
  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;
21
39
  /**
22
40
  * Creates a new PythLazerClient instance.
23
- * @param urls - List of WebSocket URLs of the Pyth Lazer service
24
- * @param token - The access token for authentication
25
- * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
26
- * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
41
+ * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
27
42
  */
28
- static create(config: WebSocketPoolConfig): Promise<PythLazerClient>;
43
+ static create(config: LazerClientConfig): Promise<PythLazerClient>;
29
44
  /**
30
45
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
31
46
  * The listener will be called for each message received, with deduplication across redundant connections.
@@ -43,4 +58,29 @@ export declare class PythLazerClient {
43
58
  */
44
59
  addAllConnectionsDownListener(handler: () => void): void;
45
60
  shutdown(): void;
61
+ /**
62
+ * Private helper method to make authenticated HTTP requests with Bearer token
63
+ * @param url - The URL to fetch
64
+ * @param options - Additional fetch options
65
+ * @returns Promise resolving to the fetch Response
66
+ */
67
+ private authenticatedFetch;
68
+ /**
69
+ * Queries the symbols endpoint to get available price feed symbols.
70
+ * @param params - Optional query parameters to filter symbols
71
+ * @returns Promise resolving to array of symbol information
72
+ */
73
+ getSymbols(params?: SymbolsQueryParams): Promise<SymbolResponse[]>;
74
+ /**
75
+ * Queries the latest price endpoint to get current price data.
76
+ * @param params - Parameters for the latest price request
77
+ * @returns Promise resolving to JsonUpdate with current price data
78
+ */
79
+ getLatestPrice(params: LatestPriceRequest): Promise<JsonUpdate>;
80
+ /**
81
+ * Queries the price endpoint to get historical price data at a specific timestamp.
82
+ * @param params - Parameters for the price request including timestamp
83
+ * @returns Promise resolving to JsonUpdate with price data at the specified time
84
+ */
85
+ getPrice(params: PriceRequest): Promise<JsonUpdate>;
46
86
  }
@@ -1,24 +1,52 @@
1
+ import fetch from "cross-fetch";
1
2
  import WebSocket from "isomorphic-ws";
3
+ import { dummyLogger } from "ts-log";
4
+ import { DEFAULT_METADATA_SERVICE_URL, DEFAULT_PRICE_SERVICE_URL, } from "./constants.js";
2
5
  import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.js";
3
6
  import { WebSocketPool } from "./socket/websocket-pool.js";
4
7
  const UINT16_NUM_BYTES = 2;
5
8
  const UINT32_NUM_BYTES = 4;
6
9
  const UINT64_NUM_BYTES = 8;
7
10
  export class PythLazerClient {
11
+ token;
12
+ metadataServiceUrl;
13
+ priceServiceUrl;
14
+ logger;
8
15
  wsp;
9
- constructor(wsp) {
16
+ constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp) {
17
+ this.token = token;
18
+ this.metadataServiceUrl = metadataServiceUrl;
19
+ this.priceServiceUrl = priceServiceUrl;
20
+ this.logger = logger;
10
21
  this.wsp = wsp;
11
22
  }
23
+ /**
24
+ * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
25
+ * @throws Error if WebSocket pool is not configured
26
+ * @returns The WebSocket pool
27
+ */
28
+ getWebSocketPool() {
29
+ if (!this.wsp) {
30
+ throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
31
+ }
32
+ return this.wsp;
33
+ }
12
34
  /**
13
35
  * Creates a new PythLazerClient instance.
14
- * @param urls - List of WebSocket URLs of the Pyth Lazer service
15
- * @param token - The access token for authentication
16
- * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
17
- * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
36
+ * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
18
37
  */
19
38
  static async create(config) {
20
- const wsp = await WebSocketPool.create(config);
21
- return new PythLazerClient(wsp);
39
+ const token = config.token;
40
+ // Collect and remove trailing slash from URLs
41
+ const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
42
+ const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
43
+ const logger = config.logger ?? dummyLogger;
44
+ // If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
45
+ let wsp;
46
+ if (config.webSocketPoolConfig) {
47
+ wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, logger);
48
+ }
49
+ return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
22
50
  }
23
51
  /**
24
52
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -27,7 +55,8 @@ export class PythLazerClient {
27
55
  * or a binary response containing EVM, Solana, or parsed payload data.
28
56
  */
29
57
  addMessageListener(handler) {
30
- this.wsp.addMessageListener((data) => {
58
+ const wsp = this.getWebSocketPool();
59
+ wsp.addMessageListener((data) => {
31
60
  if (typeof data == "string") {
32
61
  handler({
33
62
  type: "json",
@@ -79,16 +108,19 @@ export class PythLazerClient {
79
108
  });
80
109
  }
81
110
  subscribe(request) {
111
+ const wsp = this.getWebSocketPool();
82
112
  if (request.type !== "subscribe") {
83
113
  throw new Error("Request must be a subscribe request");
84
114
  }
85
- this.wsp.addSubscription(request);
115
+ wsp.addSubscription(request);
86
116
  }
87
117
  unsubscribe(subscriptionId) {
88
- this.wsp.removeSubscription(subscriptionId);
118
+ const wsp = this.getWebSocketPool();
119
+ wsp.removeSubscription(subscriptionId);
89
120
  }
90
121
  send(request) {
91
- this.wsp.sendRequest(request);
122
+ const wsp = this.getWebSocketPool();
123
+ wsp.sendRequest(request);
92
124
  }
93
125
  /**
94
126
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
@@ -96,9 +128,103 @@ export class PythLazerClient {
96
128
  * @param handler - Function to be called when all connections are down
97
129
  */
98
130
  addAllConnectionsDownListener(handler) {
99
- this.wsp.addAllConnectionsDownListener(handler);
131
+ const wsp = this.getWebSocketPool();
132
+ wsp.addAllConnectionsDownListener(handler);
100
133
  }
101
134
  shutdown() {
102
- this.wsp.shutdown();
135
+ const wsp = this.getWebSocketPool();
136
+ wsp.shutdown();
137
+ }
138
+ /**
139
+ * Private helper method to make authenticated HTTP requests with Bearer token
140
+ * @param url - The URL to fetch
141
+ * @param options - Additional fetch options
142
+ * @returns Promise resolving to the fetch Response
143
+ */
144
+ async authenticatedFetch(url, options = {}) {
145
+ const headers = {
146
+ Authorization: `Bearer ${this.token}`,
147
+ ...options.headers,
148
+ };
149
+ return fetch(url, {
150
+ ...options,
151
+ headers,
152
+ });
153
+ }
154
+ /**
155
+ * Queries the symbols endpoint to get available price feed symbols.
156
+ * @param params - Optional query parameters to filter symbols
157
+ * @returns Promise resolving to array of symbol information
158
+ */
159
+ async getSymbols(params) {
160
+ const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
161
+ if (params?.query) {
162
+ url.searchParams.set("query", params.query);
163
+ }
164
+ if (params?.asset_type) {
165
+ url.searchParams.set("asset_type", params.asset_type);
166
+ }
167
+ try {
168
+ const response = await this.authenticatedFetch(url.toString());
169
+ if (!response.ok) {
170
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
171
+ }
172
+ return (await response.json());
173
+ }
174
+ catch (error) {
175
+ throw new Error(`Failed to fetch symbols: ${error instanceof Error ? error.message : String(error)}`);
176
+ }
177
+ }
178
+ /**
179
+ * Queries the latest price endpoint to get current price data.
180
+ * @param params - Parameters for the latest price request
181
+ * @returns Promise resolving to JsonUpdate with current price data
182
+ */
183
+ async getLatestPrice(params) {
184
+ const url = `${this.priceServiceUrl}/v1/latest_price`;
185
+ try {
186
+ const body = JSON.stringify(params);
187
+ this.logger.debug("getLatestPrice", { url, body });
188
+ const response = await this.authenticatedFetch(url, {
189
+ method: "POST",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ },
193
+ body: body,
194
+ });
195
+ if (!response.ok) {
196
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
197
+ }
198
+ return (await response.json());
199
+ }
200
+ catch (error) {
201
+ throw new Error(`Failed to fetch latest price: ${error instanceof Error ? error.message : String(error)}`);
202
+ }
203
+ }
204
+ /**
205
+ * Queries the price endpoint to get historical price data at a specific timestamp.
206
+ * @param params - Parameters for the price request including timestamp
207
+ * @returns Promise resolving to JsonUpdate with price data at the specified time
208
+ */
209
+ async getPrice(params) {
210
+ const url = `${this.priceServiceUrl}/v1/price`;
211
+ try {
212
+ const body = JSON.stringify(params);
213
+ this.logger.debug("getPrice", { url, body });
214
+ const response = await this.authenticatedFetch(url, {
215
+ method: "POST",
216
+ headers: {
217
+ "Content-Type": "application/json",
218
+ },
219
+ body: body,
220
+ });
221
+ if (!response.ok) {
222
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
223
+ }
224
+ return (await response.json());
225
+ }
226
+ catch (error) {
227
+ throw new Error(`Failed to fetch price: ${error instanceof Error ? error.message : String(error)}`);
228
+ }
103
229
  }
104
230
  }
@@ -1,2 +1,6 @@
1
1
  export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
2
2
  export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
+ export declare const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
4
+ export declare const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
5
+ export declare const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
+ export declare const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -1,2 +1,6 @@
1
1
  export const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
2
2
  export const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
+ export const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
4
+ export const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
5
+ export const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
+ export const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -37,6 +37,7 @@ export type JsonBinaryData = {
37
37
  };
38
38
  export type InvalidFeedSubscriptionDetails = {
39
39
  unknownIds: number[];
40
+ unknownSymbols: string[];
40
41
  unsupportedChannels: number[];
41
42
  unstable: number[];
42
43
  };
@@ -75,3 +76,49 @@ export declare const FORMAT_MAGICS_LE: {
75
76
  LE_ECDSA: number;
76
77
  LE_UNSIGNED: number;
77
78
  };
79
+ export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate";
80
+ export type SymbolsQueryParams = {
81
+ query?: string;
82
+ asset_type?: AssetType;
83
+ };
84
+ export type SymbolResponse = {
85
+ pyth_lazer_id: number;
86
+ name: string;
87
+ symbol: string;
88
+ description: string;
89
+ asset_type: string;
90
+ exponent: number;
91
+ min_publishers: number;
92
+ min_channel: string;
93
+ state: string;
94
+ schedule: string;
95
+ cmc_id?: number | null;
96
+ hermes_id?: string | null;
97
+ interval?: string | null;
98
+ };
99
+ export type LatestPriceRequest = {
100
+ priceFeedIds?: number[];
101
+ symbols?: string[];
102
+ properties: PriceFeedProperty[];
103
+ formats: Format[];
104
+ jsonBinaryEncoding?: JsonBinaryEncoding;
105
+ parsed?: boolean;
106
+ channel: Channel;
107
+ };
108
+ export type PriceRequest = {
109
+ timestamp: number;
110
+ priceFeedIds?: number[];
111
+ symbols?: string[];
112
+ properties: PriceFeedProperty[];
113
+ formats: Format[];
114
+ jsonBinaryEncoding?: JsonBinaryEncoding;
115
+ parsed?: boolean;
116
+ channel: Channel;
117
+ };
118
+ export type JsonUpdate = {
119
+ parsed?: ParsedPayload;
120
+ evm?: JsonBinaryData;
121
+ solana?: JsonBinaryData;
122
+ leEcdsa?: JsonBinaryData;
123
+ leUnsigned?: JsonBinaryData;
124
+ };
@@ -5,10 +5,8 @@ import type { Request } from "../protocol.js";
5
5
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
7
  export type WebSocketPoolConfig = {
8
- urls: string[];
9
- token: string;
8
+ urls?: string[];
10
9
  numConnections?: number;
11
- logger?: Logger;
12
10
  rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
13
11
  onError?: (error: ErrorEvent) => void;
14
12
  };
@@ -30,7 +28,7 @@ export declare class WebSocketPool {
30
28
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
31
29
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
32
30
  */
33
- static create(config: WebSocketPoolConfig): Promise<WebSocketPool>;
31
+ static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
34
32
  /**
35
33
  * Checks for error responses in JSON messages and throws appropriate errors
36
34
  */
@@ -2,6 +2,7 @@ import TTLCache from "@isaacs/ttlcache";
2
2
  import WebSocket from "isomorphic-ws";
3
3
  import { dummyLogger } from "ts-log";
4
4
  import { ResilientWebSocket } from "./resilient-websocket.js";
5
+ import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL, } from "../constants.js";
5
6
  const DEFAULT_NUM_CONNECTIONS = 4;
6
7
  export class WebSocketPool {
7
8
  logger;
@@ -32,29 +33,30 @@ export class WebSocketPool {
32
33
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
33
34
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
34
35
  */
35
- static async create(config) {
36
- if (config.urls.length === 0) {
37
- throw new Error("No URLs provided");
38
- }
39
- const logger = config.logger ?? dummyLogger;
40
- const pool = new WebSocketPool(logger);
36
+ static async create(config, token, logger) {
37
+ const urls = config.urls ?? [
38
+ DEFAULT_STREAM_SERVICE_0_URL,
39
+ DEFAULT_STREAM_SERVICE_1_URL,
40
+ ];
41
+ const log = logger ?? dummyLogger;
42
+ const pool = new WebSocketPool(log);
41
43
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
42
44
  for (let i = 0; i < numConnections; i++) {
43
- const url = config.urls[i % config.urls.length];
45
+ const url = urls[i % urls.length];
44
46
  if (!url) {
45
47
  throw new Error(`URLs must not be null or empty`);
46
48
  }
47
49
  const wsOptions = {
48
50
  ...config.rwsConfig?.wsOptions,
49
51
  headers: {
50
- Authorization: `Bearer ${config.token}`,
52
+ Authorization: `Bearer ${token}`,
51
53
  },
52
54
  };
53
55
  const rws = new ResilientWebSocket({
54
56
  ...config.rwsConfig,
55
57
  endpoint: url,
56
58
  wsOptions,
57
- logger,
59
+ logger: log,
58
60
  });
59
61
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
60
62
  // the connection and call the onReconnect callback.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pythnetwork/pyth-lazer-sdk",
3
- "version": "2.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Pyth Lazer SDK",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -48,6 +48,7 @@
48
48
  "license": "Apache-2.0",
49
49
  "dependencies": {
50
50
  "@isaacs/ttlcache": "^1.4.1",
51
+ "cross-fetch": "^4.0.0",
51
52
  "isomorphic-ws": "^5.0.0",
52
53
  "ts-log": "^2.2.7",
53
54
  "ws": "^8.18.0"
@@ -60,7 +61,9 @@
60
61
  "test:types": "tsc",
61
62
  "test:format": "prettier --check .",
62
63
  "fix:format": "prettier --write .",
63
- "example": "node --loader ts-node/esm examples/index.js",
64
+ "example:streaming": "node --loader ts-node/esm examples/streaming.js",
65
+ "example:history": "node --loader ts-node/esm examples/history.js",
66
+ "example:symbols": "node --loader ts-node/esm examples/symbols.js",
64
67
  "doc": "typedoc --out docs/typedoc src"
65
68
  }
66
69
  }