@jsnw/kalshi-client 0.0.1
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/index.cjs +672 -0
- package/dist/index.d.cts +462 -0
- package/dist/index.d.mts +463 -0
- package/dist/index.mjs +652 -0
- package/package.json +47 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { constants, createSign } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { v4 } from "uuid";
|
|
4
|
+
import { WebSocket } from "ws";
|
|
5
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
6
|
+
//#region src/request-signer.ts
|
|
7
|
+
var RequestSigner = class {
|
|
8
|
+
privkey;
|
|
9
|
+
/**
|
|
10
|
+
* @param {RequestSignerOptions} options
|
|
11
|
+
*/
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.privkey = options.privateKey;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @param {SignatureParameters} params
|
|
17
|
+
* @return {string}
|
|
18
|
+
*/
|
|
19
|
+
getSignature({ timestamp, httpMethod, path }) {
|
|
20
|
+
const text = `${(timestamp instanceof Date ? timestamp.getTime() : timestamp).toString()}${httpMethod.toUpperCase()}${path.split("?")[0].replace(/\/+$/, "")}`;
|
|
21
|
+
const sign = createSign("RSA-SHA256");
|
|
22
|
+
sign.update(text);
|
|
23
|
+
sign.end();
|
|
24
|
+
return sign.sign({
|
|
25
|
+
key: this.privkey,
|
|
26
|
+
padding: constants.RSA_PKCS1_PSS_PADDING,
|
|
27
|
+
saltLength: constants.RSA_PSS_SALTLEN_DIGEST
|
|
28
|
+
}).toString("base64");
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/credentials-provider.ts
|
|
33
|
+
var InMemoryCredentialsProvider = class {
|
|
34
|
+
accessKey;
|
|
35
|
+
privateKey;
|
|
36
|
+
signer;
|
|
37
|
+
/**
|
|
38
|
+
* @param {InMemoryCredentialsProviderOptions} options
|
|
39
|
+
*/
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.accessKey = options.accessKey;
|
|
42
|
+
this.privateKey = options.privateKey;
|
|
43
|
+
this.signer = new RequestSigner({ privateKey: this.privateKey });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} accessKey
|
|
47
|
+
*/
|
|
48
|
+
setAccessKey(accessKey) {
|
|
49
|
+
if (this.accessKey === accessKey) return;
|
|
50
|
+
this.accessKey = accessKey;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} privateKey
|
|
54
|
+
*/
|
|
55
|
+
setPrivateKey(privateKey) {
|
|
56
|
+
if (this.privateKey === privateKey) return;
|
|
57
|
+
this.privateKey = privateKey;
|
|
58
|
+
this.signer = new RequestSigner({ privateKey });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @return {string}
|
|
62
|
+
*/
|
|
63
|
+
getAccessKey() {
|
|
64
|
+
return this.accessKey;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @param {SignatureParameters} options
|
|
68
|
+
* @return {string}
|
|
69
|
+
*/
|
|
70
|
+
getSignature(options) {
|
|
71
|
+
return this.signer.getSignature(options);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/schemas/primitives.ts
|
|
76
|
+
const primitives$fixedPointSchema = z.string().regex(/^\d{1,14}\.\d{2,8}$/);
|
|
77
|
+
const primitives$orderSideSchema = z.enum(["yes", "no"]);
|
|
78
|
+
const primitives$bookSideSchema = z.enum(["bid", "ask"]);
|
|
79
|
+
const primitives$orderActionSchema = z.enum(["buy", "sell"]);
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/schemas/orders.ts
|
|
82
|
+
const orders$orderStatusSchema = z.enum([
|
|
83
|
+
"resting",
|
|
84
|
+
"canceled",
|
|
85
|
+
"executed"
|
|
86
|
+
]);
|
|
87
|
+
const orders$orderTypeSchema = z.enum(["limit", "market"]);
|
|
88
|
+
const orders$selfTradePreventionTypeSchema = z.enum(["taker_at_cross", "maker"]);
|
|
89
|
+
const orders$orderDetailedSchema = z.object({
|
|
90
|
+
order_id: z.uuid(),
|
|
91
|
+
user_id: z.uuid(),
|
|
92
|
+
subaccount_number: z.number().min(0).max(32).default(0),
|
|
93
|
+
order_group_id: z.uuid().nullable().optional(),
|
|
94
|
+
exchange_index: z.number().min(0).default(0),
|
|
95
|
+
client_order_id: z.string(),
|
|
96
|
+
ticker: z.string(),
|
|
97
|
+
outcome_side: primitives$orderSideSchema,
|
|
98
|
+
book_side: primitives$bookSideSchema,
|
|
99
|
+
type: orders$orderTypeSchema,
|
|
100
|
+
status: orders$orderStatusSchema,
|
|
101
|
+
yes_price_dollars: primitives$fixedPointSchema,
|
|
102
|
+
no_price_dollars: primitives$fixedPointSchema,
|
|
103
|
+
fill_count_fp: primitives$fixedPointSchema,
|
|
104
|
+
remaining_count_fp: primitives$fixedPointSchema,
|
|
105
|
+
initial_count_fp: primitives$fixedPointSchema,
|
|
106
|
+
taker_fill_cost_dollars: primitives$fixedPointSchema,
|
|
107
|
+
maker_fill_cost_dollars: primitives$fixedPointSchema,
|
|
108
|
+
taker_fees_dollars: primitives$fixedPointSchema,
|
|
109
|
+
maker_fees_dollars: primitives$fixedPointSchema,
|
|
110
|
+
self_trade_prevention_type: orders$selfTradePreventionTypeSchema.nullable().optional(),
|
|
111
|
+
cancel_order_on_pause: z.boolean(),
|
|
112
|
+
created_time: z.iso.datetime(),
|
|
113
|
+
expiration_time: z.iso.datetime().nullable().optional(),
|
|
114
|
+
last_update_time: z.iso.datetime().nullable().optional()
|
|
115
|
+
});
|
|
116
|
+
const orders$orderWsSchema = orders$orderDetailedSchema.omit({
|
|
117
|
+
exchange_index: true,
|
|
118
|
+
type: true,
|
|
119
|
+
no_price_dollars: true,
|
|
120
|
+
cancel_order_on_pause: true
|
|
121
|
+
}).extend({
|
|
122
|
+
side: primitives$orderSideSchema,
|
|
123
|
+
created_ts_ms: z.number().positive(),
|
|
124
|
+
last_updated_ts_ms: z.number().positive().optional(),
|
|
125
|
+
expiration_ts_ms: z.number().positive().optional()
|
|
126
|
+
});
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/consts.ts
|
|
129
|
+
const KALSHI_WS_URL = {
|
|
130
|
+
"development": "wss://external-api-ws.demo.kalshi.co",
|
|
131
|
+
"production": "wss://external-api-ws.kalshi.com"
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/typed-emitter.ts
|
|
135
|
+
var TypedEmitter = class {
|
|
136
|
+
listeners = /* @__PURE__ */ new Map();
|
|
137
|
+
wrappers = /* @__PURE__ */ new WeakMap();
|
|
138
|
+
on(event, listener) {
|
|
139
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
140
|
+
this.listeners.get(event).add(listener);
|
|
141
|
+
}
|
|
142
|
+
once(event, listener) {
|
|
143
|
+
const wrapper = (...args) => {
|
|
144
|
+
listener(...args);
|
|
145
|
+
this.off(event, listener);
|
|
146
|
+
};
|
|
147
|
+
this.wrappers.set(listener, wrapper);
|
|
148
|
+
this.on(event, wrapper);
|
|
149
|
+
}
|
|
150
|
+
off(event, listener) {
|
|
151
|
+
const originalFn = this.wrappers.get(listener) || listener;
|
|
152
|
+
this.listeners.get(event)?.delete(originalFn);
|
|
153
|
+
this.wrappers.delete(listener);
|
|
154
|
+
}
|
|
155
|
+
emit(event, ...args) {
|
|
156
|
+
this.listeners.get(event)?.forEach((fn) => fn(...args));
|
|
157
|
+
}
|
|
158
|
+
removeListeners(event) {
|
|
159
|
+
this.listeners.delete(event);
|
|
160
|
+
}
|
|
161
|
+
removeAllListeners() {
|
|
162
|
+
this.listeners.clear();
|
|
163
|
+
}
|
|
164
|
+
listenersCount(event) {
|
|
165
|
+
if (event !== void 0) return this.listeners.get(event)?.size || 0;
|
|
166
|
+
let acc = 0;
|
|
167
|
+
for (const set of this.listeners.values()) acc += set.size;
|
|
168
|
+
return acc;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/defer.ts
|
|
173
|
+
/**
|
|
174
|
+
* @template T
|
|
175
|
+
* @return {DeferredPromise<T>}
|
|
176
|
+
*/
|
|
177
|
+
function defer() {
|
|
178
|
+
let resolve, reject;
|
|
179
|
+
return {
|
|
180
|
+
promise: new Promise((res, rej) => {
|
|
181
|
+
resolve = res;
|
|
182
|
+
reject = rej;
|
|
183
|
+
}),
|
|
184
|
+
resolve,
|
|
185
|
+
reject
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/ws-wrapper/errors.ts
|
|
190
|
+
var WsWrapperError = class extends Error {};
|
|
191
|
+
var NotConnectedError = class extends WsWrapperError {};
|
|
192
|
+
var InvalidStateError = class extends WsWrapperError {};
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/random-int.ts
|
|
195
|
+
/**
|
|
196
|
+
* Generates a random integer between min (inclusive) and max (inclusive)
|
|
197
|
+
* @param {number} min - The minimum value (inclusive)
|
|
198
|
+
* @param {number} max - The maximum value (inclusive)
|
|
199
|
+
* @returns {number} A random integer between min and max
|
|
200
|
+
*/
|
|
201
|
+
function randomInt(min, max) {
|
|
202
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/ws-wrapper/ws-wrapper.ts
|
|
206
|
+
var WsWrapper = class extends TypedEmitter {
|
|
207
|
+
wsAddress;
|
|
208
|
+
agent;
|
|
209
|
+
connectHeaders;
|
|
210
|
+
reconnectStrategy;
|
|
211
|
+
state = "idle";
|
|
212
|
+
openDefer = null;
|
|
213
|
+
closeDefer = null;
|
|
214
|
+
reconnectAbort = null;
|
|
215
|
+
ws = null;
|
|
216
|
+
closeRequested = false;
|
|
217
|
+
reconnectAttempt = 0;
|
|
218
|
+
lastError = null;
|
|
219
|
+
/**
|
|
220
|
+
* @param {WsWrapperOptions} options
|
|
221
|
+
*/
|
|
222
|
+
constructor(options) {
|
|
223
|
+
super();
|
|
224
|
+
this.wsAddress = options.address;
|
|
225
|
+
this.agent = options.agent;
|
|
226
|
+
this.connectHeaders = options.headers;
|
|
227
|
+
this.reconnectStrategy = options.reconnect;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* @return {Promise<void>}
|
|
231
|
+
*/
|
|
232
|
+
open() {
|
|
233
|
+
switch (this.state) {
|
|
234
|
+
case "idle":
|
|
235
|
+
this.state = "connecting";
|
|
236
|
+
this.openDefer = defer();
|
|
237
|
+
this.createWs();
|
|
238
|
+
return this.openDefer.promise;
|
|
239
|
+
case "connecting":
|
|
240
|
+
case "reconnecting": return this.openDefer.promise;
|
|
241
|
+
case "connected": return Promise.resolve();
|
|
242
|
+
case "closing": return Promise.reject(/* @__PURE__ */ new Error("Cannot reopen WS connection while it's still closing"));
|
|
243
|
+
default:
|
|
244
|
+
const _exhaustive = this.state;
|
|
245
|
+
throw new Error(`Unreachable: ${_exhaustive}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* @param {BufferLike} data
|
|
250
|
+
* @return {Promise<void>}
|
|
251
|
+
*/
|
|
252
|
+
send(data) {
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
if (this.state !== "connected") return reject(new NotConnectedError());
|
|
255
|
+
if (!this.ws) return reject(new InvalidStateError());
|
|
256
|
+
this.ws.send(data, (err) => {
|
|
257
|
+
if (err) return reject(err);
|
|
258
|
+
return resolve();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* @return {Promise<void>}
|
|
264
|
+
*/
|
|
265
|
+
close() {
|
|
266
|
+
switch (this.state) {
|
|
267
|
+
case "idle": return Promise.resolve();
|
|
268
|
+
case "connected":
|
|
269
|
+
case "connecting":
|
|
270
|
+
case "reconnecting":
|
|
271
|
+
this.state = "closing";
|
|
272
|
+
this.closeRequested = true;
|
|
273
|
+
this.closeDefer = defer();
|
|
274
|
+
this.ws?.close();
|
|
275
|
+
this.reconnectAbort?.abort();
|
|
276
|
+
return this.closeDefer.promise;
|
|
277
|
+
case "closing": return this.closeDefer.promise;
|
|
278
|
+
default:
|
|
279
|
+
const _exhaustive = this.state;
|
|
280
|
+
throw new Error(`Unreachable: ${_exhaustive}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
createWs() {
|
|
284
|
+
this.ws = new WebSocket(this.wsAddress, {
|
|
285
|
+
autoPong: true,
|
|
286
|
+
headers: this.getConnectHeaders(),
|
|
287
|
+
agent: this.agent
|
|
288
|
+
});
|
|
289
|
+
this.ws.on("open", this.handleOpen);
|
|
290
|
+
this.ws.on("message", this.handleMessage);
|
|
291
|
+
this.ws.on("error", this.handleError);
|
|
292
|
+
this.ws.on("close", this.handleClose);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* @return {Record<string, string>}
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
getConnectHeaders() {
|
|
299
|
+
if (!this.connectHeaders) return {};
|
|
300
|
+
if (typeof this.connectHeaders === "function") return this.connectHeaders();
|
|
301
|
+
return this.connectHeaders;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* @return {number | false}
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
getReconnectDelay() {
|
|
308
|
+
const attempt = this.reconnectAttempt;
|
|
309
|
+
if (this.closeRequested || !this.reconnectStrategy) return false;
|
|
310
|
+
if (typeof this.reconnectStrategy === "function") return this.reconnectStrategy(attempt);
|
|
311
|
+
if (typeof this.reconnectStrategy === "object") {
|
|
312
|
+
if (attempt > this.reconnectStrategy.maxAttempts) return false;
|
|
313
|
+
if (typeof this.reconnectStrategy.interval === "function") return this.reconnectStrategy.interval(attempt);
|
|
314
|
+
if (typeof this.reconnectStrategy.interval === "number") return this.reconnectStrategy.interval;
|
|
315
|
+
if (typeof this.reconnectStrategy.interval === "object" && !Array.isArray(this.reconnectStrategy.interval)) return randomInt(this.reconnectStrategy.interval.min, this.reconnectStrategy.interval.max);
|
|
316
|
+
if (Array.isArray(this.reconnectStrategy.interval)) {
|
|
317
|
+
if (this.reconnectStrategy.interval.length === 0) return 0;
|
|
318
|
+
else if (this.reconnectStrategy.interval.length === 1) return this.reconnectStrategy.interval[0];
|
|
319
|
+
else if (this.reconnectStrategy.interval.length > 1) return this.reconnectStrategy.interval[(attempt - 1) % this.reconnectStrategy.interval.length];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* @private
|
|
326
|
+
*/
|
|
327
|
+
reset() {
|
|
328
|
+
this.state = "idle";
|
|
329
|
+
this.ws?.removeAllListeners();
|
|
330
|
+
this.openDefer?.reject(this.lastError ?? /* @__PURE__ */ new Error("WS connection closed"));
|
|
331
|
+
this.closeDefer?.resolve();
|
|
332
|
+
this.closeRequested = false;
|
|
333
|
+
this.openDefer = null;
|
|
334
|
+
this.closeDefer = null;
|
|
335
|
+
this.ws = null;
|
|
336
|
+
this.lastError = null;
|
|
337
|
+
this.reconnectAttempt = 0;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
*/
|
|
341
|
+
handleOpen = () => {
|
|
342
|
+
this.state = "connected";
|
|
343
|
+
this.openDefer?.resolve();
|
|
344
|
+
this.openDefer = null;
|
|
345
|
+
const attempt = this.reconnectAttempt;
|
|
346
|
+
this.reconnectAttempt = 0;
|
|
347
|
+
this.emit("ready", attempt > 0);
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* @param {WebSocket.RawData} data
|
|
351
|
+
* @param {boolean} isBinary
|
|
352
|
+
*/
|
|
353
|
+
handleMessage = (data, isBinary) => {
|
|
354
|
+
this.emit("message", data, isBinary);
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* @param {Error} err
|
|
358
|
+
*/
|
|
359
|
+
handleError = (err) => {
|
|
360
|
+
this.lastError = err;
|
|
361
|
+
this.emit("error", err);
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* @param {number} code
|
|
365
|
+
* @param {Buffer} reason
|
|
366
|
+
*/
|
|
367
|
+
handleClose = async (code, reason) => {
|
|
368
|
+
this.reconnectAttempt += 1;
|
|
369
|
+
const reconnectDelay = this.getReconnectDelay();
|
|
370
|
+
if (reconnectDelay !== false) {
|
|
371
|
+
this.state = "reconnecting";
|
|
372
|
+
this.reconnectAbort = new AbortController();
|
|
373
|
+
try {
|
|
374
|
+
await setTimeout$1(Math.max(1, reconnectDelay), void 0, { signal: this.reconnectAbort.signal });
|
|
375
|
+
} catch (e) {
|
|
376
|
+
this.reset();
|
|
377
|
+
this.emit("close");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (this.state !== "reconnecting") return;
|
|
381
|
+
this.ws?.removeAllListeners();
|
|
382
|
+
this.ws = null;
|
|
383
|
+
this.emit("reconnecting", this.reconnectAttempt);
|
|
384
|
+
this.createWs();
|
|
385
|
+
} else {
|
|
386
|
+
this.reset();
|
|
387
|
+
this.emit("close");
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/wait-registry/errors.ts
|
|
393
|
+
var TimeoutError = class extends Error {
|
|
394
|
+
constructor(key) {
|
|
395
|
+
super(`Timed out waiting for key: ${key.toString()}`);
|
|
396
|
+
this.name = "TimeoutError";
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var RegistryClearedError = class extends Error {
|
|
400
|
+
constructor() {
|
|
401
|
+
super("Registry cleared");
|
|
402
|
+
this.name = "RegistryClearedError";
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/wait-registry/wait-registry.ts
|
|
407
|
+
var WaitRegistry = class {
|
|
408
|
+
storage = /* @__PURE__ */ new Map();
|
|
409
|
+
add(key, expiresInMs) {
|
|
410
|
+
if (this.storage.has(key)) throw new TypeError(`Key already exists: ${key.toString()}`);
|
|
411
|
+
const deferred = defer();
|
|
412
|
+
const timer = setTimeout(this.rejectOnTimeout.bind(this, key), expiresInMs);
|
|
413
|
+
timer.unref();
|
|
414
|
+
this.storage.set(key, {
|
|
415
|
+
deferred,
|
|
416
|
+
_timer: timer
|
|
417
|
+
});
|
|
418
|
+
return deferred.promise;
|
|
419
|
+
}
|
|
420
|
+
resolve(key, value) {
|
|
421
|
+
const awaiter = this.storage.get(key);
|
|
422
|
+
if (!awaiter) return;
|
|
423
|
+
clearTimeout(awaiter._timer);
|
|
424
|
+
this.storage.delete(key);
|
|
425
|
+
awaiter.deferred.resolve(value);
|
|
426
|
+
}
|
|
427
|
+
reject(key, error) {
|
|
428
|
+
const awaiter = this.storage.get(key);
|
|
429
|
+
if (!awaiter) return;
|
|
430
|
+
clearTimeout(awaiter._timer);
|
|
431
|
+
this.storage.delete(key);
|
|
432
|
+
awaiter.deferred.reject(error);
|
|
433
|
+
}
|
|
434
|
+
clear(passError) {
|
|
435
|
+
const error = passError ?? new RegistryClearedError();
|
|
436
|
+
for (const awaiter of this.storage.values()) {
|
|
437
|
+
clearTimeout(awaiter._timer);
|
|
438
|
+
awaiter.deferred.reject(error);
|
|
439
|
+
}
|
|
440
|
+
this.storage.clear();
|
|
441
|
+
}
|
|
442
|
+
rejectOnTimeout(key) {
|
|
443
|
+
const awaiter = this.storage.get(key);
|
|
444
|
+
if (!awaiter) return;
|
|
445
|
+
this.storage.delete(key);
|
|
446
|
+
clearTimeout(awaiter._timer);
|
|
447
|
+
awaiter.deferred.reject(new TimeoutError(key));
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/logging.ts
|
|
452
|
+
const noopLogger = (level, message, context) => {};
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/ws/messages.ts
|
|
455
|
+
const wsOkMessageSchema = z.object({
|
|
456
|
+
id: z.number(),
|
|
457
|
+
sid: z.number(),
|
|
458
|
+
type: z.literal("ok"),
|
|
459
|
+
msg: z.any().optional()
|
|
460
|
+
});
|
|
461
|
+
const wsErrorMessageSchema = z.object({
|
|
462
|
+
id: z.number(),
|
|
463
|
+
type: z.literal("error"),
|
|
464
|
+
msg: z.object({
|
|
465
|
+
code: z.number(),
|
|
466
|
+
msg: z.string()
|
|
467
|
+
})
|
|
468
|
+
});
|
|
469
|
+
const wsSubscribedMessageSchema = z.object({
|
|
470
|
+
id: z.number(),
|
|
471
|
+
type: z.literal("subscribed"),
|
|
472
|
+
msg: z.object({
|
|
473
|
+
channel: z.string(),
|
|
474
|
+
sid: z.number()
|
|
475
|
+
})
|
|
476
|
+
});
|
|
477
|
+
const wsUserOrderMessageSchema = z.object({
|
|
478
|
+
sid: z.number().positive(),
|
|
479
|
+
type: z.literal("user_order"),
|
|
480
|
+
msg: orders$orderWsSchema
|
|
481
|
+
});
|
|
482
|
+
const wsAnyMessageSchema = z.discriminatedUnion("type", [
|
|
483
|
+
wsOkMessageSchema,
|
|
484
|
+
wsErrorMessageSchema,
|
|
485
|
+
wsSubscribedMessageSchema,
|
|
486
|
+
wsUserOrderMessageSchema
|
|
487
|
+
]);
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/ws/errors.ts
|
|
490
|
+
var WsKalshiError = class extends Error {
|
|
491
|
+
id;
|
|
492
|
+
code;
|
|
493
|
+
/**
|
|
494
|
+
* @param {number} id
|
|
495
|
+
* @param {number} code
|
|
496
|
+
* @param {string} message
|
|
497
|
+
*/
|
|
498
|
+
constructor(id, code, message) {
|
|
499
|
+
super(message);
|
|
500
|
+
this.id = id;
|
|
501
|
+
this.code = code;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/ws/ws-client.ts
|
|
506
|
+
var WsClient = class extends TypedEmitter {
|
|
507
|
+
id;
|
|
508
|
+
baseURL;
|
|
509
|
+
credentialsProvider;
|
|
510
|
+
log;
|
|
511
|
+
ws;
|
|
512
|
+
lastMessageId = 0;
|
|
513
|
+
subscribeAwaiters = new WaitRegistry();
|
|
514
|
+
/**
|
|
515
|
+
* @param {WsClientOptions} options
|
|
516
|
+
*/
|
|
517
|
+
constructor(options) {
|
|
518
|
+
super();
|
|
519
|
+
this.id = options.id ?? v4();
|
|
520
|
+
this.baseURL = KALSHI_WS_URL[options.environment ?? "development"];
|
|
521
|
+
this.credentialsProvider = options.credentialsProvider;
|
|
522
|
+
this.log = options.log ?? noopLogger;
|
|
523
|
+
const path = "/trade-api/ws/v2", address = `${this.baseURL}${path}`;
|
|
524
|
+
this.ws = new WsWrapper({
|
|
525
|
+
...options.wsOptions,
|
|
526
|
+
address,
|
|
527
|
+
headers: () => {
|
|
528
|
+
const timestamp = Date.now();
|
|
529
|
+
const signature = this.credentialsProvider.getSignature({
|
|
530
|
+
timestamp,
|
|
531
|
+
httpMethod: "GET",
|
|
532
|
+
path
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
"KALSHI-ACCESS-KEY": this.credentialsProvider.getAccessKey(),
|
|
537
|
+
"KALSHI-ACCESS-SIGNATURE": signature,
|
|
538
|
+
"KALSHI-ACCESS-TIMESTAMP": timestamp.toString()
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
this.ws.on("ready", this.onReady);
|
|
543
|
+
this.ws.on("message", this.onMessage);
|
|
544
|
+
this.ws.on("error", this.onError);
|
|
545
|
+
this.ws.on("reconnecting", this.onReconnecting);
|
|
546
|
+
this.ws.on("close", this.onClose);
|
|
547
|
+
}
|
|
548
|
+
onReady = async (afterReconnect) => {
|
|
549
|
+
this.log("debug", "WebSocket connection ready", { afterReconnect });
|
|
550
|
+
this.emit("ready");
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* @param {WebSocket.RawData} data
|
|
554
|
+
* @param {boolean} isBinary
|
|
555
|
+
* @return {Promise<void>}
|
|
556
|
+
*/
|
|
557
|
+
onMessage = async (data, isBinary) => {
|
|
558
|
+
if (isBinary) {
|
|
559
|
+
this.log("warn", "invalid incoming message format (=binary)");
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
let jsonData = null;
|
|
563
|
+
try {
|
|
564
|
+
jsonData = JSON.parse(data.toString());
|
|
565
|
+
} catch (e) {
|
|
566
|
+
this.log("warn", "failed to decode incoming message as JSON string");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
this.log("debug", "incoming message", { message: jsonData });
|
|
570
|
+
const { data: msg, error } = wsAnyMessageSchema.safeParse(jsonData);
|
|
571
|
+
if (error) {
|
|
572
|
+
this.log("warn", "failed to validate message", {
|
|
573
|
+
message: jsonData,
|
|
574
|
+
error
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
this.processMessage(msg);
|
|
579
|
+
};
|
|
580
|
+
/**
|
|
581
|
+
* @param {number} attempt
|
|
582
|
+
*/
|
|
583
|
+
onReconnecting = (attempt) => {
|
|
584
|
+
this.log("debug", "reconnecting", { attempt });
|
|
585
|
+
this.emit("reconnect", attempt);
|
|
586
|
+
};
|
|
587
|
+
/**
|
|
588
|
+
* @param {Error} err
|
|
589
|
+
*/
|
|
590
|
+
onError = (err) => {
|
|
591
|
+
this.log("error", "WebSocket error", { err });
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
*/
|
|
595
|
+
onClose = () => {
|
|
596
|
+
this.log("debug", "WebSocket connection closed");
|
|
597
|
+
this.emit("close");
|
|
598
|
+
this.subscribeAwaiters.clear();
|
|
599
|
+
};
|
|
600
|
+
/**
|
|
601
|
+
* @param {WsAnyMessage} msg
|
|
602
|
+
* @private
|
|
603
|
+
*/
|
|
604
|
+
processMessage(msg) {
|
|
605
|
+
switch (msg.type) {
|
|
606
|
+
case "ok":
|
|
607
|
+
this.subscribeAwaiters.resolve(msg.id, true);
|
|
608
|
+
break;
|
|
609
|
+
case "error":
|
|
610
|
+
this.subscribeAwaiters.reject(msg.id, new WsKalshiError(msg.id, msg.msg.code, msg.msg.msg));
|
|
611
|
+
break;
|
|
612
|
+
case "subscribed":
|
|
613
|
+
this.subscribeAwaiters.resolve(msg.id, true);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
this.emit("message", msg);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/ws/common.ts
|
|
621
|
+
const WS_KALSHI_ERRORS = {
|
|
622
|
+
UNABLE_TO_PROCESS: 1,
|
|
623
|
+
PARAMS_REQUIRED: 2,
|
|
624
|
+
CHANNELS_REQUIRED: 3,
|
|
625
|
+
SUBSCRIPTION_IDS_REQUIRED: 4,
|
|
626
|
+
UNKNOWN_COMMAND: 5,
|
|
627
|
+
ALREADY_SUBSCRIBED: 6,
|
|
628
|
+
UNKNOWN_SUBSCRIPTION_ID: 7,
|
|
629
|
+
UNKNOWN_CHANNEL_NAME: 8,
|
|
630
|
+
AUTHENTICATION_REQUIRED: 9,
|
|
631
|
+
CHANNEL_ERROR: 10,
|
|
632
|
+
INVALID_PARAMETER: 11,
|
|
633
|
+
EXACTLY_ONE_SUBSCRIPTION_ID_IS_REQUIRED: 12,
|
|
634
|
+
UNSUPPORTED_ACTION: 13,
|
|
635
|
+
MARKET_TICKER_REQUIRED: 14,
|
|
636
|
+
ACTION_REQUIRED: 15,
|
|
637
|
+
MARKET_NOT_FOUND: 16,
|
|
638
|
+
INTERNAL_ERROR: 17,
|
|
639
|
+
COMMAND_TIMEOUT: 18,
|
|
640
|
+
SHARD_FACTOR_MUST_BE_GT0: 19,
|
|
641
|
+
SHARD_FACTOR_IS_REQUIRED: 20,
|
|
642
|
+
INVALID_SHARD_KEY: 21,
|
|
643
|
+
SHARD_FACTOR_MUST_BE_LT100: 22,
|
|
644
|
+
SUBSCRIPTION_BUFFER_OVERFLOW: 25
|
|
645
|
+
};
|
|
646
|
+
const WS_KALSHI_SERVER_ERRORS = [
|
|
647
|
+
WS_KALSHI_ERRORS.CHANNEL_ERROR,
|
|
648
|
+
WS_KALSHI_ERRORS.INTERNAL_ERROR,
|
|
649
|
+
WS_KALSHI_ERRORS.COMMAND_TIMEOUT
|
|
650
|
+
];
|
|
651
|
+
//#endregion
|
|
652
|
+
export { InMemoryCredentialsProvider, RequestSigner, WS_KALSHI_ERRORS, WS_KALSHI_SERVER_ERRORS, WsClient, WsKalshiError, orders$orderDetailedSchema, orders$orderStatusSchema, orders$orderTypeSchema, orders$orderWsSchema, orders$selfTradePreventionTypeSchema, primitives$bookSideSchema, primitives$fixedPointSchema, primitives$orderActionSchema, primitives$orderSideSchema, wsAnyMessageSchema, wsErrorMessageSchema, wsOkMessageSchema, wsSubscribedMessageSchema, wsUserOrderMessageSchema };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jsnw/kalshi-client",
|
|
3
|
+
"private": false,
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.0.1",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"node": {
|
|
13
|
+
"import": {
|
|
14
|
+
"default": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.mts"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"default": "./dist/index.cjs",
|
|
19
|
+
"types": "./dist/index.d.cts"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"uuid": "^14.0.0",
|
|
28
|
+
"ws": "^8.21.0",
|
|
29
|
+
"zod": "^4.4.3"
|
|
30
|
+
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"bufferutil": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^24.12.4",
|
|
36
|
+
"@types/ws": "^8.18.1",
|
|
37
|
+
"nodemon": "^3.1.14",
|
|
38
|
+
"rimraf": "^6.1.3",
|
|
39
|
+
"ts-node": "^10.9.2",
|
|
40
|
+
"tsdown": "^0.22.1",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"clean": "rimraf ./dist",
|
|
45
|
+
"build": "tsdown"
|
|
46
|
+
}
|
|
47
|
+
}
|