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