UNPKG

ftx-api-typed

Version:

Node.js/typescript connector for FTX's REST APIs and WebSockets

336 lines 16 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebsocketClient = exports.wsKeyGeneral = exports.WsConnectionState = void 0; const events_1 = require("events"); const rest_client_1 = require("./rest-client"); const logger_1 = require("./logger"); const requestUtils_1 = require("./util/requestUtils"); const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); const WsStore_1 = __importDefault(require("./util/WsStore")); const wsMessages_1 = require("./util/wsMessages"); const node_support_1 = require("./util/node-support"); const loggerCategory = { category: 'ftx-ws' }; const READY_STATE_INITIAL = 0; const READY_STATE_CONNECTING = 1; const READY_STATE_CONNECTED = 2; const READY_STATE_CLOSING = 3; const READY_STATE_RECONNECTING = 4; var WsConnectionState; (function (WsConnectionState) { WsConnectionState[WsConnectionState["READY_STATE_INITIAL"] = 0] = "READY_STATE_INITIAL"; WsConnectionState[WsConnectionState["READY_STATE_CONNECTING"] = 1] = "READY_STATE_CONNECTING"; WsConnectionState[WsConnectionState["READY_STATE_CONNECTED"] = 2] = "READY_STATE_CONNECTED"; WsConnectionState[WsConnectionState["READY_STATE_CLOSING"] = 3] = "READY_STATE_CLOSING"; WsConnectionState[WsConnectionState["READY_STATE_RECONNECTING"] = 4] = "READY_STATE_RECONNECTING"; })(WsConnectionState = exports.WsConnectionState || (exports.WsConnectionState = {})); ; exports.wsKeyGeneral = 'ftx'; ; class WebsocketClient extends events_1.EventEmitter { constructor(options, logger) { var _a; super(); this.logger = logger || logger_1.DefaultLogger; this.wsStore = new WsStore_1.default(this.logger); this.options = Object.assign({ pongTimeout: 7500, pingInterval: 10000, reconnectTimeout: 500 }, options); if (options.domain != ((_a = this.options.restOptions) === null || _a === void 0 ? void 0 : _a.domain)) { this.options.restOptions = Object.assign(Object.assign({}, this.options.restOptions), { domain: options.domain }); } this.restClient = new rest_client_1.RestClient(undefined, undefined, this.options.restOptions, this.options.requestOptions); } isLivenet() { return true; } /** * Add topic/topics to WS subscription list */ subscribe(wsTopics) { const mixedTopics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; const topics = mixedTopics.map(topic => { return typeof topic === 'string' ? { channel: topic } : topic; }); topics.forEach(topic => this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic)); // attempt to send subscription topic per websocket this.wsStore.getKeys().forEach(wsKey => { // if connected, send subscription request if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { return this.requestSubscribeTopics(wsKey, topics); } // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect if (!this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING) && !this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)) { return this.connect(wsKey); } }); } /** * Remove topic/topics from WS subscription list */ unsubscribe(wsTopics) { const mixedTopics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; const topics = mixedTopics.map(topic => { return typeof topic === 'string' ? { channel: topic } : topic; }); topics.forEach(topic => this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic)); this.wsStore.getKeys().forEach(wsKey => { // unsubscribe request only necessary if active connection exists if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { this.requestUnsubscribeTopics(wsKey, topics); } }); } close(wsKey) { var _a; this.logger.info('Closing connection', Object.assign(Object.assign({}, loggerCategory), { wsKey })); this.setWsState(wsKey, READY_STATE_CLOSING); this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); (_a = this.getWs(wsKey)) === null || _a === void 0 ? void 0 : _a.close(); } /** * Request connection of all dependent websockets, instead of waiting for automatic connection by library */ connectAll() { return [this.connect(exports.wsKeyGeneral)]; } connect(wsKey) { return __awaiter(this, void 0, void 0, function* () { try { if (this.wsStore.isWsOpen(wsKey)) { this.logger.error('Refused to connect to ws with existing active connection', Object.assign(Object.assign({}, loggerCategory), { wsKey })); return this.wsStore.getWs(wsKey); } if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { this.logger.error('Refused to connect to ws, connection attempt already active', Object.assign(Object.assign({}, loggerCategory), { wsKey })); return; } if (!this.wsStore.getConnectionState(wsKey) || this.wsStore.isConnectionState(wsKey, READY_STATE_INITIAL)) { this.setWsState(wsKey, READY_STATE_CONNECTING); } const url = (0, requestUtils_1.getWsUrl)(this.options); const ws = this.connectToWsUrl(url, wsKey); return this.wsStore.setWs(wsKey, ws); } catch (err) { this.parseWsError('Connection failed', err, wsKey); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); this.emit('error', { error: err, wsKey, type: 'CONNECTION_FAILED' }); } }); } requestTryAuthenticate(wsKey) { return __awaiter(this, void 0, void 0, function* () { const { key, secret, subAccountName } = this.options; if (!key || !secret) { this.logger.debug(`Connection "${wsKey}" will remain unauthenticated due to missing key/secret`); return; } const timestamp = new Date().getTime(); const authMsg = (0, wsMessages_1.getWsAuthMessage)(key, yield (0, node_support_1.signWsAuthenticate)(timestamp, secret), timestamp, subAccountName); this.tryWsSend(wsKey, JSON.stringify(authMsg)); }); } parseWsError(context, error, wsKey) { const logContext = Object.assign(Object.assign({}, loggerCategory), { wsKey, error }); if (!error.message) { this.logger.error(`${context} due to unexpected error: `, logContext); return; } switch (error.message) { case 'Unexpected server response: 401': this.logger.error(`${context} due to 401 authorization failure.`, logContext); break; default: this.logger.error(`${context} due to unexpected response error: ${(error === null || error === void 0 ? void 0 : error.msg) || (error === null || error === void 0 ? void 0 : error.message) || error}`, logContext); break; } } /** * Return params required to make authorized request */ getAuthParams(wsKey) { return __awaiter(this, void 0, void 0, function* () { const { key, secret } = this.options; if (key && secret) { this.logger.debug('Getting auth\'d request params', Object.assign(Object.assign({}, loggerCategory), { wsKey })); const timeOffset = yield this.restClient.getTimeOffset(); const params = { api_key: this.options.key, expires: (Date.now() + timeOffset + 5000) }; params.signature = (0, node_support_1.signMessage)('GET/realtime' + params.expires, secret); return params; } else if (!key || !secret) { this.logger.warning('Connot authenticate websocket, either api or private keys missing.', Object.assign(Object.assign({}, loggerCategory), { wsKey })); } else { this.logger.debug('Starting public only websocket client.', Object.assign(Object.assign({}, loggerCategory), { wsKey })); } return ''; }); } reconnectWithDelay(wsKey, connectionDelayMs) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CONNECTING) { this.setWsState(wsKey, READY_STATE_RECONNECTING); } setTimeout(() => { this.logger.info('Reconnecting to websocket', Object.assign(Object.assign({}, loggerCategory), { wsKey })); this.connect(wsKey); }, connectionDelayMs); } ping(wsKey) { this.clearPongTimer(wsKey); this.logger.silly('Sending ping', Object.assign(Object.assign({}, loggerCategory), { wsKey })); this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' })); this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { var _a; this.logger.info('Pong timeout - clearing timers & closing socket to reconnect', Object.assign(Object.assign({}, loggerCategory), { wsKey })); this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); (_a = this.getWs(wsKey)) === null || _a === void 0 ? void 0 : _a.close(); }, this.options.pongTimeout); } // Send a ping at intervals clearPingTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (wsState === null || wsState === void 0 ? void 0 : wsState.activePingTimer) { clearInterval(wsState.activePingTimer); wsState.activePingTimer = undefined; } } // Expect a pong within a time limit clearPongTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (wsState === null || wsState === void 0 ? void 0 : wsState.activePongTimer) { clearTimeout(wsState.activePongTimer); wsState.activePongTimer = undefined; } } /** * Send WS message to subscribe to topics. */ requestSubscribeTopics(wsKey, topics) { topics.forEach(topic => { const wsMessage = JSON.stringify(Object.assign({ op: 'subscribe' }, topic)); this.tryWsSend(wsKey, wsMessage); }); } /** * Send WS message to unsubscribe from topics. */ requestUnsubscribeTopics(wsKey, topics) { topics.forEach(topic => { const wsMessage = JSON.stringify(Object.assign({ op: 'unsubscribe' }, topic)); this.tryWsSend(wsKey, wsMessage); }); } tryWsSend(wsKey, wsMessage) { var _a; try { this.logger.silly(`Sending upstream ws message: `, Object.assign(Object.assign({}, loggerCategory), { wsMessage, wsKey })); if (!wsKey) { throw new Error('Cannot send message due to no known websocket for this wsKey'); } (_a = this.getWs(wsKey)) === null || _a === void 0 ? void 0 : _a.send(wsMessage); } catch (e) { this.logger.error(`Failed to send WS message`, Object.assign(Object.assign({}, loggerCategory), { wsMessage, wsKey, exception: e })); } } connectToWsUrl(url, wsKey) { this.logger.silly(`Opening WS connection to URL: ${url}`, Object.assign(Object.assign({}, loggerCategory), { wsKey })); const ws = new isomorphic_ws_1.default(url); ws.onopen = (event) => this.onWsOpen(event, wsKey); ws.onmessage = (event) => this.onWsMessage(event, wsKey); ws.onerror = (event) => this.onWsError(event, wsKey); ws.onclose = (event) => this.onWsClose(event, wsKey); return ws; } onWsOpen(event, wsKey) { return __awaiter(this, void 0, void 0, function* () { if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { this.logger.info('Websocket connected', Object.assign(Object.assign({}, loggerCategory), { wsKey, livenet: this.isLivenet() })); this.emit('open', { wsKey, event }); } else if (this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)) { this.logger.info('Websocket reconnected', Object.assign(Object.assign({}, loggerCategory), { wsKey })); this.emit('reconnected', { wsKey, event }); } this.setWsState(wsKey, READY_STATE_CONNECTED); yield this.requestTryAuthenticate(wsKey); this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]); this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval); }); } onWsMessage(event, wsKey) { try { this.clearPongTimer(wsKey); const msg = (0, requestUtils_1.parseRawWsMessage)(event); if (msg.channel) { this.emit('update', msg); } else { this.logger.debug('Websocket event: ', event.data || event); this.onWsMessageResponse(msg, wsKey); } } catch (e) { this.logger.error('Exception parsing ws message: ', Object.assign(Object.assign({}, loggerCategory), { rawEvent: event, wsKey, error: e })); this.emit('error', { wsKey, error: e, rawEvent: event }); } } onWsError(err, wsKey) { this.parseWsError('Websocket error', err, wsKey); if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { this.emit('error', err); } } onWsClose(event, wsKey) { this.logger.info('Websocket connection closed', Object.assign(Object.assign({}, loggerCategory), { wsKey })); if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) { this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); this.emit('reconnect'); } else { this.setWsState(wsKey, READY_STATE_INITIAL); this.emit('close'); } } onWsMessageResponse(response, wsKey) { if ((0, wsMessages_1.isWsPong)(response)) { this.logger.silly('Received pong', Object.assign(Object.assign({}, loggerCategory), { wsKey })); this.clearPongTimer(wsKey); } else { this.emit('response', response); } } getWs(wsKey) { return this.wsStore.getWs(wsKey); } setWsState(wsKey, state) { this.wsStore.setConnectionState(wsKey, state); } getWsKeyForTopic(topic) { return exports.wsKeyGeneral; } } exports.WebsocketClient = WebsocketClient; ; //# sourceMappingURL=websocket-client.js.map