bybit-api
Advanced tools
Comparing version 1.3.2 to 2.0.0
@@ -1,3 +0,4 @@ | ||
export * from './rest-client'; | ||
export * from './inverse-client'; | ||
export * from './linear-client'; | ||
export * from './websocket-client'; | ||
export * from './logger'; |
@@ -13,5 +13,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./rest-client"), exports); | ||
__exportStar(require("./inverse-client"), exports); | ||
__exportStar(require("./linear-client"), exports); | ||
__exportStar(require("./websocket-client"), exports); | ||
__exportStar(require("./logger"), exports); | ||
//# sourceMappingURL=index.js.map |
@@ -1,2 +0,2 @@ | ||
export interface RestClientInverseOptions { | ||
export interface RestClientOptions { | ||
recv_window?: number; | ||
@@ -12,2 +12,4 @@ sync_interval_ms?: number | string; | ||
export declare function serializeParams(params?: object, strict_validation?: boolean): string; | ||
export declare function getBaseRESTInverseUrl(useLivenet?: boolean, restInverseOptions?: RestClientInverseOptions): string; | ||
export declare function getRestBaseUrl(useLivenet?: boolean, restInverseOptions?: RestClientOptions): string; | ||
export declare function isPublicEndpoint(endpoint: string): boolean; | ||
export declare function isWsPong(response: any): any; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getBaseRESTInverseUrl = exports.serializeParams = exports.signMessage = void 0; | ||
exports.isWsPong = exports.isPublicEndpoint = exports.getRestBaseUrl = exports.serializeParams = exports.signMessage = void 0; | ||
const crypto_1 = require("crypto"); | ||
@@ -26,3 +26,3 @@ function signMessage(message, secret) { | ||
; | ||
function getBaseRESTInverseUrl(useLivenet, restInverseOptions) { | ||
function getRestBaseUrl(useLivenet, restInverseOptions) { | ||
const baseUrlsInverse = { | ||
@@ -40,3 +40,20 @@ livenet: 'https://api.bybit.com', | ||
} | ||
exports.getBaseRESTInverseUrl = getBaseRESTInverseUrl; | ||
exports.getRestBaseUrl = getRestBaseUrl; | ||
function isPublicEndpoint(endpoint) { | ||
if (endpoint.startsWith('v2/public')) { | ||
return true; | ||
} | ||
if (endpoint.startsWith('public/linear')) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
exports.isPublicEndpoint = isPublicEndpoint; | ||
function isWsPong(response) { | ||
return (response.request && | ||
response.request.op === 'ping' && | ||
response.ret_msg === 'pong' && | ||
response.success === true); | ||
} | ||
exports.isWsPong = isWsPong; | ||
//# sourceMappingURL=requestUtils.js.map |
import { AxiosRequestConfig, Method } from 'axios'; | ||
import { RestClientInverseOptions, GenericAPIResponse } from './requestUtils'; | ||
import { RestClientOptions, GenericAPIResponse } from './requestUtils'; | ||
export default class RequestUtil { | ||
@@ -11,3 +11,3 @@ private timeOffset; | ||
private secret; | ||
constructor(key: string | undefined, secret: string | undefined, baseUrl: string, options?: RestClientInverseOptions, requestOptions?: AxiosRequestConfig); | ||
constructor(key: string | undefined, secret: string | undefined, baseUrl: string, options?: RestClientOptions, requestOptions?: AxiosRequestConfig); | ||
get(endpoint: string, params?: any): GenericAPIResponse; | ||
@@ -14,0 +14,0 @@ post(endpoint: string, params?: any): GenericAPIResponse; |
@@ -54,4 +54,3 @@ "use strict"; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const publicEndpoint = endpoint.startsWith('v2/public'); | ||
if (!publicEndpoint) { | ||
if (!requestUtils_1.isPublicEndpoint(endpoint)) { | ||
if (!this.key || !this.secret) { | ||
@@ -58,0 +57,0 @@ throw new Error('Private endpoints require api and private keys set'); |
/// <reference types="node" /> | ||
import { EventEmitter } from 'events'; | ||
import { DefaultLogger } from './logger'; | ||
export interface WebsocketClientOptions { | ||
import WebSocket from 'isomorphic-ws'; | ||
export declare enum WsConnectionState { | ||
READY_STATE_INITIAL = 0, | ||
READY_STATE_CONNECTING = 1, | ||
READY_STATE_CONNECTED = 2, | ||
READY_STATE_CLOSING = 3, | ||
READY_STATE_RECONNECTING = 4 | ||
} | ||
export interface WSClientConfigurableOptions { | ||
key?: string; | ||
secret?: string; | ||
livenet?: boolean; | ||
linear?: boolean; | ||
pongTimeout?: number; | ||
@@ -15,31 +24,73 @@ pingInterval?: number; | ||
} | ||
declare type Logger = typeof DefaultLogger; | ||
export interface WebsocketClientOptions extends WSClientConfigurableOptions { | ||
livenet: boolean; | ||
linear: boolean; | ||
pongTimeout: number; | ||
pingInterval: number; | ||
reconnectTimeout: number; | ||
} | ||
export declare const wsKeyInverse = "inverse"; | ||
export declare const wsKeyLinearPrivate = "linearPrivate"; | ||
export declare const wsKeyLinearPublic = "linearPublic"; | ||
export declare interface WebsocketClient { | ||
on(event: 'open' | 'reconnected', listener: ({ wsKey: string, event: any }: { | ||
wsKey: any; | ||
event: any; | ||
}) => void): this; | ||
on(event: 'response' | 'update' | 'error', listener: (response: any) => void): this; | ||
on(event: 'reconnect' | 'close', listener: () => void): this; | ||
} | ||
export declare class WebsocketClient extends EventEmitter { | ||
private logger; | ||
private readyState; | ||
private pingInterval?; | ||
private pongTimeout?; | ||
private client; | ||
private _subscriptions; | ||
private ws; | ||
private restClient; | ||
private options; | ||
constructor(options: WebsocketClientOptions, logger?: Logger); | ||
subscribe(topics: any): void; | ||
unsubscribe(topics: any): void; | ||
close(): void; | ||
_getWsUrl(): string; | ||
_connect(): Promise<void>; | ||
_authenticate(): Promise<string>; | ||
_reconnect(timeout: any): void; | ||
_ping(): void; | ||
_teardown(): void; | ||
_wsOpenHandler(): void; | ||
_wsMessageHandler(message: any): void; | ||
_wsOnErrorHandler(err: any): void; | ||
_wsCloseHandler(): void; | ||
_handleResponse(response: any): void; | ||
_handleUpdate(message: any): void; | ||
_subscribe(topics: any): void; | ||
_unsubscribe(topics: any): void; | ||
private wsStore; | ||
constructor(options: WSClientConfigurableOptions, logger?: typeof DefaultLogger); | ||
isLivenet(): boolean; | ||
isLinear(): boolean; | ||
isInverse(): boolean; | ||
/** | ||
* Add topic/topics to WS subscription list | ||
*/ | ||
subscribe(wsTopics: string[] | string): void; | ||
/** | ||
* Remove topic/topics from WS subscription list | ||
*/ | ||
unsubscribe(wsTopics: string[] | string): void; | ||
close(wsKey: string): void; | ||
/** | ||
* Request connection of all dependent websockets, instead of waiting for automatic connection by library | ||
*/ | ||
connectAll(): Promise<WebSocket | undefined>[] | undefined; | ||
private connect; | ||
private parseWsError; | ||
/** | ||
* Return params required to make authorized request | ||
*/ | ||
private getAuthParams; | ||
private reconnectWithDelay; | ||
private ping; | ||
private clearTimers; | ||
private clearPingTimer; | ||
private clearPongTimer; | ||
/** | ||
* Send WS message to subscribe to topics. | ||
*/ | ||
private requestSubscribeTopics; | ||
/** | ||
* Send WS message to unsubscribe from topics. | ||
*/ | ||
private requestUnsubscribeTopics; | ||
private tryWsSend; | ||
private connectToWsUrl; | ||
private onWsOpen; | ||
private onWsMessage; | ||
private onWsError; | ||
private onWsClose; | ||
private onWsMessageResponse; | ||
private onWsMessageUpdate; | ||
private getWs; | ||
private setWsState; | ||
private getWsUrl; | ||
private getWsKeyForTopic; | ||
} | ||
export {}; |
@@ -15,13 +15,27 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.WebsocketClient = void 0; | ||
exports.WebsocketClient = exports.wsKeyLinearPublic = exports.wsKeyLinearPrivate = exports.wsKeyInverse = exports.WsConnectionState = void 0; | ||
const events_1 = require("events"); | ||
const rest_client_1 = require("./rest-client"); | ||
const inverse_client_1 = require("./inverse-client"); | ||
const linear_client_1 = require("./linear-client"); | ||
const logger_1 = require("./logger"); | ||
const requestUtils_1 = require("./util/requestUtils"); | ||
// import WebSocket from 'ws'; | ||
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); | ||
const wsUrls = { | ||
const WsStore_1 = __importDefault(require("./util/WsStore")); | ||
const inverseEndpoints = { | ||
livenet: 'wss://stream.bybit.com/realtime', | ||
testnet: 'wss://stream-testnet.bybit.com/realtime' | ||
}; | ||
const linearEndpoints = { | ||
private: { | ||
livenet: 'wss://stream.bybit.com/realtime_private', | ||
livenet2: 'wss://stream.bytick.com/realtime_public', | ||
testnet: 'wss://stream-testnet.bybit.com/realtime_private' | ||
}, | ||
public: { | ||
livenet: 'wss://stream.bybit.com/realtime_public', | ||
livenet2: 'wss://stream.bytick.com/realtime_private', | ||
testnet: 'wss://stream-testnet.bybit.com/realtime_public' | ||
} | ||
}; | ||
const loggerCategory = { category: 'bybit-ws' }; | ||
const READY_STATE_INITIAL = 0; | ||
@@ -32,3 +46,23 @@ const READY_STATE_CONNECTING = 1; | ||
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.wsKeyInverse = 'inverse'; | ||
exports.wsKeyLinearPrivate = 'linearPrivate'; | ||
exports.wsKeyLinearPublic = 'linearPublic'; | ||
const getLinearWsKeyForTopic = (topic) => { | ||
const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'wallet']; | ||
if (privateLinearTopics.includes(topic)) { | ||
return exports.wsKeyLinearPrivate; | ||
} | ||
return exports.wsKeyLinearPublic; | ||
}; | ||
class WebsocketClient extends events_1.EventEmitter { | ||
@@ -38,63 +72,119 @@ constructor(options, logger) { | ||
this.logger = logger || logger_1.DefaultLogger; | ||
this.readyState = READY_STATE_INITIAL; | ||
this.pingInterval = undefined; | ||
this.pongTimeout = undefined; | ||
this.options = Object.assign({ livenet: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500 }, options); | ||
this.client = new rest_client_1.RestClient(undefined, undefined, this.options.livenet, this.options.restOptions, this.options.requestOptions); | ||
this._subscriptions = new Set(); | ||
this._connect(); | ||
this.wsStore = new WsStore_1.default(this.logger); | ||
this.options = Object.assign({ livenet: false, linear: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500 }, options); | ||
if (this.isLinear()) { | ||
this.restClient = new linear_client_1.LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); | ||
} | ||
else { | ||
this.restClient = new inverse_client_1.InverseClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); | ||
} | ||
} | ||
subscribe(topics) { | ||
if (!Array.isArray(topics)) | ||
topics = [topics]; | ||
topics.forEach(topic => this._subscriptions.add(topic)); | ||
// subscribe not necessary if not yet connected (will subscribe onOpen) | ||
if (this.readyState === READY_STATE_CONNECTED) | ||
this._subscribe(topics); | ||
isLivenet() { | ||
return this.options.livenet === true; | ||
} | ||
unsubscribe(topics) { | ||
if (!Array.isArray(topics)) | ||
topics = [topics]; | ||
topics.forEach(topic => this._subscriptions.delete(topic)); | ||
// unsubscribe not necessary if not yet connected | ||
if (this.readyState === READY_STATE_CONNECTED) | ||
this._unsubscribe(topics); | ||
isLinear() { | ||
return this.options.linear === true; | ||
} | ||
close() { | ||
this.logger.info('Closing connection', { category: 'bybit-ws' }); | ||
this.readyState = READY_STATE_CLOSING; | ||
this._teardown(); | ||
this.ws && this.ws.close(); | ||
isInverse() { | ||
return !this.isLinear(); | ||
} | ||
_getWsUrl() { | ||
if (this.options.wsUrl) { | ||
return this.options.wsUrl; | ||
/** | ||
* Add topic/topics to WS subscription list | ||
*/ | ||
subscribe(wsTopics) { | ||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; | ||
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, [...this.wsStore.getTopics(wsKey)]); | ||
} | ||
// 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 topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; | ||
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, [...this.wsStore.getTopics(wsKey)]); | ||
} | ||
}); | ||
} | ||
close(wsKey) { | ||
var _a; | ||
this.logger.info('Closing connection', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
this.setWsState(wsKey, READY_STATE_CLOSING); | ||
this.clearTimers(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() { | ||
if (this.isInverse()) { | ||
return [this.connect(exports.wsKeyInverse)]; | ||
} | ||
return wsUrls[this.options.livenet ? 'livenet' : 'testnet']; | ||
if (this.isLinear()) { | ||
return [this.connect(exports.wsKeyLinearPublic), this.connect(exports.wsKeyLinearPrivate)]; | ||
} | ||
} | ||
_connect() { | ||
connect(wsKey) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
if (this.readyState === READY_STATE_INITIAL) | ||
this.readyState = READY_STATE_CONNECTING; | ||
const authParams = yield this._authenticate(); | ||
const url = this._getWsUrl() + authParams; | ||
const ws = new isomorphic_ws_1.default(url); | ||
ws.onopen = this._wsOpenHandler.bind(this); | ||
ws.onmessage = this._wsMessageHandler.bind(this); | ||
ws.onerror = this._wsOnErrorHandler.bind(this); | ||
ws.onclose = this._wsCloseHandler.bind(this); | ||
this.ws = ws; | ||
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 authParams = yield this.getAuthParams(wsKey); | ||
const url = this.getWsUrl(wsKey) + authParams; | ||
const ws = this.connectToWsUrl(url, wsKey); | ||
return this.wsStore.setWs(wsKey, ws); | ||
} | ||
catch (err) { | ||
this.logger.error('Connection failed: ', err); | ||
this._reconnect(this.options.reconnectTimeout); | ||
this.parseWsError('Connection failed', err, wsKey); | ||
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); | ||
} | ||
}); | ||
} | ||
_authenticate() { | ||
parseWsError(context, error, wsKey) { | ||
if (!error.message) { | ||
this.logger.error(`${context} due to unexpected error: `, error); | ||
return; | ||
} | ||
switch (error.message) { | ||
case 'Unexpected server response: 401': | ||
this.logger.error(`${context} due to 401 authorization failure.`, Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
break; | ||
default: | ||
this.logger.error(`{context} due to unexpected response error: ${error.msg}`, Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
break; | ||
} | ||
} | ||
/** | ||
* Return params required to make authorized request | ||
*/ | ||
getAuthParams(wsKey) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (this.options.key && this.options.secret) { | ||
this.logger.debug('Starting authenticated websocket client.', { category: 'bybit-ws' }); | ||
const timeOffset = yield this.client.getTimeOffset(); | ||
const { key, secret } = this.options; | ||
if (key && secret && wsKey !== exports.wsKeyLinearPublic) { | ||
this.logger.debug('Getting auth\'d request params', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
const timeOffset = yield this.restClient.getTimeOffset(); | ||
const params = { | ||
@@ -104,10 +194,10 @@ api_key: this.options.key, | ||
}; | ||
params.signature = requestUtils_1.signMessage('GET/realtime' + params.expires, this.options.secret); | ||
params.signature = requestUtils_1.signMessage('GET/realtime' + params.expires, secret); | ||
return '?' + requestUtils_1.serializeParams(params); | ||
} | ||
else if (this.options.key || this.options.secret) { | ||
this.logger.warning('Could not authenticate websocket, either api key or private key missing.', { category: 'bybit-ws' }); | ||
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.', { category: 'bybit-ws' }); | ||
this.logger.debug('Starting public only websocket client.', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
} | ||
@@ -117,81 +207,130 @@ return ''; | ||
} | ||
_reconnect(timeout) { | ||
this._teardown(); | ||
if (this.readyState !== READY_STATE_CONNECTING) { | ||
this.readyState = READY_STATE_RECONNECTING; | ||
reconnectWithDelay(wsKey, connectionDelayMs) { | ||
this.clearTimers(wsKey); | ||
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CONNECTING) { | ||
this.setWsState(wsKey, READY_STATE_RECONNECTING); | ||
} | ||
setTimeout(() => { | ||
this.logger.info('Reconnecting to server', { category: 'bybit-ws' }); | ||
this._connect(); | ||
}, timeout); | ||
this.logger.info('Reconnecting to websocket', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
this.connect(wsKey); | ||
}, connectionDelayMs); | ||
} | ||
_ping() { | ||
clearTimeout(this.pongTimeout); | ||
delete this.pongTimeout; | ||
this.logger.silly('Sending ping', { category: 'bybit-ws' }); | ||
this.ws.send(JSON.stringify({ op: 'ping' })); | ||
this.pongTimeout = setTimeout(() => { | ||
this.logger.info('Pong timeout', { category: 'bybit-ws' }); | ||
this._teardown(); | ||
// this.ws.terminate(); | ||
// TODO: does this work? | ||
this.ws.close(); | ||
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 - closing socket to reconnect', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
(_a = this.getWs(wsKey)) === null || _a === void 0 ? void 0 : _a.close(); | ||
}, this.options.pongTimeout); | ||
} | ||
_teardown() { | ||
if (this.pingInterval) | ||
clearInterval(this.pingInterval); | ||
if (this.pongTimeout) | ||
clearTimeout(this.pongTimeout); | ||
this.pongTimeout = undefined; | ||
this.pingInterval = undefined; | ||
clearTimers(wsKey) { | ||
this.clearPingTimer(wsKey); | ||
this.clearPongTimer(wsKey); | ||
} | ||
_wsOpenHandler() { | ||
if (this.readyState === READY_STATE_CONNECTING) { | ||
this.logger.info('Websocket connected', { category: 'bybit-ws', livenet: this.options.livenet }); | ||
this.emit('open'); | ||
// 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; | ||
} | ||
else if (this.readyState === READY_STATE_RECONNECTING) { | ||
this.logger.info('Websocket reconnected', { category: 'bybit-ws', livenet: this.options.livenet }); | ||
this.emit('reconnected'); | ||
} | ||
// 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; | ||
} | ||
this.readyState = READY_STATE_CONNECTED; | ||
this._subscribe([...this._subscriptions]); | ||
this.pingInterval = setInterval(this._ping.bind(this), this.options.pingInterval); | ||
} | ||
_wsMessageHandler(message) { | ||
const msg = JSON.parse(message && message.data || message); | ||
/** | ||
* Send WS message to subscribe to topics. | ||
*/ | ||
requestSubscribeTopics(wsKey, topics) { | ||
const wsMessage = JSON.stringify({ | ||
op: 'subscribe', | ||
args: topics | ||
}); | ||
this.tryWsSend(wsKey, wsMessage); | ||
} | ||
/** | ||
* Send WS message to unsubscribe from topics. | ||
*/ | ||
requestUnsubscribeTopics(wsKey, topics) { | ||
const wsMessage = JSON.stringify({ | ||
op: 'unsubscribe', | ||
args: topics | ||
}); | ||
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) { | ||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { | ||
this.logger.info('Websocket connected', Object.assign(Object.assign({}, loggerCategory), { wsKey, livenet: this.isLivenet(), linear: this.isLinear() })); | ||
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); | ||
this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]); | ||
this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval); | ||
} | ||
onWsMessage(event, wsKey) { | ||
const msg = JSON.parse(event && event.data || event); | ||
if ('success' in msg) { | ||
this._handleResponse(msg); | ||
this.onWsMessageResponse(msg, wsKey); | ||
} | ||
else if (msg.topic) { | ||
this._handleUpdate(msg); | ||
this.onWsMessageUpdate(msg); | ||
} | ||
else { | ||
this.logger.warning('Got unhandled ws message', msg); | ||
this.logger.warning('Got unhandled ws message', Object.assign(Object.assign({}, loggerCategory), { message: msg, event, wsKey })); | ||
} | ||
} | ||
_wsOnErrorHandler(err) { | ||
this.logger.error('Websocket error', { category: 'bybit-ws', err }); | ||
if (this.readyState === READY_STATE_CONNECTED) | ||
onWsError(err, wsKey) { | ||
this.parseWsError('Websocket error', err, wsKey); | ||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { | ||
this.emit('error', err); | ||
} | ||
} | ||
_wsCloseHandler() { | ||
this.logger.info('Websocket connection closed', { category: 'bybit-ws' }); | ||
if (this.readyState !== READY_STATE_CLOSING) { | ||
this._reconnect(this.options.reconnectTimeout); | ||
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.readyState = READY_STATE_INITIAL; | ||
this.setWsState(wsKey, READY_STATE_INITIAL); | ||
this.emit('close'); | ||
} | ||
} | ||
_handleResponse(response) { | ||
if (response.request && | ||
response.request.op === 'ping' && | ||
response.ret_msg === 'pong' && | ||
response.success === true) { | ||
this.logger.silly('pong recieved', { category: 'bybit-ws' }); | ||
clearTimeout(this.pongTimeout); | ||
onWsMessageResponse(response, wsKey) { | ||
if (requestUtils_1.isWsPong(response)) { | ||
this.logger.silly('Received pong', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
this.clearPongTimer(wsKey); | ||
} | ||
@@ -202,19 +341,31 @@ else { | ||
} | ||
_handleUpdate(message) { | ||
onWsMessageUpdate(message) { | ||
this.emit('update', message); | ||
} | ||
_subscribe(topics) { | ||
const msgStr = JSON.stringify({ | ||
op: 'subscribe', | ||
'args': topics | ||
}); | ||
this.ws.send(msgStr); | ||
getWs(wsKey) { | ||
return this.wsStore.getWs(wsKey); | ||
} | ||
_unsubscribe(topics) { | ||
const msgStr = JSON.stringify({ | ||
op: 'unsubscribe', | ||
'args': topics | ||
}); | ||
this.ws.send(msgStr); | ||
setWsState(wsKey, state) { | ||
this.wsStore.setConnectionState(wsKey, state); | ||
} | ||
getWsUrl(wsKey) { | ||
if (this.options.wsUrl) { | ||
return this.options.wsUrl; | ||
} | ||
const networkKey = this.options.livenet ? 'livenet' : 'testnet'; | ||
if (this.isLinear() || wsKey.startsWith('linear')) { | ||
if (wsKey === exports.wsKeyLinearPublic) { | ||
return linearEndpoints.public[networkKey]; | ||
} | ||
if (wsKey === exports.wsKeyLinearPrivate) { | ||
return linearEndpoints.private[networkKey]; | ||
} | ||
this.logger.error('Unhandled linear wsKey: ', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
return linearEndpoints[networkKey]; | ||
} | ||
return inverseEndpoints[networkKey]; | ||
} | ||
getWsKeyForTopic(topic) { | ||
return this.isInverse() ? exports.wsKeyInverse : getLinearWsKeyForTopic(topic); | ||
} | ||
} | ||
@@ -221,0 +372,0 @@ exports.WebsocketClient = WebsocketClient; |
{ | ||
"name": "bybit-api", | ||
"version": "1.3.2", | ||
"description": "Node.js connector for the Bybit APIs and WebSockets", | ||
"version": "2.0.0", | ||
"description": "Node.js connector for Bybit's Inverse & Linear REST APIs and WebSockets", | ||
"main": "lib/index.js", | ||
@@ -14,6 +14,7 @@ "types": "lib/index.d.ts", | ||
"clean": "rm -rf lib dist", | ||
"prebuild": "npm run clean", | ||
"build": "tsc", | ||
"build:clean": "npm run clean && npm run build", | ||
"build:watch": "npm run clean && tsc --watch", | ||
"pack": "webpack --config webpack/webpack.config.js", | ||
"prepublish": "npm run build", | ||
"prepublish": "npm run build:clean", | ||
"betapublish": "npm publish --tag beta" | ||
@@ -45,2 +46,3 @@ }, | ||
"bybit", | ||
"bybit api", | ||
"api", | ||
@@ -51,2 +53,5 @@ "websocket", | ||
"inverse", | ||
"linear", | ||
"usdt", | ||
"trading bots", | ||
"nodejs", | ||
@@ -53,0 +58,0 @@ "node", |
121
README.md
@@ -16,2 +16,3 @@ # bybit-api | ||
- Discuss & collaborate with other node devs? Join our [Node.js Algo Traders](https://t.me/nodetraders) engineering community on telegram. | ||
- `'bybit-api' has no exported member 'RestClient'`: use `InverseClient` instead of `RestClient` | ||
@@ -21,3 +22,3 @@ ## Documentation | ||
- [Bybit API Inverse Documentation](https://bybit-exchange.github.io/docs/inverse/#t-introduction). | ||
- [Bybit API Linear Documentation (not supported yet)](https://bybit-exchange.github.io/docs/linear/#t-introduction) | ||
- [Bybit API Linear Documentation](https://bybit-exchange.github.io/docs/linear/#t-introduction) | ||
@@ -44,6 +45,28 @@ ## Structure | ||
### Inverse Contracts | ||
#### Rest client | ||
Since inverse and linear (USDT) contracts don't use the exact same APIs, the REST abstractions are split into two modules. To use the inverse REST APIs, import the `InverseClient`: | ||
```javascript | ||
const { RestClient } = require('bybit-api'); | ||
const { InverseClient } = require('bybit-api'); | ||
const restInverseOptions = { | ||
// override the max size of the request window (in ms) | ||
recv_window?: number; | ||
// how often to sync time drift with bybit servers | ||
sync_interval_ms?: number | string; | ||
// Default: false. Disable above sync mechanism if true. | ||
disable_time_sync?: boolean; | ||
// Default: false. If true, we'll throw errors if any params are undefined | ||
strict_param_validation?: boolean; | ||
// Optionally override API protocol + domain | ||
// e.g 'https://api.bytick.com' | ||
baseUrl?: string; | ||
// Default: true. whether to try and post-process request exceptions. | ||
parse_exceptions?: boolean; | ||
}; | ||
const API_KEY = 'xxx'; | ||
@@ -53,2 +76,30 @@ const PRIVATE_KEY = 'yyy'; | ||
const client = new InverseClient( | ||
API_KEY, | ||
PRIVATE_KEY, | ||
// optional, uses testnet by default. Set to 'true' to use livenet. | ||
useLivenet, | ||
// restInverseOptions, | ||
// requestLibraryOptions | ||
); | ||
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'}) | ||
.then(result => { | ||
console.log(result); | ||
}) | ||
.catch(err => { | ||
console.error(err); | ||
}); | ||
``` | ||
See inverse [inverse-client.ts](./src/inverse-client.ts) for further information. | ||
### Linear Contracts | ||
To use the Linear (USDT) REST APIs, import the `LinearClient`: | ||
```javascript | ||
const { LinearClient } = require('bybit-api'); | ||
const restInverseOptions = { | ||
@@ -75,3 +126,7 @@ // override the max size of the request window (in ms) | ||
const client = new RestClient( | ||
const API_KEY = 'xxx'; | ||
const PRIVATE_KEY = 'yyy'; | ||
const useLivenet = false; | ||
const client = new LinearClient( | ||
API_KEY, | ||
@@ -87,3 +142,3 @@ PRIVATE_KEY, | ||
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'}) | ||
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSDT'}) | ||
.then(result => { | ||
@@ -97,5 +152,10 @@ console.log(result); | ||
See inverse [rest-client.ts](./src/rest-client.ts) for further information. | ||
### WebSockets | ||
#### Websocket client | ||
Inverse & linear WebSockets can be used via a shared `WebsocketClient`. | ||
Note: to use the linear websockets, pass "linear: true" in the constructor options when instancing the `WebsocketClient`. | ||
To connect to both linear and inverse websockets, make two instances of the WebsocketClient: | ||
```javascript | ||
@@ -111,16 +171,18 @@ const { WebsocketClient } = require('bybit-api'); | ||
// The following parameters are optional: | ||
/* | ||
The following parameters are optional: | ||
*/ | ||
// defaults to false == testnet. set to true for livenet. | ||
// defaults to false == testnet. Set to true for livenet. | ||
// livenet: true | ||
// override which URL to use for websocket connections | ||
// wsUrl: 'wss://stream.bytick.com/realtime' | ||
// defaults to fase == inverse. Set to true for linear (USDT) trading. | ||
// linear: true | ||
// how long to wait (in ms) before deciding the connection should be terminated & reconnected | ||
// pongTimeout: 1000, | ||
// how often to check (in ms) that WS connection is still alive | ||
// pingInterval: 10000, | ||
// how long to wait (in ms) before deciding the connection should be terminated & reconnected | ||
// pongTimeout: 1000, | ||
// how long to wait before attempting to reconnect (in ms) after connection is closed | ||
@@ -132,4 +194,7 @@ // reconnectTimeout: 500, | ||
// config for axios to pass to RestClient. E.g for proxy support | ||
// config for axios used for HTTP requests. E.g for proxy support | ||
// requestOptions: { } | ||
// override which URL to use for websocket connections | ||
// wsUrl: 'wss://stream.bytick.com/realtime' | ||
}; | ||
@@ -139,13 +204,19 @@ | ||
// subscribe to multiple topics at once | ||
ws.subscribe(['position', 'execution', 'trade']); | ||
// and/or subscribe to individual topics on demand | ||
ws.subscribe('kline.BTCUSD.1m'); | ||
ws.on('open', () => { | ||
console.log('connection open'); | ||
// Listen to events coming from websockets. This is the primary data source | ||
ws.on('update', data => { | ||
console.log('update', data); | ||
}); | ||
ws.on('update', message => { | ||
console.log('update', message); | ||
// Optional: Listen to websocket connection open event (automatic after subscribing to one or more topics) | ||
ws.on('open', ({ wsKey, event }) => { | ||
console.log('connection open for websocket with ID: ' + wsKey); | ||
}); | ||
// Optional: Listen to responses to websocket queries (e.g. the response after subscribing to a topic) | ||
ws.on('response', response => { | ||
@@ -155,2 +226,3 @@ console.log('response', response); | ||
// Optional: Listen to connection close event. Unexpected connection closes are automatically reconnected. | ||
ws.on('close', () => { | ||
@@ -160,2 +232,4 @@ console.log('connection closed'); | ||
// Optional: Listen to raw error events. | ||
// Note: responses to invalid topics are currently only sent in the "response" event. | ||
ws.on('error', err => { | ||
@@ -165,3 +239,3 @@ console.error('ERR', err); | ||
``` | ||
See inverse [websocket-client.ts](./src/websocket-client.ts) for further information. | ||
See [websocket-client.ts](./src/websocket-client.ts) for further information. | ||
@@ -172,3 +246,3 @@ ### Customise Logging | ||
```js | ||
const { RestClient, WebsocketClient, DefaultLogger } = require('bybit-api'); | ||
const { WebsocketClient, DefaultLogger } = require('bybit-api'); | ||
@@ -178,3 +252,6 @@ // Disable all logging on the silly level | ||
const ws = new WebsocketClient({key: 'xxx', secret: 'yyy'}, DefaultLogger); | ||
const ws = new WebsocketClient( | ||
{ key: 'xxx', secret: 'yyy' }, | ||
DefaultLogger | ||
); | ||
``` | ||
@@ -181,0 +258,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
108019
31
1874
263
1