@pythnetwork/price-service-client 1.4.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.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Pyth Contributors.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,119 @@
1
+ import { HexString, PriceFeed } from "@pythnetwork/price-service-sdk";
2
+ import { Logger } from "ts-log";
3
+ export type DurationInMs = number;
4
+ export type PriceFeedRequestConfig = {
5
+ verbose?: boolean;
6
+ binary?: boolean;
7
+ };
8
+ export type PriceServiceConnectionConfig = {
9
+ timeout?: DurationInMs;
10
+ /**
11
+ * Number of times a HTTP request will be retried before the API returns a failure. Default: 3.
12
+ *
13
+ * The connection uses exponential back-off for the delay between retries. However,
14
+ * it will timeout regardless of the retries at the configured `timeout` time.
15
+ */
16
+ httpRetries?: number;
17
+ logger?: Logger;
18
+ verbose?: boolean;
19
+ priceFeedRequestConfig?: PriceFeedRequestConfig;
20
+ };
21
+ export type PriceFeedUpdateCallback = (priceFeed: PriceFeed) => void;
22
+ export declare class PriceServiceConnection {
23
+ private httpClient;
24
+ private priceFeedCallbacks;
25
+ private wsClient;
26
+ private wsEndpoint;
27
+ private logger;
28
+ private priceFeedRequestConfig;
29
+ /**
30
+ * Custom handler for web socket errors (connection and message parsing).
31
+ *
32
+ * Default handler only logs the errors.
33
+ */
34
+ onWsError: (error: Error) => void;
35
+ /**
36
+ * Constructs a new Connection.
37
+ *
38
+ * @param endpoint endpoint URL to the price service. Example: https://website/example/
39
+ * @param config Optional PriceServiceConnectionConfig for custom configurations.
40
+ */
41
+ constructor(endpoint: string, config?: PriceServiceConnectionConfig);
42
+ /**
43
+ * Fetch Latest PriceFeeds of given price ids.
44
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
45
+ *
46
+ * @param priceIds Array of hex-encoded price ids.
47
+ * @returns Array of PriceFeeds
48
+ */
49
+ getLatestPriceFeeds(priceIds: HexString[]): Promise<PriceFeed[] | undefined>;
50
+ /**
51
+ * Fetch latest VAA of given price ids.
52
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
53
+ *
54
+ * This function is coupled to wormhole implemntation.
55
+ *
56
+ * @param priceIds Array of hex-encoded price ids.
57
+ * @returns Array of base64 encoded VAAs.
58
+ */
59
+ getLatestVaas(priceIds: HexString[]): Promise<string[]>;
60
+ /**
61
+ * Fetch the earliest VAA of the given price id that is published since the given publish time.
62
+ * This will throw an error if the given publish time is in the future, or if the publish time
63
+ * is old and the price service endpoint does not have a db backend for historical requests.
64
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price id)
65
+ *
66
+ * This function is coupled to wormhole implemntation.
67
+ *
68
+ * @param priceId Hex-encoded price id.
69
+ * @param publishTime Epoch timestamp in seconds.
70
+ * @returns Tuple of VAA and publishTime.
71
+ */
72
+ getVaa(priceId: HexString, publishTime: EpochTimeStamp): Promise<[string, EpochTimeStamp]>;
73
+ /**
74
+ * Fetch the list of available price feed ids.
75
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response.
76
+ *
77
+ * @returns Array of hex-encoded price ids.
78
+ */
79
+ getPriceFeedIds(): Promise<HexString[]>;
80
+ /**
81
+ * Subscribe to updates for given price ids.
82
+ *
83
+ * It will start a websocket connection if it's not started yet.
84
+ * Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
85
+ * it calls `connection.onWsError`. If you want to handle the errors you should set the
86
+ * `onWsError` function to your custom error handler.
87
+ *
88
+ * @param priceIds Array of hex-encoded price ids.
89
+ * @param cb Callback function that is called with a PriceFeed upon updates to given price ids.
90
+ */
91
+ subscribePriceFeedUpdates(priceIds: HexString[], cb: PriceFeedUpdateCallback): Promise<void>;
92
+ /**
93
+ * Unsubscribe from updates for given price ids.
94
+ *
95
+ * It will close the websocket connection if it's not subscribed to any price feed updates anymore.
96
+ * Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
97
+ * it calls `connection.onWsError`. If you want to handle the errors you should set the
98
+ * `onWsError` function to your custom error handler.
99
+ *
100
+ * @param priceIds Array of hex-encoded price ids.
101
+ * @param cb Optional callback, if set it will only unsubscribe this callback from updates for given price ids.
102
+ */
103
+ unsubscribePriceFeedUpdates(priceIds: HexString[], cb?: PriceFeedUpdateCallback): Promise<void>;
104
+ /**
105
+ * Starts connection websocket.
106
+ *
107
+ * This function is called automatically upon subscribing to price feed updates.
108
+ */
109
+ startWebSocket(): Promise<void>;
110
+ /**
111
+ * Closes connection websocket.
112
+ *
113
+ * At termination, the websocket should be closed to finish the
114
+ * process elegantly. It will automatically close when the connection
115
+ * is subscribed to no price feeds.
116
+ */
117
+ closeWebSocket(): void;
118
+ }
119
+ //# sourceMappingURL=PriceServiceConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PriceServiceConnection.d.ts","sourceRoot":"","sources":["../src/PriceServiceConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAItE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,MAAM,sBAAsB,GAAG;IAEnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAEzC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACjD,CAAC;AAsBF,MAAM,MAAM,uBAAuB,GAAG,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;AAErE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,UAAU,CAAgB;IAElC,OAAO,CAAC,kBAAkB,CAA+C;IACzE,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO,CAAC,MAAM,CAAqB;IAEnC,OAAO,CAAC,sBAAsB,CAAyB;IAEvD;;;;OAIG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAElC;;;;;OAKG;gBACS,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,4BAA4B;IAyBnE;;;;;;OAMG;IACG,mBAAmB,CACvB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC;IAkBnC;;;;;;;;OAQG;IACG,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS7D;;;;;;;;;;;OAWG;IACG,MAAM,CACV,OAAO,EAAE,SAAS,EAClB,WAAW,EAAE,cAAc,GAC1B,OAAO,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAUpC;;;;;OAKG;IACG,eAAe,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAK7C;;;;;;;;;;OAUG;IACG,yBAAyB,CAC7B,QAAQ,EAAE,SAAS,EAAE,EACrB,EAAE,EAAE,uBAAuB;IA6B7B;;;;;;;;;;OAUG;IACG,2BAA2B,CAC/B,QAAQ,EAAE,SAAS,EAAE,EACrB,EAAE,CAAC,EAAE,uBAAuB;IA4C9B;;;;OAIG;IACG,cAAc;IAwEpB;;;;;;OAMG;IACH,cAAc;CAKf"}
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PriceServiceConnection = void 0;
4
+ const price_service_sdk_1 = require("@pythnetwork/price-service-sdk");
5
+ const axios_1 = require("axios");
6
+ const axios_retry_1 = require("axios-retry");
7
+ const ResillientWebSocket_1 = require("./ResillientWebSocket");
8
+ const utils_1 = require("./utils");
9
+ class PriceServiceConnection {
10
+ httpClient;
11
+ priceFeedCallbacks;
12
+ wsClient;
13
+ wsEndpoint;
14
+ logger;
15
+ priceFeedRequestConfig;
16
+ /**
17
+ * Custom handler for web socket errors (connection and message parsing).
18
+ *
19
+ * Default handler only logs the errors.
20
+ */
21
+ onWsError;
22
+ /**
23
+ * Constructs a new Connection.
24
+ *
25
+ * @param endpoint endpoint URL to the price service. Example: https://website/example/
26
+ * @param config Optional PriceServiceConnectionConfig for custom configurations.
27
+ */
28
+ constructor(endpoint, config) {
29
+ this.httpClient = axios_1.default.create({
30
+ baseURL: endpoint,
31
+ timeout: config?.timeout || 5000,
32
+ });
33
+ (0, axios_retry_1.default)(this.httpClient, {
34
+ retries: config?.httpRetries || 3,
35
+ retryDelay: axios_retry_1.default.exponentialDelay,
36
+ });
37
+ this.priceFeedRequestConfig = {
38
+ binary: config?.priceFeedRequestConfig?.binary,
39
+ verbose: config?.priceFeedRequestConfig?.verbose ?? config?.verbose,
40
+ };
41
+ this.priceFeedCallbacks = new Map();
42
+ this.logger = config?.logger;
43
+ this.onWsError = (error) => {
44
+ this.logger?.error(error);
45
+ };
46
+ this.wsEndpoint = (0, utils_1.makeWebsocketUrl)(endpoint);
47
+ }
48
+ /**
49
+ * Fetch Latest PriceFeeds of given price ids.
50
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
51
+ *
52
+ * @param priceIds Array of hex-encoded price ids.
53
+ * @returns Array of PriceFeeds
54
+ */
55
+ async getLatestPriceFeeds(priceIds) {
56
+ if (priceIds.length === 0) {
57
+ return [];
58
+ }
59
+ const response = await this.httpClient.get("/api/latest_price_feeds", {
60
+ params: {
61
+ ids: priceIds,
62
+ verbose: this.priceFeedRequestConfig.verbose,
63
+ binary: this.priceFeedRequestConfig.binary,
64
+ },
65
+ });
66
+ const priceFeedsJson = response.data;
67
+ return priceFeedsJson.map((priceFeedJson) => price_service_sdk_1.PriceFeed.fromJson(priceFeedJson));
68
+ }
69
+ /**
70
+ * Fetch latest VAA of given price ids.
71
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
72
+ *
73
+ * This function is coupled to wormhole implemntation.
74
+ *
75
+ * @param priceIds Array of hex-encoded price ids.
76
+ * @returns Array of base64 encoded VAAs.
77
+ */
78
+ async getLatestVaas(priceIds) {
79
+ const response = await this.httpClient.get("/api/latest_vaas", {
80
+ params: {
81
+ ids: priceIds,
82
+ },
83
+ });
84
+ return response.data;
85
+ }
86
+ /**
87
+ * Fetch the earliest VAA of the given price id that is published since the given publish time.
88
+ * This will throw an error if the given publish time is in the future, or if the publish time
89
+ * is old and the price service endpoint does not have a db backend for historical requests.
90
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price id)
91
+ *
92
+ * This function is coupled to wormhole implemntation.
93
+ *
94
+ * @param priceId Hex-encoded price id.
95
+ * @param publishTime Epoch timestamp in seconds.
96
+ * @returns Tuple of VAA and publishTime.
97
+ */
98
+ async getVaa(priceId, publishTime) {
99
+ const response = await this.httpClient.get("/api/get_vaa", {
100
+ params: {
101
+ id: priceId,
102
+ publish_time: publishTime,
103
+ },
104
+ });
105
+ return [response.data.vaa, response.data.publishTime];
106
+ }
107
+ /**
108
+ * Fetch the list of available price feed ids.
109
+ * This will throw an axios error if there is a network problem or the price service returns a non-ok response.
110
+ *
111
+ * @returns Array of hex-encoded price ids.
112
+ */
113
+ async getPriceFeedIds() {
114
+ const response = await this.httpClient.get("/api/price_feed_ids");
115
+ return response.data;
116
+ }
117
+ /**
118
+ * Subscribe to updates for given price ids.
119
+ *
120
+ * It will start a websocket connection if it's not started yet.
121
+ * Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
122
+ * it calls `connection.onWsError`. If you want to handle the errors you should set the
123
+ * `onWsError` function to your custom error handler.
124
+ *
125
+ * @param priceIds Array of hex-encoded price ids.
126
+ * @param cb Callback function that is called with a PriceFeed upon updates to given price ids.
127
+ */
128
+ async subscribePriceFeedUpdates(priceIds, cb) {
129
+ if (this.wsClient === undefined) {
130
+ await this.startWebSocket();
131
+ }
132
+ priceIds = priceIds.map((priceId) => (0, utils_1.removeLeading0xIfExists)(priceId));
133
+ const newPriceIds = [];
134
+ for (const id of priceIds) {
135
+ if (!this.priceFeedCallbacks.has(id)) {
136
+ this.priceFeedCallbacks.set(id, new Set());
137
+ newPriceIds.push(id);
138
+ }
139
+ this.priceFeedCallbacks.get(id).add(cb);
140
+ }
141
+ const message = {
142
+ ids: newPriceIds,
143
+ type: "subscribe",
144
+ verbose: this.priceFeedRequestConfig.verbose,
145
+ binary: this.priceFeedRequestConfig.binary,
146
+ };
147
+ await this.wsClient?.send(JSON.stringify(message));
148
+ }
149
+ /**
150
+ * Unsubscribe from updates for given price ids.
151
+ *
152
+ * It will close the websocket connection if it's not subscribed to any price feed updates anymore.
153
+ * Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
154
+ * it calls `connection.onWsError`. If you want to handle the errors you should set the
155
+ * `onWsError` function to your custom error handler.
156
+ *
157
+ * @param priceIds Array of hex-encoded price ids.
158
+ * @param cb Optional callback, if set it will only unsubscribe this callback from updates for given price ids.
159
+ */
160
+ async unsubscribePriceFeedUpdates(priceIds, cb) {
161
+ if (this.wsClient === undefined) {
162
+ await this.startWebSocket();
163
+ }
164
+ priceIds = priceIds.map((priceId) => (0, utils_1.removeLeading0xIfExists)(priceId));
165
+ const removedPriceIds = [];
166
+ for (const id of priceIds) {
167
+ if (this.priceFeedCallbacks.has(id)) {
168
+ let idRemoved = false;
169
+ if (cb === undefined) {
170
+ this.priceFeedCallbacks.delete(id);
171
+ idRemoved = true;
172
+ }
173
+ else {
174
+ this.priceFeedCallbacks.get(id).delete(cb);
175
+ if (this.priceFeedCallbacks.get(id).size === 0) {
176
+ this.priceFeedCallbacks.delete(id);
177
+ idRemoved = true;
178
+ }
179
+ }
180
+ if (idRemoved) {
181
+ removedPriceIds.push(id);
182
+ }
183
+ }
184
+ }
185
+ const message = {
186
+ ids: removedPriceIds,
187
+ type: "unsubscribe",
188
+ };
189
+ await this.wsClient?.send(JSON.stringify(message));
190
+ if (this.priceFeedCallbacks.size === 0) {
191
+ this.closeWebSocket();
192
+ }
193
+ }
194
+ /**
195
+ * Starts connection websocket.
196
+ *
197
+ * This function is called automatically upon subscribing to price feed updates.
198
+ */
199
+ async startWebSocket() {
200
+ if (this.wsEndpoint === undefined) {
201
+ throw new Error("Websocket endpoint is undefined.");
202
+ }
203
+ this.wsClient = new ResillientWebSocket_1.ResilientWebSocket(this.wsEndpoint, this.logger);
204
+ this.wsClient.onError = this.onWsError;
205
+ this.wsClient.onReconnect = () => {
206
+ if (this.priceFeedCallbacks.size > 0) {
207
+ const message = {
208
+ ids: Array.from(this.priceFeedCallbacks.keys()),
209
+ type: "subscribe",
210
+ verbose: this.priceFeedRequestConfig.verbose,
211
+ binary: this.priceFeedRequestConfig.binary,
212
+ };
213
+ this.logger?.info("Resubscribing to existing price feeds.");
214
+ this.wsClient?.send(JSON.stringify(message));
215
+ }
216
+ };
217
+ this.wsClient.onMessage = (data) => {
218
+ this.logger?.info(`Received message ${data.toString()}`);
219
+ let message;
220
+ try {
221
+ message = JSON.parse(data.toString());
222
+ }
223
+ catch (e) {
224
+ this.logger?.error(`Error parsing message ${data.toString()} as JSON.`);
225
+ this.logger?.error(e);
226
+ this.onWsError(e);
227
+ return;
228
+ }
229
+ if (message.type === "response") {
230
+ if (message.status === "error") {
231
+ this.logger?.error(`Error response from the websocket server ${message.error}.`);
232
+ this.onWsError(new Error(message.error));
233
+ }
234
+ }
235
+ else if (message.type === "price_update") {
236
+ let priceFeed;
237
+ try {
238
+ priceFeed = price_service_sdk_1.PriceFeed.fromJson(message.price_feed);
239
+ }
240
+ catch (e) {
241
+ this.logger?.error(`Error parsing price feeds from message ${data.toString()}.`);
242
+ this.logger?.error(e);
243
+ this.onWsError(e);
244
+ return;
245
+ }
246
+ if (this.priceFeedCallbacks.has(priceFeed.id)) {
247
+ for (const cb of this.priceFeedCallbacks.get(priceFeed.id)) {
248
+ cb(priceFeed);
249
+ }
250
+ }
251
+ }
252
+ else {
253
+ this.logger?.warn(`Ignoring unsupported server response ${data.toString()}.`);
254
+ }
255
+ };
256
+ await this.wsClient.startWebSocket();
257
+ }
258
+ /**
259
+ * Closes connection websocket.
260
+ *
261
+ * At termination, the websocket should be closed to finish the
262
+ * process elegantly. It will automatically close when the connection
263
+ * is subscribed to no price feeds.
264
+ */
265
+ closeWebSocket() {
266
+ this.wsClient?.closeWebSocket();
267
+ this.wsClient = undefined;
268
+ this.priceFeedCallbacks.clear();
269
+ }
270
+ }
271
+ exports.PriceServiceConnection = PriceServiceConnection;
@@ -0,0 +1,39 @@
1
+ import * as WebSocket from "isomorphic-ws";
2
+ import { Logger } from "ts-log";
3
+ /**
4
+ * This class wraps websocket to provide a resilient web socket client.
5
+ *
6
+ * It will reconnect if connection fails with exponential backoff. Also, in node, it will reconnect
7
+ * if it receives no ping request from server within a while as indication of timeout (assuming
8
+ * the server sends it regularly).
9
+ *
10
+ * This class also logs events if logger is given and by replacing onError method you can handle
11
+ * connection errors yourself (e.g: do not retry and close the connection).
12
+ */
13
+ export declare class ResilientWebSocket {
14
+ private endpoint;
15
+ private wsClient;
16
+ private wsUserClosed;
17
+ private wsFailedAttempts;
18
+ private pingTimeout;
19
+ private logger;
20
+ onError: (error: Error) => void;
21
+ onMessage: (data: WebSocket.Data) => void;
22
+ onReconnect: () => void;
23
+ constructor(endpoint: string, logger?: Logger);
24
+ send(data: any): Promise<void>;
25
+ startWebSocket(): Promise<void>;
26
+ /**
27
+ * Heartbeat is only enabled in node clients because they support handling
28
+ * ping-pong events.
29
+ *
30
+ * This approach only works when server constantly pings the clients which.
31
+ * Otherwise you might consider sending ping and acting on pong responses
32
+ * yourself.
33
+ */
34
+ private heartbeat;
35
+ private waitForMaybeReadyWebSocket;
36
+ private restartUnexpectedClosedWebsocket;
37
+ closeWebSocket(): void;
38
+ }
39
+ //# sourceMappingURL=ResillientWebSocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResillientWebSocket.d.ts","sourceRoot":"","sources":["../src/ResillientWebSocket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC;;;;;;;;;GASG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,YAAY,CAAU;IAC9B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,MAAM,CAAqB;IAEnC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,SAAS,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC;IAC1C,WAAW,EAAE,MAAM,IAAI,CAAC;gBAEZ,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAavC,IAAI,CAAC,IAAI,EAAE,GAAG;IAcd,cAAc;IAqDpB;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;YAcH,0BAA0B;YAgB1B,gCAAgC;IAkB9C,cAAc;CAQf"}
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResilientWebSocket = void 0;
4
+ const WebSocket = require("isomorphic-ws");
5
+ const PING_TIMEOUT_DURATION = 30000 + 3000; // It is 30s on the server and 3s is added for delays
6
+ /**
7
+ * This class wraps websocket to provide a resilient web socket client.
8
+ *
9
+ * It will reconnect if connection fails with exponential backoff. Also, in node, it will reconnect
10
+ * if it receives no ping request from server within a while as indication of timeout (assuming
11
+ * the server sends it regularly).
12
+ *
13
+ * This class also logs events if logger is given and by replacing onError method you can handle
14
+ * connection errors yourself (e.g: do not retry and close the connection).
15
+ */
16
+ class ResilientWebSocket {
17
+ endpoint;
18
+ wsClient;
19
+ wsUserClosed;
20
+ wsFailedAttempts;
21
+ pingTimeout;
22
+ logger;
23
+ onError;
24
+ onMessage;
25
+ onReconnect;
26
+ constructor(endpoint, logger) {
27
+ this.endpoint = endpoint;
28
+ this.logger = logger;
29
+ this.wsFailedAttempts = 0;
30
+ this.onError = (error) => {
31
+ this.logger?.error(error);
32
+ };
33
+ this.wsUserClosed = true;
34
+ this.onMessage = () => { };
35
+ this.onReconnect = () => { };
36
+ }
37
+ async send(data) {
38
+ this.logger?.info(`Sending ${data}`);
39
+ await this.waitForMaybeReadyWebSocket();
40
+ if (this.wsClient === undefined) {
41
+ this.logger?.error("Couldn't connect to the websocket server. Error callback is called.");
42
+ }
43
+ else {
44
+ this.wsClient?.send(data);
45
+ }
46
+ }
47
+ async startWebSocket() {
48
+ if (this.wsClient !== undefined) {
49
+ return;
50
+ }
51
+ this.logger?.info(`Creating Web Socket client`);
52
+ this.wsClient = new WebSocket(this.endpoint);
53
+ this.wsUserClosed = false;
54
+ this.wsClient.onopen = () => {
55
+ this.wsFailedAttempts = 0;
56
+ // Ping handler is undefined in browser side so heartbeat is disabled.
57
+ if (this.wsClient.on !== undefined) {
58
+ this.heartbeat();
59
+ }
60
+ };
61
+ this.wsClient.onerror = (event) => {
62
+ this.onError(event.error);
63
+ };
64
+ this.wsClient.onmessage = (event) => {
65
+ this.onMessage(event.data);
66
+ };
67
+ this.wsClient.onclose = async () => {
68
+ if (this.pingTimeout !== undefined) {
69
+ clearInterval(this.pingTimeout);
70
+ }
71
+ if (this.wsUserClosed === false) {
72
+ this.wsFailedAttempts += 1;
73
+ this.wsClient = undefined;
74
+ const waitTime = expoBackoff(this.wsFailedAttempts);
75
+ this.logger?.error(`Connection closed unexpectedly or because of timeout. Reconnecting after ${waitTime}ms.`);
76
+ await sleep(waitTime);
77
+ this.restartUnexpectedClosedWebsocket();
78
+ }
79
+ else {
80
+ this.logger?.info("The connection has been closed successfully.");
81
+ }
82
+ };
83
+ if (this.wsClient.on !== undefined) {
84
+ // Ping handler is undefined in browser side
85
+ this.wsClient.on("ping", this.heartbeat.bind(this));
86
+ }
87
+ }
88
+ /**
89
+ * Heartbeat is only enabled in node clients because they support handling
90
+ * ping-pong events.
91
+ *
92
+ * This approach only works when server constantly pings the clients which.
93
+ * Otherwise you might consider sending ping and acting on pong responses
94
+ * yourself.
95
+ */
96
+ heartbeat() {
97
+ this.logger?.info("Heartbeat");
98
+ if (this.pingTimeout !== undefined) {
99
+ clearTimeout(this.pingTimeout);
100
+ }
101
+ this.pingTimeout = setTimeout(() => {
102
+ this.logger?.warn(`Connection timed out. Reconnecting...`);
103
+ this.wsClient?.terminate();
104
+ this.restartUnexpectedClosedWebsocket();
105
+ }, PING_TIMEOUT_DURATION);
106
+ }
107
+ async waitForMaybeReadyWebSocket() {
108
+ let waitedTime = 0;
109
+ while (this.wsClient !== undefined &&
110
+ this.wsClient.readyState !== this.wsClient.OPEN) {
111
+ if (waitedTime > 1000) {
112
+ this.wsClient.close();
113
+ return;
114
+ }
115
+ else {
116
+ waitedTime += 10;
117
+ await sleep(10);
118
+ }
119
+ }
120
+ }
121
+ async restartUnexpectedClosedWebsocket() {
122
+ if (this.wsUserClosed === true) {
123
+ return;
124
+ }
125
+ await this.startWebSocket();
126
+ await this.waitForMaybeReadyWebSocket();
127
+ if (this.wsClient === undefined) {
128
+ this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
129
+ return;
130
+ }
131
+ this.onReconnect();
132
+ }
133
+ closeWebSocket() {
134
+ if (this.wsClient !== undefined) {
135
+ const client = this.wsClient;
136
+ this.wsClient = undefined;
137
+ client.close();
138
+ }
139
+ this.wsUserClosed = true;
140
+ }
141
+ }
142
+ exports.ResilientWebSocket = ResilientWebSocket;
143
+ async function sleep(ms) {
144
+ return new Promise((resolve) => setTimeout(resolve, ms));
145
+ }
146
+ function expoBackoff(attempts) {
147
+ return 2 ** attempts * 100;
148
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PriceServiceClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PriceServiceClient.d.ts","sourceRoot":"","sources":["../../src/examples/PriceServiceClient.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const yargs_1 = require("yargs");
4
+ const helpers_1 = require("yargs/helpers");
5
+ const index_1 = require("../index");
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
10
+ .option("endpoint", {
11
+ description: "Endpoint URL for the price service. e.g: https://endpoint/example",
12
+ type: "string",
13
+ required: true,
14
+ })
15
+ .option("price-ids", {
16
+ description: "Space separated price feed ids (in hex without leading 0x) to fetch." +
17
+ " e.g: f9c0172ba10dfa4d19088d...",
18
+ type: "array",
19
+ required: true,
20
+ })
21
+ .help()
22
+ .alias("help", "h")
23
+ .parserConfiguration({
24
+ "parse-numbers": false,
25
+ })
26
+ .parseSync();
27
+ async function run() {
28
+ const connection = new index_1.PriceServiceConnection(argv.endpoint, {
29
+ logger: console,
30
+ priceFeedRequestConfig: {
31
+ binary: true,
32
+ },
33
+ });
34
+ const priceIds = argv.priceIds;
35
+ const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
36
+ console.log(priceFeeds);
37
+ console.log(priceFeeds?.at(0)?.getPriceNoOlderThan(60));
38
+ console.log("Subscribing to price feed updates.");
39
+ await connection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
40
+ console.log(`Current price for ${priceFeed.id}: ${JSON.stringify(priceFeed.getPriceNoOlderThan(60))}.`);
41
+ console.log(priceFeed.getVAA());
42
+ });
43
+ await sleep(600000);
44
+ // To close the websocket you should either unsubscribe from all
45
+ // price feeds or call `connection.stopWebSocket()` directly.
46
+ console.log("Unsubscribing from price feed updates.");
47
+ await connection.unsubscribePriceFeedUpdates(priceIds);
48
+ // connection.closeWebSocket();
49
+ }
50
+ run();
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { DurationInMs, PriceServiceConnection, PriceServiceConnectionConfig, } from "./PriceServiceConnection";
2
+ export { HexString, PriceFeedMetadata, PriceFeed, Price, UnixTimestamp, } from "@pythnetwork/price-service-sdk";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,SAAS,EACT,KAAK,EACL,aAAa,GACd,MAAM,gCAAgC,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Price = exports.PriceFeed = exports.PriceFeedMetadata = exports.PriceServiceConnection = void 0;
4
+ var PriceServiceConnection_1 = require("./PriceServiceConnection");
5
+ Object.defineProperty(exports, "PriceServiceConnection", { enumerable: true, get: function () { return PriceServiceConnection_1.PriceServiceConnection; } });
6
+ var price_service_sdk_1 = require("@pythnetwork/price-service-sdk");
7
+ Object.defineProperty(exports, "PriceFeedMetadata", { enumerable: true, get: function () { return price_service_sdk_1.PriceFeedMetadata; } });
8
+ Object.defineProperty(exports, "PriceFeed", { enumerable: true, get: function () { return price_service_sdk_1.PriceFeed; } });
9
+ Object.defineProperty(exports, "Price", { enumerable: true, get: function () { return price_service_sdk_1.Price; } });
package/lib/utils.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { HexString } from "@pythnetwork/price-service-sdk";
2
+ /**
3
+ * Convert http(s) endpoint to ws(s) endpoint.
4
+ *
5
+ * @param endpoint Http(s) protocol endpoint
6
+ * @returns Ws(s) protocol endpoint of the same address
7
+ */
8
+ export declare function makeWebsocketUrl(endpoint: string): string;
9
+ export declare function removeLeading0xIfExists(id: HexString): HexString;
10
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,UAOhD;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,CAMhE"}
package/lib/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeLeading0xIfExists = exports.makeWebsocketUrl = void 0;
4
+ /**
5
+ * Convert http(s) endpoint to ws(s) endpoint.
6
+ *
7
+ * @param endpoint Http(s) protocol endpoint
8
+ * @returns Ws(s) protocol endpoint of the same address
9
+ */
10
+ function makeWebsocketUrl(endpoint) {
11
+ const url = new URL("ws", endpoint);
12
+ const useHttps = url.protocol === "https:";
13
+ url.protocol = useHttps ? "wss:" : "ws:";
14
+ return url.toString();
15
+ }
16
+ exports.makeWebsocketUrl = makeWebsocketUrl;
17
+ function removeLeading0xIfExists(id) {
18
+ if (id.startsWith("0x")) {
19
+ return id.substring(2);
20
+ }
21
+ else {
22
+ return id;
23
+ }
24
+ }
25
+ exports.removeLeading0xIfExists = removeLeading0xIfExists;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@pythnetwork/price-service-client",
3
+ "version": "1.4.0",
4
+ "description": "Pyth price service client",
5
+ "author": {
6
+ "name": "Pyth Data Association"
7
+ },
8
+ "homepage": "https://pyth.network",
9
+ "main": "lib/index.js",
10
+ "types": "lib/index.d.ts",
11
+ "files": [
12
+ "lib/**/*"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/pyth-network/pyth-crosschain",
17
+ "directory": "price_service/client/js"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "test": "jest --testPathIgnorePatterns=.*.e2e.test.ts --passWithNoTests",
24
+ "test:e2e": "jest --testPathPattern=.*.e2e.test.ts",
25
+ "build": "tsc",
26
+ "example": "npm run build && node lib/examples/PriceServiceClient.js",
27
+ "format": "prettier --write \"src/**/*.ts\"",
28
+ "lint": "eslint src/",
29
+ "prepublishOnly": "npm run build && npm test && npm run lint",
30
+ "preversion": "npm run lint",
31
+ "version": "npm run format && git add -A src"
32
+ },
33
+ "keywords": [
34
+ "pyth",
35
+ "oracle"
36
+ ],
37
+ "license": "Apache-2.0",
38
+ "devDependencies": {
39
+ "@types/jest": "^29.4.0",
40
+ "@types/yargs": "^17.0.10",
41
+ "@typescript-eslint/eslint-plugin": "^5.21.0",
42
+ "@typescript-eslint/parser": "^5.21.0",
43
+ "eslint": "^8.14.0",
44
+ "jest": "^29.4.0",
45
+ "prettier": "^2.6.2",
46
+ "ts-jest": "^29.0.5",
47
+ "typescript": "^4.6.3",
48
+ "yargs": "^17.4.1"
49
+ },
50
+ "dependencies": {
51
+ "@pythnetwork/price-service-sdk": "*",
52
+ "@types/ws": "^8.5.3",
53
+ "axios": "^1.2.5",
54
+ "axios-retry": "^3.4.0",
55
+ "isomorphic-ws": "^4.0.1",
56
+ "ts-log": "^2.2.4",
57
+ "ws": "^8.6.0"
58
+ },
59
+ "gitHead": "94f38fdd745ba4211e4aee0b361b2f6d82522f8c"
60
+ }