UNPKG

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
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