kucoin-api
Version:
Complete & robust Node.js SDK for Kucoin's REST APIs and WebSockets, with TypeScript & strong end to end tests.
334 lines • 12.2 kB
JavaScript
import { FuturesClient } from './FuturesClient.js';
import { BaseWebsocketClient } from './lib/BaseWSClient.js';
import { neverGuard } from './lib/misc-util.js';
import { WS_KEY_MAP, } from './lib/websocket/websocket-util.js';
import { SpotClient } from './SpotClient.js';
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
export const WS_LOGGER_CATEGORY = { category: 'kucoin-ws' };
/** Any WS keys in this list will trigger auth on connect, if credentials are available */
const PRIVATE_WS_KEYS = [
WS_KEY_MAP.spotPrivateV1,
WS_KEY_MAP.futuresPrivateV1,
];
/** Any WS keys in this list will ALWAYS skip the authentication process, even if credentials are available */
export const PUBLIC_WS_KEYS = [
WS_KEY_MAP.spotPublicV1,
WS_KEY_MAP.futuresPublicV1,
];
export class WebsocketClient extends BaseWebsocketClient {
RESTClientCache = {
spot: undefined,
futures: undefined,
};
getRESTClient(wsKey) {
const getClientType = (wsKey) => {
if (wsKey.startsWith('spot'))
return 'spot';
if (wsKey.startsWith('futures'))
return 'futures';
return null;
};
const clientType = getClientType(wsKey);
if (!clientType) {
throw new Error(`Unhandled WsKey: "${wsKey}"`);
}
if (this.RESTClientCache[clientType]) {
return this.RESTClientCache[clientType];
}
const ClientClass = clientType === 'spot' ? SpotClient : FuturesClient;
const newClient = new ClientClass(this.getRestClientOptions(), this.options.requestOptions);
this.RESTClientCache[clientType] = newClient;
return newClient;
}
getRestClientOptions() {
return {
apiKey: this.options.apiKey,
apiSecret: this.options.apiSecret,
...this.options,
...this.options.restOptions,
};
}
async getWSConnectionInfo(wsKey) {
const restClient = this.getRESTClient(wsKey);
if (wsKey === 'spotPrivateV1' || wsKey === 'futuresPrivateV1') {
return restClient.getPrivateWSConnectionToken();
}
return restClient.getPublicWSConnectionToken();
}
/**
* Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
*/
connectAll() {
return Promise.all([
this.connect(WS_KEY_MAP.spotPublicV1),
this.connect(WS_KEY_MAP.spotPrivateV1),
this.connect(WS_KEY_MAP.futuresPublicV1),
this.connect(WS_KEY_MAP.futuresPrivateV1),
]);
}
/**
* Request subscription to one or more topics. Pass topics as either an array of strings, or array of objects (if the topic has parameters).
* Objects should be formatted as {topic: string, params: object}.
*
* - Subscriptions are automatically routed to the correct websocket connection.
* - Authentication/connection is automatic.
* - Resubscribe after network issues is automatic.
*
* Call `unsubscribe(topics)` to remove topics
*/
subscribe(requests, wsKey) {
if (!Array.isArray(requests)) {
this.subscribeTopicsForWsKey([requests], wsKey);
return;
}
if (requests.length) {
this.subscribeTopicsForWsKey(requests, wsKey);
}
}
/**
* Unsubscribe from one or more topics. Similar to subscribe() but in reverse.
*
* - Requests are automatically routed to the correct websocket connection.
* - These topics will be removed from the topic cache, so they won't be subscribed to again.
*/
unsubscribe(requests, wsKey) {
if (!Array.isArray(requests)) {
this.unsubscribeTopicsForWsKey([requests], wsKey);
return;
}
if (requests.length) {
this.unsubscribeTopicsForWsKey(requests, wsKey);
}
}
async sendWSAPIRequest(wsKey, channel, params) {
this.logger.trace(`sendWSAPIRequest(): assert "${wsKey}" is connected`, {
channel,
params,
});
return;
}
/**
*
* Internal methods
*
*/
/**
* Whatever url this method returns, it's connected to as-is!
*
* If a token or anything else is needed in the URL, this is a good place to add it.
*/
async getWsUrl(wsKey) {
if (this.options.wsUrl) {
return this.options.wsUrl;
}
const connectionInfo = await this.getWSConnectionInfo(wsKey);
this.logger.trace(`getWSConnectionInfo`, {
wsKey,
...connectionInfo,
});
const server = connectionInfo.data.instanceServers[0];
if (!server) {
this.logger.error(`No servers returned by connection info response?`, JSON.stringify({
wsKey,
connectionInfo,
}, null, 2));
throw new Error(`No servers returned by connection info response?`);
}
const connectionUrl = `${server.endpoint}?token=${connectionInfo.data.token}`;
return connectionUrl;
}
sendPingEvent(wsKey) {
return this.tryWsSend(wsKey, `{ "id": "${Date.now()}", "type": "ping" }`);
}
sendPongEvent(wsKey) {
try {
this.logger.trace(`Sending upstream ws PONG: `, {
...WS_LOGGER_CATEGORY,
wsMessage: 'PONG',
wsKey,
});
if (!wsKey) {
throw new Error('Cannot send PONG, no wsKey provided');
}
const wsState = this.getWsStore().get(wsKey);
if (!wsState || !wsState?.ws) {
throw new Error(`Cannot send pong, ${wsKey} socket not connected yet`);
}
// Send a protocol layer pong
wsState.ws.pong();
}
catch (e) {
this.logger.error(`Failed to send WS PONG`, {
...WS_LOGGER_CATEGORY,
wsMessage: 'PONG',
wsKey,
exception: e,
});
}
}
// Not really used for kucoin - they don't send pings
isWsPing(msg) {
if (msg?.data === 'ping') {
return true;
}
return false;
}
isWsPong(msg) {
if (msg?.data?.includes('pong')) {
return true;
}
// this.logger.info(`Not a pong: `, msg);
return false;
}
resolveEmittableEvents(wsKey, event) {
const results = [];
try {
const parsed = JSON.parse(event.data);
const responseEvents = ['subscribe', 'unsubscribe', 'ack'];
const authenticatedEvents = ['login', 'access'];
const connectionReadyEvents = ['welcome'];
const eventType = parsed.event || parsed.type;
if (typeof eventType === 'string') {
if (parsed.success === false) {
results.push({
eventType: 'exception',
event: parsed,
});
return results;
}
if (connectionReadyEvents.includes(eventType)) {
return [
{
eventType: 'connectionReady',
event: parsed,
},
];
}
// These are request/reply pattern events (e.g. after subscribing to topics or authenticating)
if (responseEvents.includes(eventType)) {
results.push({
eventType: 'response',
event: parsed,
});
return results;
}
// Request/reply pattern for authentication success
if (authenticatedEvents.includes(eventType)) {
results.push({
eventType: 'authenticated',
event: parsed,
});
return results;
}
if (eventType === 'message') {
return [{ eventType: 'update', event: parsed }];
}
this.logger.error(`!! (${wsKey}) Unhandled string event type "${eventType}". Defaulting to "update" channel...`, parsed);
results.push({
eventType: 'update',
event: parsed,
});
return results;
}
this.logger.error(`!! (${wsKey}) Unhandled non-string event type "${eventType}". Defaulting to "update" channel...`, parsed);
results.push({
eventType: 'update',
event: parsed,
});
}
catch (e) {
results.push({
event: {
message: 'Failed to parse event data due to exception',
exception: e,
eventData: event.data,
},
eventType: 'exception',
});
this.logger.error(`Failed to parse event data due to exception: `, {
exception: e,
eventData: event.data,
});
}
return results;
}
/**
* Determines if a topic is for a private channel, using a hardcoded list of strings
*/
isPrivateTopicRequest(request, wsKey) {
return request && PRIVATE_WS_KEYS.includes(wsKey);
}
getWsKeyForMarket(market, isPrivate) {
return isPrivate
? market === 'spot'
? WS_KEY_MAP.spotPrivateV1
: WS_KEY_MAP.futuresPrivateV1
: market === 'spot'
? WS_KEY_MAP.spotPublicV1
: WS_KEY_MAP.futuresPublicV1;
}
getWsMarketForWsKey(key) {
switch (key) {
case 'futuresPrivateV1':
case 'futuresPublicV1': {
return 'futures';
}
case 'spotPrivateV1':
case 'spotPublicV1': {
return 'spot';
}
default: {
throw neverGuard(key, `Unhandled ws key "${key}"`);
}
}
}
getPrivateWSKeys() {
return PRIVATE_WS_KEYS;
}
/** Force subscription requests to be sent in smaller batches, if a number is returned */
getMaxTopicsPerSubscribeEvent(wsKey) {
switch (wsKey) {
case 'futuresPrivateV1':
case 'futuresPublicV1':
case 'spotPrivateV1':
case 'spotPublicV1': {
// Return a number if there's a limit on the number of sub topics per rq
// Always 1 at a time for this exchange
return 1;
}
default: {
throw neverGuard(wsKey, `getMaxTopicsPerSubscribeEvent(): Unhandled wsKey`);
}
}
}
/**
* Map one or more topics into fully prepared "subscribe request" events (already stringified and ready to send)
*/
async getWsOperationEventsForTopics(topicRequests, wsKey, operation) {
if (!topicRequests.length) {
return [];
}
// Operations structured in a way that this exchange understands
const operationEvents = topicRequests.map((topicRequest) => {
const isPrivateWsTopic = this.isPrivateTopicRequest(topicRequest, wsKey);
const wsRequestEvent = {
id: getRandomInt(999999999999),
type: operation,
topic: topicRequest.topic,
privateChannel: isPrivateWsTopic,
response: true,
...topicRequest.payload,
};
return wsRequestEvent;
});
// Events that are ready to send (usually stringified JSON)
return operationEvents.map((event) => JSON.stringify(event));
}
// Not used for kucoin - auth is part of the WS URL
async getWsAuthRequestEvent(wsKey) {
return { wsKey };
}
}
//# sourceMappingURL=WebsocketClient.js.map