@pythnetwork/price-service-client 1.10.0 → 1.11.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/dist/{PriceServiceConnection.cjs → cjs/PriceServiceConnection.cjs} +13 -1
- package/dist/{PriceServiceConnection.d.ts → cjs/PriceServiceConnection.d.ts} +8 -0
- package/dist/esm/PriceServiceConnection.d.ts +139 -0
- package/dist/esm/PriceServiceConnection.mjs +307 -0
- package/dist/esm/ResillientWebSocket.d.ts +38 -0
- package/dist/esm/ResillientWebSocket.mjs +138 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/utils.d.ts +9 -0
- package/dist/esm/utils.mjs +14 -0
- package/package.json +38 -14
- /package/dist/{ResillientWebSocket.cjs → cjs/ResillientWebSocket.cjs} +0 -0
- /package/dist/{ResillientWebSocket.d.ts → cjs/ResillientWebSocket.d.ts} +0 -0
- /package/dist/{index.cjs → cjs/index.cjs} +0 -0
- /package/dist/{index.d.ts → cjs/index.d.ts} +0 -0
- /package/dist/{package.json → cjs/package.json} +0 -0
- /package/dist/{utils.cjs → cjs/utils.cjs} +0 -0
- /package/dist/{utils.d.ts → cjs/utils.d.ts} +0 -0
|
@@ -25,6 +25,7 @@ class PriceServiceConnection {
|
|
|
25
25
|
wsEndpoint;
|
|
26
26
|
logger;
|
|
27
27
|
priceFeedRequestConfig;
|
|
28
|
+
accessToken;
|
|
28
29
|
/**
|
|
29
30
|
* Custom handler for web socket errors (connection and message parsing).
|
|
30
31
|
*
|
|
@@ -36,9 +37,13 @@ class PriceServiceConnection {
|
|
|
36
37
|
* @param endpoint - endpoint URL to the price service. Example: https://website/example/
|
|
37
38
|
* @param config - Optional PriceServiceConnectionConfig for custom configurations.
|
|
38
39
|
*/ constructor(endpoint, config){
|
|
40
|
+
this.accessToken = config?.accessToken;
|
|
39
41
|
this.httpClient = _axios.default.create({
|
|
40
42
|
baseURL: endpoint,
|
|
41
|
-
timeout: config?.timeout || 5000
|
|
43
|
+
timeout: config?.timeout || 5000,
|
|
44
|
+
headers: this.accessToken === undefined ? {} : {
|
|
45
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
46
|
+
}
|
|
42
47
|
});
|
|
43
48
|
(0, _axiosretry.default)(this.httpClient, {
|
|
44
49
|
retries: config?.httpRetries || 3,
|
|
@@ -70,6 +75,13 @@ class PriceServiceConnection {
|
|
|
70
75
|
}
|
|
71
76
|
};
|
|
72
77
|
this.wsEndpoint = (0, _utils.makeWebsocketUrl)(endpoint);
|
|
78
|
+
// Append access token as query param for WebSocket connections
|
|
79
|
+
// since browser WebSocket API does not support custom headers.
|
|
80
|
+
if (this.accessToken && this.wsEndpoint) {
|
|
81
|
+
const wsUrl = new URL(this.wsEndpoint);
|
|
82
|
+
wsUrl.searchParams.append("ACCESS_TOKEN", this.accessToken);
|
|
83
|
+
this.wsEndpoint = wsUrl.toString();
|
|
84
|
+
}
|
|
73
85
|
}
|
|
74
86
|
/**
|
|
75
87
|
* Fetch Latest PriceFeeds of given price ids.
|
|
@@ -19,6 +19,13 @@ export type PriceServiceConnectionConfig = {
|
|
|
19
19
|
logger?: Logger | undefined;
|
|
20
20
|
verbose?: boolean | undefined;
|
|
21
21
|
priceFeedRequestConfig?: PriceFeedRequestConfig | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Optional API access token for authentication.
|
|
24
|
+
* When provided, this token will be included in all requests either:
|
|
25
|
+
* - As a Bearer token in the Authorization header (for HTTP requests)
|
|
26
|
+
* - As an ACCESS_TOKEN query parameter (for WebSocket connections)
|
|
27
|
+
*/
|
|
28
|
+
accessToken?: string | undefined;
|
|
22
29
|
};
|
|
23
30
|
export type PriceFeedUpdateCallback = (priceFeed: PriceFeed) => void;
|
|
24
31
|
export declare class PriceServiceConnection {
|
|
@@ -28,6 +35,7 @@ export declare class PriceServiceConnection {
|
|
|
28
35
|
private wsEndpoint;
|
|
29
36
|
private logger;
|
|
30
37
|
private priceFeedRequestConfig;
|
|
38
|
+
private accessToken;
|
|
31
39
|
/**
|
|
32
40
|
* Custom handler for web socket errors (connection and message parsing).
|
|
33
41
|
*
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { HexString } from "@pythnetwork/price-service-sdk";
|
|
2
|
+
import { PriceFeed } from "@pythnetwork/price-service-sdk";
|
|
3
|
+
import type { Logger } from "ts-log";
|
|
4
|
+
export type DurationInMs = number;
|
|
5
|
+
export type PriceFeedRequestConfig = {
|
|
6
|
+
verbose?: boolean | undefined;
|
|
7
|
+
binary?: boolean | undefined;
|
|
8
|
+
allowOutOfOrder?: boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
export type PriceServiceConnectionConfig = {
|
|
11
|
+
timeout?: DurationInMs | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Number of times a HTTP request will be retried before the API returns a failure. Default: 3.
|
|
14
|
+
*
|
|
15
|
+
* The connection uses exponential back-off for the delay between retries. However,
|
|
16
|
+
* it will timeout regardless of the retries at the configured `timeout` time.
|
|
17
|
+
*/
|
|
18
|
+
httpRetries?: number | undefined;
|
|
19
|
+
logger?: Logger | undefined;
|
|
20
|
+
verbose?: boolean | undefined;
|
|
21
|
+
priceFeedRequestConfig?: PriceFeedRequestConfig | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Optional API access token for authentication.
|
|
24
|
+
* When provided, this token will be included in all requests either:
|
|
25
|
+
* - As a Bearer token in the Authorization header (for HTTP requests)
|
|
26
|
+
* - As an ACCESS_TOKEN query parameter (for WebSocket connections)
|
|
27
|
+
*/
|
|
28
|
+
accessToken?: string | undefined;
|
|
29
|
+
};
|
|
30
|
+
export type PriceFeedUpdateCallback = (priceFeed: PriceFeed) => void;
|
|
31
|
+
export declare class PriceServiceConnection {
|
|
32
|
+
private httpClient;
|
|
33
|
+
private priceFeedCallbacks;
|
|
34
|
+
private wsClient;
|
|
35
|
+
private wsEndpoint;
|
|
36
|
+
private logger;
|
|
37
|
+
private priceFeedRequestConfig;
|
|
38
|
+
private accessToken;
|
|
39
|
+
/**
|
|
40
|
+
* Custom handler for web socket errors (connection and message parsing).
|
|
41
|
+
*
|
|
42
|
+
* Default handler only logs the errors.
|
|
43
|
+
*/
|
|
44
|
+
onWsError: (error: Error) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Constructs a new Connection.
|
|
47
|
+
*
|
|
48
|
+
* @param endpoint - endpoint URL to the price service. Example: https://website/example/
|
|
49
|
+
* @param config - Optional PriceServiceConnectionConfig for custom configurations.
|
|
50
|
+
*/
|
|
51
|
+
constructor(endpoint: string, config?: PriceServiceConnectionConfig);
|
|
52
|
+
/**
|
|
53
|
+
* Fetch Latest PriceFeeds of given price ids.
|
|
54
|
+
* 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)
|
|
55
|
+
*
|
|
56
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
57
|
+
* @returns Array of PriceFeeds
|
|
58
|
+
*/
|
|
59
|
+
getLatestPriceFeeds(priceIds: HexString[]): Promise<PriceFeed[] | undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* Fetch latest VAA of given price ids.
|
|
62
|
+
* 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)
|
|
63
|
+
*
|
|
64
|
+
* This function is coupled to wormhole implemntation.
|
|
65
|
+
*
|
|
66
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
67
|
+
* @returns Array of base64 encoded VAAs.
|
|
68
|
+
*/
|
|
69
|
+
getLatestVaas(priceIds: HexString[]): Promise<string[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Fetch the earliest VAA of the given price id that is published since the given publish time.
|
|
72
|
+
* This will throw an error if the given publish time is in the future, or if the publish time
|
|
73
|
+
* is old and the price service endpoint does not have a db backend for historical requests.
|
|
74
|
+
* 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)
|
|
75
|
+
*
|
|
76
|
+
* This function is coupled to wormhole implemntation.
|
|
77
|
+
*
|
|
78
|
+
* @param priceId - Hex-encoded price id.
|
|
79
|
+
* @param publishTime - Epoch timestamp in seconds.
|
|
80
|
+
* @returns Tuple of VAA and publishTime.
|
|
81
|
+
*/
|
|
82
|
+
getVaa(priceId: HexString, publishTime: EpochTimeStamp): Promise<[string, EpochTimeStamp]>;
|
|
83
|
+
/**
|
|
84
|
+
* Fetch the PriceFeed of the given price id that is published since the given publish time.
|
|
85
|
+
* This will throw an error if the given publish time is in the future, or if the publish time
|
|
86
|
+
* is old and the price service endpoint does not have a db backend for historical requests.
|
|
87
|
+
* 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)
|
|
88
|
+
*
|
|
89
|
+
* @param priceId - Hex-encoded price id.
|
|
90
|
+
* @param publishTime - Epoch timestamp in seconds.
|
|
91
|
+
* @returns PriceFeed
|
|
92
|
+
*/
|
|
93
|
+
getPriceFeed(priceId: HexString, publishTime: EpochTimeStamp): Promise<PriceFeed>;
|
|
94
|
+
/**
|
|
95
|
+
* Fetch the list of available price feed ids.
|
|
96
|
+
* This will throw an axios error if there is a network problem or the price service returns a non-ok response.
|
|
97
|
+
*
|
|
98
|
+
* @returns Array of hex-encoded price ids.
|
|
99
|
+
*/
|
|
100
|
+
getPriceFeedIds(): Promise<HexString[]>;
|
|
101
|
+
/**
|
|
102
|
+
* Subscribe to updates for given price ids.
|
|
103
|
+
*
|
|
104
|
+
* It will start a websocket connection if it's not started yet.
|
|
105
|
+
* Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
|
|
106
|
+
* it calls `connection.onWsError`. If you want to handle the errors you should set the
|
|
107
|
+
* `onWsError` function to your custom error handler.
|
|
108
|
+
*
|
|
109
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
110
|
+
* @param cb - Callback function that is called with a PriceFeed upon updates to given price ids.
|
|
111
|
+
*/
|
|
112
|
+
subscribePriceFeedUpdates(priceIds: HexString[], cb: PriceFeedUpdateCallback): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Unsubscribe from updates for given price ids.
|
|
115
|
+
*
|
|
116
|
+
* It will close the websocket connection if it's not subscribed to any price feed updates anymore.
|
|
117
|
+
* Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
|
|
118
|
+
* it calls `connection.onWsError`. If you want to handle the errors you should set the
|
|
119
|
+
* `onWsError` function to your custom error handler.
|
|
120
|
+
*
|
|
121
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
122
|
+
* @param cb - Optional callback, if set it will only unsubscribe this callback from updates for given price ids.
|
|
123
|
+
*/
|
|
124
|
+
unsubscribePriceFeedUpdates(priceIds: HexString[], cb?: PriceFeedUpdateCallback): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Starts connection websocket.
|
|
127
|
+
*
|
|
128
|
+
* This function is called automatically upon subscribing to price feed updates.
|
|
129
|
+
*/
|
|
130
|
+
startWebSocket(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Closes connection websocket.
|
|
133
|
+
*
|
|
134
|
+
* At termination, the websocket should be closed to finish the
|
|
135
|
+
* process elegantly. It will automatically close when the connection
|
|
136
|
+
* is subscribed to no price feeds.
|
|
137
|
+
*/
|
|
138
|
+
closeWebSocket(): void;
|
|
139
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable unicorn/no-process-exit */ /* eslint-disable n/no-process-exit */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-base-to-string */ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable no-console */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import { PriceFeed } from "@pythnetwork/price-service-sdk";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import axiosRetry from "axios-retry";
|
|
4
|
+
import { ResilientWebSocket } from "./ResillientWebSocket.mjs";
|
|
5
|
+
import { makeWebsocketUrl, removeLeading0xIfExists } from "./utils.mjs";
|
|
6
|
+
export class PriceServiceConnection {
|
|
7
|
+
httpClient;
|
|
8
|
+
priceFeedCallbacks;
|
|
9
|
+
wsClient;
|
|
10
|
+
wsEndpoint;
|
|
11
|
+
logger;
|
|
12
|
+
priceFeedRequestConfig;
|
|
13
|
+
accessToken;
|
|
14
|
+
/**
|
|
15
|
+
* Custom handler for web socket errors (connection and message parsing).
|
|
16
|
+
*
|
|
17
|
+
* Default handler only logs the errors.
|
|
18
|
+
*/ onWsError;
|
|
19
|
+
/**
|
|
20
|
+
* Constructs a new Connection.
|
|
21
|
+
*
|
|
22
|
+
* @param endpoint - endpoint URL to the price service. Example: https://website/example/
|
|
23
|
+
* @param config - Optional PriceServiceConnectionConfig for custom configurations.
|
|
24
|
+
*/ constructor(endpoint, config){
|
|
25
|
+
this.accessToken = config?.accessToken;
|
|
26
|
+
this.httpClient = axios.create({
|
|
27
|
+
baseURL: endpoint,
|
|
28
|
+
timeout: config?.timeout || 5000,
|
|
29
|
+
headers: this.accessToken === undefined ? {} : {
|
|
30
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
axiosRetry(this.httpClient, {
|
|
34
|
+
retries: config?.httpRetries || 3,
|
|
35
|
+
// eslint-disable-next-line import/no-named-as-default-member
|
|
36
|
+
retryDelay: axiosRetry.exponentialDelay.bind(axiosRetry)
|
|
37
|
+
});
|
|
38
|
+
this.priceFeedRequestConfig = {
|
|
39
|
+
binary: config?.priceFeedRequestConfig?.binary,
|
|
40
|
+
verbose: config?.priceFeedRequestConfig?.verbose ?? config?.verbose,
|
|
41
|
+
allowOutOfOrder: config?.priceFeedRequestConfig?.allowOutOfOrder
|
|
42
|
+
};
|
|
43
|
+
this.priceFeedCallbacks = new Map();
|
|
44
|
+
// Default logger is console for only warnings and errors.
|
|
45
|
+
this.logger = config?.logger || {
|
|
46
|
+
trace: ()=>{},
|
|
47
|
+
debug: ()=>{},
|
|
48
|
+
info: ()=>{},
|
|
49
|
+
warn: console.warn,
|
|
50
|
+
error: console.error
|
|
51
|
+
};
|
|
52
|
+
this.onWsError = (error)=>{
|
|
53
|
+
this.logger.error(error);
|
|
54
|
+
// Exit the process if it is running in node.
|
|
55
|
+
if (typeof process !== "undefined" && typeof process.exit === "function") {
|
|
56
|
+
this.logger.error("Halting the process due to the websocket error");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
} else {
|
|
59
|
+
this.logger.error("Cannot halt process. Please handle the websocket error.");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
this.wsEndpoint = makeWebsocketUrl(endpoint);
|
|
63
|
+
// Append access token as query param for WebSocket connections
|
|
64
|
+
// since browser WebSocket API does not support custom headers.
|
|
65
|
+
if (this.accessToken && this.wsEndpoint) {
|
|
66
|
+
const wsUrl = new URL(this.wsEndpoint);
|
|
67
|
+
wsUrl.searchParams.append("ACCESS_TOKEN", this.accessToken);
|
|
68
|
+
this.wsEndpoint = wsUrl.toString();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetch Latest PriceFeeds of given price ids.
|
|
73
|
+
* 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)
|
|
74
|
+
*
|
|
75
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
76
|
+
* @returns Array of PriceFeeds
|
|
77
|
+
*/ async getLatestPriceFeeds(priceIds) {
|
|
78
|
+
if (priceIds.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const response = await this.httpClient.get("/api/latest_price_feeds", {
|
|
82
|
+
params: {
|
|
83
|
+
ids: priceIds,
|
|
84
|
+
verbose: this.priceFeedRequestConfig.verbose,
|
|
85
|
+
binary: this.priceFeedRequestConfig.binary
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const priceFeedsJson = response.data;
|
|
89
|
+
return priceFeedsJson.map((priceFeedJson)=>PriceFeed.fromJson(priceFeedJson));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Fetch latest VAA of given price ids.
|
|
93
|
+
* 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)
|
|
94
|
+
*
|
|
95
|
+
* This function is coupled to wormhole implemntation.
|
|
96
|
+
*
|
|
97
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
98
|
+
* @returns Array of base64 encoded VAAs.
|
|
99
|
+
*/ async getLatestVaas(priceIds) {
|
|
100
|
+
const response = await this.httpClient.get("/api/latest_vaas", {
|
|
101
|
+
params: {
|
|
102
|
+
ids: priceIds
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return response.data;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Fetch the earliest VAA of the given price id that is published since the given publish time.
|
|
109
|
+
* This will throw an error if the given publish time is in the future, or if the publish time
|
|
110
|
+
* is old and the price service endpoint does not have a db backend for historical requests.
|
|
111
|
+
* 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)
|
|
112
|
+
*
|
|
113
|
+
* This function is coupled to wormhole implemntation.
|
|
114
|
+
*
|
|
115
|
+
* @param priceId - Hex-encoded price id.
|
|
116
|
+
* @param publishTime - Epoch timestamp in seconds.
|
|
117
|
+
* @returns Tuple of VAA and publishTime.
|
|
118
|
+
*/ async getVaa(priceId, publishTime) {
|
|
119
|
+
const response = await this.httpClient.get("/api/get_vaa", {
|
|
120
|
+
params: {
|
|
121
|
+
id: priceId,
|
|
122
|
+
publish_time: publishTime
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return [
|
|
126
|
+
response.data.vaa,
|
|
127
|
+
response.data.publishTime
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Fetch the PriceFeed of the given price id that is published since the given publish time.
|
|
132
|
+
* This will throw an error if the given publish time is in the future, or if the publish time
|
|
133
|
+
* is old and the price service endpoint does not have a db backend for historical requests.
|
|
134
|
+
* 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)
|
|
135
|
+
*
|
|
136
|
+
* @param priceId - Hex-encoded price id.
|
|
137
|
+
* @param publishTime - Epoch timestamp in seconds.
|
|
138
|
+
* @returns PriceFeed
|
|
139
|
+
*/ async getPriceFeed(priceId, publishTime) {
|
|
140
|
+
const response = await this.httpClient.get("/api/get_price_feed", {
|
|
141
|
+
params: {
|
|
142
|
+
id: priceId,
|
|
143
|
+
publish_time: publishTime,
|
|
144
|
+
verbose: this.priceFeedRequestConfig.verbose,
|
|
145
|
+
binary: this.priceFeedRequestConfig.binary
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return PriceFeed.fromJson(response.data);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Fetch the list of available price feed ids.
|
|
152
|
+
* This will throw an axios error if there is a network problem or the price service returns a non-ok response.
|
|
153
|
+
*
|
|
154
|
+
* @returns Array of hex-encoded price ids.
|
|
155
|
+
*/ async getPriceFeedIds() {
|
|
156
|
+
const response = await this.httpClient.get("/api/price_feed_ids");
|
|
157
|
+
return response.data;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Subscribe to updates for given price ids.
|
|
161
|
+
*
|
|
162
|
+
* It will start a websocket connection if it's not started yet.
|
|
163
|
+
* Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
|
|
164
|
+
* it calls `connection.onWsError`. If you want to handle the errors you should set the
|
|
165
|
+
* `onWsError` function to your custom error handler.
|
|
166
|
+
*
|
|
167
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
168
|
+
* @param cb - Callback function that is called with a PriceFeed upon updates to given price ids.
|
|
169
|
+
*/ async subscribePriceFeedUpdates(priceIds, cb) {
|
|
170
|
+
if (this.wsClient === undefined) {
|
|
171
|
+
await this.startWebSocket();
|
|
172
|
+
}
|
|
173
|
+
priceIds = priceIds.map((priceId)=>removeLeading0xIfExists(priceId));
|
|
174
|
+
const newPriceIds = [];
|
|
175
|
+
for (const id of priceIds){
|
|
176
|
+
if (!this.priceFeedCallbacks.has(id)) {
|
|
177
|
+
this.priceFeedCallbacks.set(id, new Set());
|
|
178
|
+
newPriceIds.push(id);
|
|
179
|
+
}
|
|
180
|
+
this.priceFeedCallbacks.get(id).add(cb);
|
|
181
|
+
}
|
|
182
|
+
const message = {
|
|
183
|
+
ids: newPriceIds,
|
|
184
|
+
type: "subscribe",
|
|
185
|
+
verbose: this.priceFeedRequestConfig.verbose,
|
|
186
|
+
binary: this.priceFeedRequestConfig.binary,
|
|
187
|
+
allow_out_of_order: this.priceFeedRequestConfig.allowOutOfOrder
|
|
188
|
+
};
|
|
189
|
+
await this.wsClient?.send(JSON.stringify(message));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Unsubscribe from updates for given price ids.
|
|
193
|
+
*
|
|
194
|
+
* It will close the websocket connection if it's not subscribed to any price feed updates anymore.
|
|
195
|
+
* Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
|
|
196
|
+
* it calls `connection.onWsError`. If you want to handle the errors you should set the
|
|
197
|
+
* `onWsError` function to your custom error handler.
|
|
198
|
+
*
|
|
199
|
+
* @param priceIds - Array of hex-encoded price ids.
|
|
200
|
+
* @param cb - Optional callback, if set it will only unsubscribe this callback from updates for given price ids.
|
|
201
|
+
*/ async unsubscribePriceFeedUpdates(priceIds, cb) {
|
|
202
|
+
if (this.wsClient === undefined) {
|
|
203
|
+
await this.startWebSocket();
|
|
204
|
+
}
|
|
205
|
+
priceIds = priceIds.map((priceId)=>removeLeading0xIfExists(priceId));
|
|
206
|
+
const removedPriceIds = [];
|
|
207
|
+
for (const id of priceIds){
|
|
208
|
+
if (this.priceFeedCallbacks.has(id)) {
|
|
209
|
+
let idRemoved = false;
|
|
210
|
+
if (cb === undefined) {
|
|
211
|
+
this.priceFeedCallbacks.delete(id);
|
|
212
|
+
idRemoved = true;
|
|
213
|
+
} else {
|
|
214
|
+
this.priceFeedCallbacks.get(id).delete(cb);
|
|
215
|
+
if (this.priceFeedCallbacks.get(id).size === 0) {
|
|
216
|
+
this.priceFeedCallbacks.delete(id);
|
|
217
|
+
idRemoved = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (idRemoved) {
|
|
221
|
+
removedPriceIds.push(id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const message = {
|
|
226
|
+
ids: removedPriceIds,
|
|
227
|
+
type: "unsubscribe"
|
|
228
|
+
};
|
|
229
|
+
await this.wsClient?.send(JSON.stringify(message));
|
|
230
|
+
if (this.priceFeedCallbacks.size === 0) {
|
|
231
|
+
this.closeWebSocket();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Starts connection websocket.
|
|
236
|
+
*
|
|
237
|
+
* This function is called automatically upon subscribing to price feed updates.
|
|
238
|
+
*/ async startWebSocket() {
|
|
239
|
+
if (this.wsEndpoint === undefined) {
|
|
240
|
+
throw new Error("Websocket endpoint is undefined.");
|
|
241
|
+
}
|
|
242
|
+
this.wsClient = new ResilientWebSocket(this.wsEndpoint, this.logger);
|
|
243
|
+
this.wsClient.onError = this.onWsError;
|
|
244
|
+
this.wsClient.onReconnect = ()=>{
|
|
245
|
+
if (this.priceFeedCallbacks.size > 0) {
|
|
246
|
+
const message = {
|
|
247
|
+
ids: [
|
|
248
|
+
...this.priceFeedCallbacks.keys()
|
|
249
|
+
],
|
|
250
|
+
type: "subscribe",
|
|
251
|
+
verbose: this.priceFeedRequestConfig.verbose,
|
|
252
|
+
binary: this.priceFeedRequestConfig.binary,
|
|
253
|
+
allow_out_of_order: this.priceFeedRequestConfig.allowOutOfOrder
|
|
254
|
+
};
|
|
255
|
+
this.logger.info("Resubscribing to existing price feeds.");
|
|
256
|
+
this.wsClient?.send(JSON.stringify(message));
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
this.wsClient.onMessage = (data)=>{
|
|
260
|
+
this.logger.info(`Received message ${data.toString()}`);
|
|
261
|
+
let message;
|
|
262
|
+
try {
|
|
263
|
+
message = JSON.parse(data.toString());
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.logger.error(`Error parsing message ${data.toString()} as JSON.`);
|
|
266
|
+
this.logger.error(error);
|
|
267
|
+
this.onWsError(error);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (message.type === "response") {
|
|
271
|
+
if (message.status === "error") {
|
|
272
|
+
this.logger.error(`Error response from the websocket server ${message.error}.`);
|
|
273
|
+
this.onWsError(new Error(message.error));
|
|
274
|
+
}
|
|
275
|
+
} else if (message.type === "price_update") {
|
|
276
|
+
let priceFeed;
|
|
277
|
+
try {
|
|
278
|
+
priceFeed = PriceFeed.fromJson(message.price_feed);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
this.logger.error(`Error parsing price feeds from message ${data.toString()}.`);
|
|
281
|
+
this.logger.error(error);
|
|
282
|
+
this.onWsError(error);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (this.priceFeedCallbacks.has(priceFeed.id)) {
|
|
286
|
+
for (const cb of this.priceFeedCallbacks.get(priceFeed.id)){
|
|
287
|
+
cb(priceFeed);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
this.logger.warn(`Ignoring unsupported server response ${data.toString()}.`);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
await this.wsClient.startWebSocket();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Closes connection websocket.
|
|
298
|
+
*
|
|
299
|
+
* At termination, the websocket should be closed to finish the
|
|
300
|
+
* process elegantly. It will automatically close when the connection
|
|
301
|
+
* is subscribed to no price feeds.
|
|
302
|
+
*/ closeWebSocket() {
|
|
303
|
+
this.wsClient?.closeWebSocket();
|
|
304
|
+
this.wsClient = undefined;
|
|
305
|
+
this.priceFeedCallbacks.clear();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import WebSocket from "isomorphic-ws";
|
|
2
|
+
import type { 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: unknown): 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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable unicorn/prefer-add-event-listener */ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-empty-function */ import WebSocket from "isomorphic-ws";
|
|
2
|
+
const PING_TIMEOUT_DURATION = 30_000 + 3000; // It is 30s on the server and 3s is added for delays
|
|
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
|
+
*/ export class ResilientWebSocket {
|
|
13
|
+
endpoint;
|
|
14
|
+
wsClient;
|
|
15
|
+
wsUserClosed;
|
|
16
|
+
wsFailedAttempts;
|
|
17
|
+
pingTimeout;
|
|
18
|
+
logger;
|
|
19
|
+
onError;
|
|
20
|
+
onMessage;
|
|
21
|
+
onReconnect;
|
|
22
|
+
constructor(endpoint, logger){
|
|
23
|
+
this.endpoint = endpoint;
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
this.wsFailedAttempts = 0;
|
|
26
|
+
this.onError = (error)=>{
|
|
27
|
+
this.logger?.error(error);
|
|
28
|
+
};
|
|
29
|
+
this.wsUserClosed = true;
|
|
30
|
+
this.onMessage = ()=>{};
|
|
31
|
+
this.onReconnect = ()=>{};
|
|
32
|
+
}
|
|
33
|
+
async send(data) {
|
|
34
|
+
this.logger?.info(`Sending ${data}`);
|
|
35
|
+
await this.waitForMaybeReadyWebSocket();
|
|
36
|
+
if (this.wsClient === undefined) {
|
|
37
|
+
this.logger?.error("Couldn't connect to the websocket server. Error callback is called.");
|
|
38
|
+
} else {
|
|
39
|
+
this.wsClient.send(data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async startWebSocket() {
|
|
43
|
+
if (this.wsClient !== undefined) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.logger?.info(`Creating Web Socket client`);
|
|
47
|
+
this.wsClient = new WebSocket(this.endpoint);
|
|
48
|
+
this.wsUserClosed = false;
|
|
49
|
+
this.wsClient.addEventListener("open", ()=>{
|
|
50
|
+
this.wsFailedAttempts = 0;
|
|
51
|
+
// Ping handler is undefined in browser side so heartbeat is disabled.
|
|
52
|
+
if (this.wsClient.on !== undefined) {
|
|
53
|
+
this.heartbeat();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
this.wsClient.onerror = (event)=>{
|
|
57
|
+
this.onError(event.error);
|
|
58
|
+
};
|
|
59
|
+
this.wsClient.onmessage = (event)=>{
|
|
60
|
+
this.onMessage(event.data);
|
|
61
|
+
};
|
|
62
|
+
this.wsClient.addEventListener("close", async ()=>{
|
|
63
|
+
if (this.pingTimeout !== undefined) {
|
|
64
|
+
clearInterval(this.pingTimeout);
|
|
65
|
+
}
|
|
66
|
+
if (this.wsUserClosed) {
|
|
67
|
+
this.logger?.info("The connection has been closed successfully.");
|
|
68
|
+
} else {
|
|
69
|
+
this.wsFailedAttempts += 1;
|
|
70
|
+
this.wsClient = undefined;
|
|
71
|
+
const waitTime = expoBackoff(this.wsFailedAttempts);
|
|
72
|
+
this.logger?.error(`Connection closed unexpectedly or because of timeout. Reconnecting after ${waitTime}ms.`);
|
|
73
|
+
await sleep(waitTime);
|
|
74
|
+
this.restartUnexpectedClosedWebsocket();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
if (this.wsClient.on !== undefined) {
|
|
78
|
+
// Ping handler is undefined in browser side
|
|
79
|
+
this.wsClient.on("ping", this.heartbeat.bind(this));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Heartbeat is only enabled in node clients because they support handling
|
|
84
|
+
* ping-pong events.
|
|
85
|
+
*
|
|
86
|
+
* This approach only works when server constantly pings the clients which.
|
|
87
|
+
* Otherwise you might consider sending ping and acting on pong responses
|
|
88
|
+
* yourself.
|
|
89
|
+
*/ heartbeat() {
|
|
90
|
+
this.logger?.info("Heartbeat");
|
|
91
|
+
if (this.pingTimeout !== undefined) {
|
|
92
|
+
clearTimeout(this.pingTimeout);
|
|
93
|
+
}
|
|
94
|
+
this.pingTimeout = setTimeout(()=>{
|
|
95
|
+
this.logger?.warn(`Connection timed out. Reconnecting...`);
|
|
96
|
+
this.wsClient?.terminate();
|
|
97
|
+
this.restartUnexpectedClosedWebsocket();
|
|
98
|
+
}, PING_TIMEOUT_DURATION);
|
|
99
|
+
}
|
|
100
|
+
async waitForMaybeReadyWebSocket() {
|
|
101
|
+
let waitedTime = 0;
|
|
102
|
+
while(this.wsClient !== undefined && this.wsClient.readyState !== this.wsClient.OPEN){
|
|
103
|
+
if (waitedTime > 5000) {
|
|
104
|
+
this.wsClient.close();
|
|
105
|
+
return;
|
|
106
|
+
} else {
|
|
107
|
+
waitedTime += 10;
|
|
108
|
+
await sleep(10);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async restartUnexpectedClosedWebsocket() {
|
|
113
|
+
if (this.wsUserClosed) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await this.startWebSocket();
|
|
117
|
+
await this.waitForMaybeReadyWebSocket();
|
|
118
|
+
if (this.wsClient === undefined) {
|
|
119
|
+
this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.onReconnect();
|
|
123
|
+
}
|
|
124
|
+
closeWebSocket() {
|
|
125
|
+
if (this.wsClient !== undefined) {
|
|
126
|
+
const client = this.wsClient;
|
|
127
|
+
this.wsClient = undefined;
|
|
128
|
+
client.close();
|
|
129
|
+
}
|
|
130
|
+
this.wsUserClosed = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function sleep(ms) {
|
|
134
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
135
|
+
}
|
|
136
|
+
function expoBackoff(attempts) {
|
|
137
|
+
return 2 ** attempts * 100;
|
|
138
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { type DurationInMs, PriceServiceConnection, type PriceServiceConnectionConfig, } from "./PriceServiceConnection.js";
|
|
2
|
+
export { type HexString, PriceFeedMetadata, PriceFeed, Price, type UnixTimestamp, isAccumulatorUpdateData, parseAccumulatorUpdateData, type AccumulatorUpdateData, } from "@pythnetwork/price-service-sdk";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "module" }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { 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;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert http(s) endpoint to ws(s) endpoint.
|
|
3
|
+
*
|
|
4
|
+
* @param endpoint - Http(s) protocol endpoint
|
|
5
|
+
* @returns Ws(s) protocol endpoint of the same address
|
|
6
|
+
*/ export function makeWebsocketUrl(endpoint) {
|
|
7
|
+
const url = new URL("ws", endpoint);
|
|
8
|
+
const useHttps = url.protocol === "https:";
|
|
9
|
+
url.protocol = useHttps ? "wss:" : "ws:";
|
|
10
|
+
return url.toString();
|
|
11
|
+
}
|
|
12
|
+
export function removeLeading0xIfExists(id) {
|
|
13
|
+
return id.startsWith("0x") ? id.slice(2) : id;
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pythnetwork/price-service-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Pyth price service client",
|
|
5
5
|
"deprecated": "This package is deprecated and is no longer maintained. Please use @pythnetwork/hermes-client instead.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Pyth Data Association"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://pyth.network",
|
|
10
|
-
"main": "./dist/index.cjs",
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
10
|
+
"main": "./dist/cjs/index.cjs",
|
|
11
|
+
"types": "./dist/cjs/index.d.ts",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist/**/*"
|
|
14
14
|
],
|
|
@@ -45,32 +45,56 @@
|
|
|
45
45
|
"@pythnetwork/price-service-sdk": "1.9.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": "
|
|
48
|
+
"node": "^24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"type": "module",
|
|
51
51
|
"exports": {
|
|
52
52
|
"./PriceServiceConnection": {
|
|
53
|
-
"
|
|
54
|
-
|
|
53
|
+
"require": {
|
|
54
|
+
"types": "./dist/cjs/PriceServiceConnection.d.ts",
|
|
55
|
+
"default": "./dist/cjs/PriceServiceConnection.cjs"
|
|
56
|
+
},
|
|
57
|
+
"import": {
|
|
58
|
+
"types": "./dist/esm/PriceServiceConnection.d.ts",
|
|
59
|
+
"default": "./dist/esm/PriceServiceConnection.mjs"
|
|
60
|
+
}
|
|
55
61
|
},
|
|
56
62
|
"./ResillientWebSocket": {
|
|
57
|
-
"
|
|
58
|
-
|
|
63
|
+
"require": {
|
|
64
|
+
"types": "./dist/cjs/ResillientWebSocket.d.ts",
|
|
65
|
+
"default": "./dist/cjs/ResillientWebSocket.cjs"
|
|
66
|
+
},
|
|
67
|
+
"import": {
|
|
68
|
+
"types": "./dist/esm/ResillientWebSocket.d.ts",
|
|
69
|
+
"default": "./dist/esm/ResillientWebSocket.mjs"
|
|
70
|
+
}
|
|
59
71
|
},
|
|
60
72
|
".": {
|
|
61
|
-
"
|
|
62
|
-
|
|
73
|
+
"require": {
|
|
74
|
+
"types": "./dist/cjs/index.d.ts",
|
|
75
|
+
"default": "./dist/cjs/index.cjs"
|
|
76
|
+
},
|
|
77
|
+
"import": {
|
|
78
|
+
"types": "./dist/esm/index.d.ts",
|
|
79
|
+
"default": "./dist/esm/index.mjs"
|
|
80
|
+
}
|
|
63
81
|
},
|
|
64
82
|
"./utils": {
|
|
65
|
-
"
|
|
66
|
-
|
|
83
|
+
"require": {
|
|
84
|
+
"types": "./dist/cjs/utils.d.ts",
|
|
85
|
+
"default": "./dist/cjs/utils.cjs"
|
|
86
|
+
},
|
|
87
|
+
"import": {
|
|
88
|
+
"types": "./dist/esm/utils.d.ts",
|
|
89
|
+
"default": "./dist/esm/utils.mjs"
|
|
90
|
+
}
|
|
67
91
|
},
|
|
68
92
|
"./package.json": "./package.json"
|
|
69
93
|
},
|
|
70
|
-
"module": "./dist/esm/index.
|
|
94
|
+
"module": "./dist/esm/index.mjs",
|
|
71
95
|
"scripts": {
|
|
72
96
|
"test:e2e": "jest --testPathPattern=.*.e2e.test.ts",
|
|
73
|
-
"build": "ts-duality
|
|
97
|
+
"build": "ts-duality",
|
|
74
98
|
"example": "pnpm run build && node lib/examples/PriceServiceClient.js",
|
|
75
99
|
"fix:format": "prettier --write \"src/**/*.ts\"",
|
|
76
100
|
"fix:lint": "eslint src/ --fix --max-warnings 0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|