Merge pull request #45 from tiagosiebler/feat/ts

TypeScript Introduction
This commit is contained in:
Tiago
2020-12-26 17:53:45 +00:00
committed by GitHub
19 changed files with 3365 additions and 830 deletions

View File

@@ -41,6 +41,7 @@ jobs:
registry-url: https://registry.npmjs.org/
#- run: npm ci
- run: npm run build
- run: npm publish
if: steps.version-updated.outputs.has-updated
env:

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ node_modules/
.env
.env.test
.cache
lib
bundleReport.html

View File

@@ -5,16 +5,11 @@
[1]: https://www.npmjs.com/package/bybit-api
A production-ready Node.js connector for the Bybit APIs and WebSockets.
A production-ready Node.js connector for the Bybit APIs and WebSockets, with TypeScript & browser support.
## Installation
`npm install --save bybit-api`
## Usage
Create API credentials at Bybit
- [Livenet](https://bybit.com/app/user/api-management?affiliate_id=9410&language=en-US&group_id=0&group_type=1)
- [Testnet](https://testnet.bybit.com/app/user/api-management)
## Issues & Discussion
- Issues? Check the [issues tab](https://github.com/tiagosiebler/bybit-api/issues).
- Discuss & collaborate with other node devs? Join our [Node.js Algo Traders](https://t.me/nodetraders) engineering community on telegram.
@@ -24,6 +19,25 @@ Most methods accept JS objects. These can be populated using parameters specifie
- [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)
## Structure
This project uses typescript. Resources are stored in 3 key structures:
- [src](./src) - the whole connector written in typescript
- [lib](./lib) - the javascript version of the project (compiled from typescript). This should not be edited directly, as it will be overwritten with each release.
- [dist](./dist) - the packed bundle of the project for use in browser environments.
## Usage
Create API credentials at Bybit
- [Livenet](https://bybit.com/app/user/api-management?affiliate_id=9410&language=en-US&group_id=0&group_type=1)
- [Testnet](https://testnet.bybit.com/app/user/api-management)
### Browser Usage
Build a bundle using webpack:
- `npm install`
- `npm build`
- `npm pack`
The bundle can be found in `dist/`. Altough usage should be largely consistent, smaller differences will exist. Documentation is still TODO.
### Inverse Contracts
#### Rest client
```javascript
@@ -31,8 +45,39 @@ const {RestClient} = require('bybit-api');
const API_KEY = 'xxx';
const PRIVATE_KEY = 'yyy';
const useLivenet = false;
const client = new RestClient(API_KEY, PRIVATE_KEY);
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 client = new RestClient(
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 => {
@@ -43,7 +88,7 @@ client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
});
```
See inverse [rest-client.js](./lib/rest-client.js) for further information.
See inverse [rest-client.ts](./src/rest-client.ts) for further information.
#### Websocket client
```javascript
@@ -105,7 +150,7 @@ ws.on('error', err => {
console.error('ERR', err);
});
```
See inverse [websocket-client.js](./lib/websocket-client.js) & [ws api docs](./doc/websocket-client.md) for further information.
See inverse [websocket-client.ts](./src/websocket-client.ts) for further information.
### Customise Logging
Pass a custom logger which supports the log methods `silly`, `debug`, `notice`, `info`, `warning` and `error`, or override methods from the default logger as desired:

View File

@@ -1,157 +0,0 @@
# Rest API
## Class: RestClient
### new RestClient([key][, secret][, livenet][, options])
- `key` {String} Bybit API Key
- `secret` {String} Bybit private key
- `livenet` {Boolean} If false (default), use testnet.
- `options` {Object} Optional settings for custom behaviour.
- `recv_window` {Number} Optional, default 5000. Increase if recv errors are seen.
- `sync_interval_ms` {Number} Optional, default 3600000. Interval at which syncTime is performed.
If you only use the [public endpoints](#public-endpoints) you can omit key and secret.
### Private enpoints
#### async placeActiveOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-placev2active)
#### async getActiveOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getactive)
#### async cancelActiveOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelv2active)
#### async cancelAllActiveOrders(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelallactive)
#### async replaceActiveOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-replaceactive)
#### async queryActiveOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-queryactive)
#### async placeConditionalOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-placecond)
#### async getConditionalOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getcond)
#### async cancelConditionalOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelcond)
#### async cancelAllConditionalOrders(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelallcond)
#### async replaceConditionalOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-replacecond)
#### async queryConditionalOrder(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-querycond)
#### async getUserLeverage()
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getleverage)
#### async changeUserLeverage(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-changeleverage)
#### async getPosition(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-mypositionv2)
#### async getPositions()
*Deprecated v1 method*
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#positionlistget)
#### async changePositionMargin(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-changemargin)
#### async setTradingStop(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-tradingstop)
#### async getWalletFundRecords(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-walletrecords)
#### async getWithdrawRecords(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-withdrawrecords)
#### async getAssetExchangeRecords(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-assetexchangerecords)
#### async getWalletBalance(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-balance)
#### async setRiskLimit(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-setrisklimit)
#### async getRiskLimitList()
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getrisklimit)
#### async getLastFundingRate(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-fundingrate)
#### async getMyLastFundingFee(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-mylastfundingfee)
#### async getPredictedFunding(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-predictedfunding)
#### async getTradeRecords(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-usertraderecords)
### Public enpoints
#### async getOrderBook(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-orderbook)
#### async getKline(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-querykline)
#### async getOpenInterest(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-marketopeninterest)
#### async getLatestBigDeal(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-marketbigdeal)
#### async getLongShortRatio(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-marketaccountratio)
#### async getLatestInformation()
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-latestsymbolinfo)
#### async getPublicTradingRecords(params)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-publictradingrecords)
#### async getServerTime()
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-servertime)
#### async getApiAnnouncements()
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-announcement)
#### async getSymbols()
Returns symbol information (such as tick size & min notional):
[Meeting price restrictions](https://bybit-exchange.github.io/docs/inverse/#price-price)
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-querysymbol)
#### async getTimeOffset()
Returns the time offset in ms to the server time retrieved by [`async getServerTime`](#async-getservertime).
If positive the time on the server is ahead of the clients time, if negative the time on the server is behind the clients time.
## Example
```javascript
const {RestClient} = require('bybit-api');
const API_KEY = 'xxx';
const PRIVATE_KEY = 'yyy';
const client = new RestClient(API_KEY, PRIVATE_KEY);
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
.then(result => {
console.log(result);
})
.catch(err => {
console.error(err);
});
```

View File

@@ -1,9 +1 @@
const RestClient = require('./lib/rest-client');
const WebsocketClient = require('./lib/websocket-client');
const DefaultLogger = require('./lib/logger');
module.exports = {
RestClient,
WebsocketClient,
DefaultLogger
};
module.exports = require('lib/index');

View File

@@ -1,9 +0,0 @@
module.exports = {
silly: function() {console.log(arguments);},
debug: function() {console.log(arguments);},
notice: function() {console.log(arguments);},
info: function() {console.info(arguments);},
warning: function() {console.warn(arguments);},
error: function() {console.error(arguments);},
};

View File

@@ -1,346 +0,0 @@
const assert = require('assert');
const RequestWrapper = require('./util/requestWrapper');
module.exports = class RestClient {
/**
* @public Creates an instance of the inverse REST API client.
*
* @param {string} key - your API key
* @param {string} secret - your API secret
* @param {boolean} [livenet=false]
* @param {*} [options={}] options to configure REST API connectivity
* @param {*} [requestOptions={}] HTTP networking options for axios
*/
constructor(key, secret, livenet=false, options={}, requestOptions={}) {
this.request = new RequestWrapper(...arguments);
}
async placeActiveOrder(params) {
assert(params, 'No params passed');
assert(params.side, 'Parameter side is required');
assert(params.symbol, 'Parameter symbol is required');
assert(params.order_type, 'Parameter order_type is required');
assert(params.qty, 'Parameter qty is required');
assert(params.time_in_force, 'Parameter time_in_force is required');
if (params.order_type === 'Limit') assert(params.price, 'Parameter price is required for limit orders');
return await this.request.post('v2/private/order/create', params);
}
async getActiveOrder(params) {
return await this.request.get('open-api/order/list', params);
}
async cancelActiveOrder(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
return await this.request.post('v2/private/order/cancel', params);
}
async cancelAllActiveOrders(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('v2/private/order/cancelAll', params);
}
async replaceActiveOrder(params) {
assert(params, 'No params passed');
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('v2/private/order/replace', params);
}
/**
* @deprecated use replaceActiveOrder()
*/
async replaceActiveOrderOld(params) {
assert(params, 'No params passed');
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('open-api/order/replace', params);
}
async queryActiveOrder(params) {
assert(params, 'No params passed');
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/private/order', params);
}
async placeConditionalOrder(params) {
assert(params, 'No params passed');
assert(params.side, 'Parameter side is required');
assert(params.symbol, 'Parameter symbol is required');
assert(params.order_type, 'Parameter order_type is required');
assert(params.qty, 'Parameter qty is required');
assert(params.base_price, 'Parameter base_price is required');
assert(params.stop_px, 'Parameter stop_px is required');
assert(params.time_in_force, 'Parameter time_in_force is required');
if (params.order_type === 'Limit') assert(params.price, 'Parameter price is required for limit orders');
return await this.request.post('v2/private/stop-order/create', params);
}
/**
* @deprecated use placeConditionalOrder
*/
async placeConditionalOrderOld(params) {
assert(params, 'No params passed');
assert(params.side, 'Parameter side is required');
assert(params.symbol, 'Parameter symbol is required');
assert(params.order_type, 'Parameter order_type is required');
assert(params.qty, 'Parameter qty is required');
assert(params.time_in_force, 'Parameter time_in_force is required');
assert(params.base_price, 'Parameter base_price is required');
assert(params.stop_px, 'Parameter stop_px is required');
if (params.order_type === 'Limit') assert(params.price, 'Parameter price is required for limit orders');
return await this.request.post('open-api/stop-order/create', params);
}
async getConditionalOrder(params) {
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/private/stop-order/list', params);
}
/**
* @deprecated use placeConditionalOrder
*/
async getConditionalOrderOld(params) {
return await this.request.get('open-api/stop-order/list', params);
}
async cancelConditionalOrder(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.stop_order_id || params.order_link_id, 'Parameter stop_order_id OR order_link_id is required');
return await this.request.post('v2/private/stop-order/cancel', params);
}
/**
* @deprecated use cancelConditionalOrder
*/
async cancelConditionalOrderOld(params) {
assert(params, 'No params passed');
assert(params.stop_order_id, 'Parameter stop_order_id is required');
return await this.request.post('open-api/stop-order/cancel', params);
}
async cancelAllConditionalOrders(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('v2/private/stop-order/cancelAll', params);
}
async replaceConditionalOrder(params) {
assert(params, 'No params passed');
assert(params.stop_order_id || params.order_link_id, 'Parameter stop_order_id OR order_link_id is required');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('v2/private/stop-order/replace', params);
}
/**
* @deprecated use replaceConditionalOrder
*/
async replaceConditionalOrderOld(params) {
assert(params, 'No params passed');
assert(params.stop_order_id, 'Parameter stop_order_id is required');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('open-api/stop-order/replace', params);
}
async queryConditionalOrder(params) {
assert(params, 'No params passed');
assert(params.stop_order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/private/stop-order', params);
}
/**
* @deprecated use getPosition() instead
*/
async getUserLeverage() {
return await this.request.get('user/leverage');
}
async getPosition(params) {
return await this.request.get('v2/private/position/list', params);
}
async changeUserLeverage(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
if (typeof params.leverage === 'undefined') {
throw new Error('Parameter leverage is required');
}
return await this.request.post('user/leverage/save', params);
}
/**
* @deprecated use getPosition() instead
*/
async getPositions() {
return await this.request.get('position/list');
}
async changePositionMargin(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.margin, 'Parameter margin is required');
return await this.request.post('position/change-position-margin', params);
}
async setTradingStop(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.post('open-api/position/trading-stop', params);
}
async getWalletFundRecords(params) {
return await this.request.get('open-api/wallet/fund/records', params);
}
async getWithdrawRecords(params) {
return await this.request.get('open-api/wallet/withdraw/list', params);
}
async getAssetExchangeRecords(params) {
return await this.request.get('v2/private/exchange-order/list', params);
}
async getWalletBalance(params) {
return await this.request.get('v2/private/wallet/balance', params);
}
async setRiskLimit(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.risk_id, 'Parameter risk_id is required');
return await this.request.post('open-api/wallet/risk-limit', params);
}
async getRiskLimitList() {
return await this.request.get('open-api/wallet/risk-limit/list');
}
async getLastFundingRate(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('open-api/funding/prev-funding-rate', params);
}
async getMyLastFundingFee(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('open-api/funding/prev-funding', params);
}
async getPredictedFunding(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('open-api/funding/predicted-funding', params);
}
async getTradeRecords(params) {
assert(params, 'No params passed');
assert(params.order_id || params.symbol, 'Parameter order_id OR symbol is required');
return await this.request.get('v2/private/execution/list', params);
}
async getOrderBook(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/public/orderBook/L2', params);
}
async getKline(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.interval, 'Parameter interval is required');
assert(params.from, 'Parameter from is required');
return await this.request.get('v2/public/kline/list', params);
}
async getOpenInterest(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.period, 'Parameter period is required');
return await this.request.get('v2/public/open-interest', params);
}
async getLatestBigDeal(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/public/big-deal', params);
}
async getLongShortRatio(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
assert(params.period, 'Parameter period is required');
return await this.request.get('v2/public/account-ratio', params);
}
async getLatestInformation() {
return await this.request.get('v2/public/tickers');
}
async getPublicTradingRecords(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/public/trading-records', params);
}
async getPublicLiquidations(params) {
assert(params, 'No params passed');
assert(params.symbol, 'Parameter symbol is required');
return await this.request.get('v2/public/liq-records', params);
}
async getServerTime() {
return await this.request.get('v2/public/time');
}
async getApiAnnouncements() {
return await this.request.get('v2/public/announcement');
}
async getSymbols() {
return await this.request.get('v2/public/symbols');
}
async getTimeOffset() {
return await this.request.getTimeOffset();
}
};

View File

@@ -1,21 +0,0 @@
const { createHmac } = require('crypto');
module.exports = {
signMessage(message, secret) {
return createHmac('sha256', secret)
.update(message)
.digest('hex');
},
serializeParams(params = {}, strict_validation = false) {
return Object.keys(params)
.sort()
.map(key => {
const value = params[key];
if (strict_validation === true && typeof value === 'undefined') {
throw new Error('Failed to sign API request due to undefined parameter');
}
return `${key}=${value}`;
})
.join('&');
}
};

View File

@@ -1,167 +0,0 @@
const assert = require('assert');
const axios = require('axios');
const { signMessage, serializeParams } = require('./requestUtils');
const baseUrls = {
livenet: 'https://api.bybit.com',
testnet: 'https://api-testnet.bybit.com'
};
module.exports = class RequestUtil {
constructor(key, secret, livenet=false, options={}, requestOptions={}) {
this._timeOffset = null;
this._syncTimePromise = null;
this.options = {
recv_window: 5000,
// how often to sync time drift with bybit servers
sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined
strict_param_validation: false,
...options
};
this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet'];
if (options.baseUrl) {
this.baseUrl = options.baseUrl;
}
this.globalRequestOptions = {
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5,
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...requestOptions,
headers: {
'referer': 'bybitapinode'
},
};
if (key) {
assert(secret, 'Secret is required for private enpoints');
}
this._syncTime();
setInterval(this._syncTime.bind(this), parseInt(this.options.sync_interval_ms));
this.key = key;
this.secret = secret;
}
async get(endpoint, params) {
const result = await this._call('GET', endpoint, params);
return result;
}
async post(endpoint, params) {
const result = await this._call('POST', endpoint, params);
return result;
}
async getTimeOffset() {
const start = Date.now();
const result = await this.get('v2/public/time');
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
}
async _call(method, endpoint, params) {
const publicEndpoint = endpoint.startsWith('v2/public');
if (!publicEndpoint) {
if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set');
}
if (this._timeOffset === null) {
await this._syncTime();
}
params = this._signRequest(params);
}
const options = {
...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join('/'),
method: method,
json: true
};
switch (method) {
case 'GET':
options.params = params;
break;
default:
options.data = params;
break;
}
return axios(options).then(response => {
if (response.status == 200) {
return response.data;
}
throw {
code: response.status,
message: response.statusText,
body: response.data,
requestOptions: options
};
})
.catch(e => {
if (!e.response) {
// Something happened in setting up the request that triggered an Error
if (!e.request) {
throw e.message;
}
// request made but no response received
throw e;
}
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
throw {
code: e.response.statusCode,
message: e.response.message,
body: e.response.body,
requestOptions: options,
headers: e.response.headers
};
});
}
_signRequest(data) {
const params = {
...data,
api_key: this.key,
timestamp: Date.now() + this._timeOffset
};
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) {
params.recv_window = this.options.recv_window;
}
if (this.key && this.secret) {
const serializedParams = serializeParams(params, this.options.strict_param_validation);
params.sign = signMessage(serializedParams, this.secret);
}
return params;
}
_syncTime() {
if (this._syncTimePromise !== null) {
return this._syncTimePromise;
}
this._syncTimePromise = this.getTimeOffset().then(offset => {
this._timeOffset = offset;
this._syncTimePromise = null;
});
return this._syncTimePromise;
}
};

2354
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,43 @@
{
"name": "bybit-api",
"version": "1.2.5",
"version": "1.3.0",
"description": "A production-ready Node.js connector for the Bybit APIs and WebSockets",
"main": "index.js",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib/*",
"index.js"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"clean": "rm -rf lib dist",
"prebuild": "npm run clean",
"build": "tsc",
"pack": "webpack --config webpack/webpack.config.js",
"prepublish": "npm run build",
"betapublish": "npm publish --tag beta"
},
"repository": {
"type": "git",
"url": "https://github.com/tiagosiebler/bybit-api"
"author": "Tiago Siebler (https://github.com/tiagosiebler)",
"contributors": [
"Stefan Aebischer <os@pixtron.ch> (https://pixtron.ch)"
],
"dependencies": {
"axios": "^0.21.0",
"isomorphic-ws": "^4.0.1",
"ws": "^7.4.0"
},
"devDependencies": {
"@types/node": "^14.14.7",
"buffer": "^6.0.2",
"crypto-browserify": "^3.12.0",
"eslint": "^7.10.0",
"source-map-loader": "^1.1.2",
"stream-browserify": "^3.0.0",
"ts-loader": "^8.0.11",
"typescript": "^4.0.5",
"webpack": "^5.4.0",
"webpack-bundle-analyzer": "^4.1.0",
"webpack-cli": "^4.2.0"
},
"keywords": [
"bybit",
@@ -24,20 +53,17 @@
"bitcoin",
"best"
],
"author": "Tiago Siebler (https://github.com/tiagosiebler)",
"contributors": [
"Stefan Aebischer <os@pixtron.ch> (https://pixtron.ch)"
],
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/tiagosiebler"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tiagosiebler/bybit-api"
},
"bugs": {
"url": "https://github.com/tiagosiebler/bybit-api/issues"
},
"homepage": "https://github.com/tiagosiebler/bybit-api#readme",
"dependencies": {
"axios": "^0.21.0",
"ws": "^7.3.1"
},
"devDependencies": {
"eslint": "^7.10.0"
}
"homepage": "https://github.com/tiagosiebler/bybit-api#readme"
}

3
src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './rest-client';
export * from './websocket-client';
export * from './logger';

22
src/logger.ts Normal file
View File

@@ -0,0 +1,22 @@
export type LogParams = null | any;
export const DefaultLogger = {
silly: (...params: LogParams): void => {
console.log(params);
},
debug: (...params: LogParams): void => {
console.log(params);
},
notice: (...params: LogParams): void => {
console.log(params);
},
info: (...params: LogParams): void => {
console.info(params);
},
warning: (...params: LogParams): void => {
console.error(params);
},
error: (...params: LogParams): void => {
console.error(params);
}
};

531
src/rest-client.ts Normal file
View File

@@ -0,0 +1,531 @@
import { AxiosRequestConfig } from 'axios';
import { GenericAPIResponse, getBaseRESTInverseUrl, RestClientInverseOptions } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
export class RestClient {
private requestWrapper: RequestWrapper;
/**
* @public Creates an instance of the inverse REST API client.
*
* @param {string} key - your API key
* @param {string} secret - your API secret
* @param {boolean} [useLivenet=false]
* @param {RestClientInverseOptions} [restInverseOptions={}] options to configure REST API connectivity
* @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios
*/
constructor(
key?: string | undefined,
secret?: string | undefined,
useLivenet?: boolean,
restInverseOptions: RestClientInverseOptions = {},
httpOptions: AxiosRequestConfig = {}
) {
this.requestWrapper = new RequestWrapper(
key,
secret,
getBaseRESTInverseUrl(useLivenet),
restInverseOptions,
httpOptions
);
return this;
}
/**
*
* Market Data Endpoints
*
*/
getOrderBook(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/orderBook/L2', params);
}
getKline(params: {
symbol: string;
interval: string;
from: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/kline/list', params);
}
/**
* @deprecated use getTickers() instead
*/
getLatestInformation(params?: {
symbol?: string;
}): GenericAPIResponse {
return this.getTickers(params);
}
getTickers(params?: {
symbol?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/tickers', params);
}
/**
* @deprecated use getTrades() instead
*/
getPublicTradingRecords(params: {
symbol: string;
from?: number;
limit?: number;
}): GenericAPIResponse {
return this.getTrades(params);
}
getTrades(params: {
symbol: string;
from?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/trading-records', params);
}
getSymbols(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/symbols');
}
/**
* @deprecated use getLiquidations() instead
*/
getPublicLiquidations(params: {
symbol: string;
from?: number;
limit?: number;
start_time?: number;
end_time?: number;
}): GenericAPIResponse {
return this.getLiquidations(params);
}
getLiquidations(params: {
symbol: string;
from?: number;
limit?: number;
start_time?: number;
end_time?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/liq-records', params);
}
getMarkPriceKline(params: {
symbol: string;
interval: string;
from: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/mark-price-kline', params);
}
getOpenInterest(params: {
symbol: string;
period: string;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/open-interest', params);
}
getLatestBigDeal(params: {
symbol: string;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/big-deal', params);
}
getLongShortRatio(params: {
symbol: string;
period: string;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/account-ratio', params);
}
/**
*
* Account Data Endpoints
*
*/
placeActiveOrder(orderRequest: {
side: string;
symbol: string;
order_type: string;
qty: number;
price?: number;
time_in_force: string;
take_profit?: number;
stop_loss?: number;
reduce_only?: boolean;
close_on_trigger?: boolean;
order_link_id?: string;
}): GenericAPIResponse {
// if (orderRequest.order_type === 'Limit' && !orderRequest.price) {
// throw new Error('Price required for limit orders');
// }
return this.requestWrapper.post('v2/private/order/create', orderRequest);
}
getActiveOrderList(params: {
symbol: string;
order_status?: string;
direction?: string;
limit?: number;
cursor?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/order/list', params);
}
/**
* @deprecated use getActiveOrderList() instead
*/
getActiveOrder(params: {
order_id?: string;
order_link_id?: string;
symbol?: string;
order?: string;
page?: number;
limit?: number;
order_status?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('open-api/order/list', params);
}
cancelActiveOrder(params: {
symbol: string;
order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
// if (!params.order_id && !params.order_link_id) {
// throw new Error('Parameter order_id OR order_link_id is required');
// }
return this.requestWrapper.post('v2/private/order/cancel', params);
}
cancelAllActiveOrders(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/order/cancelAll', params);
}
replaceActiveOrder(params: {
order_id?: string;
order_link_id?: string;
symbol: string;
p_r_qty?: string;
p_r_price?: string;
}): GenericAPIResponse {
// if (!params.order_id && !params.order_link_id) {
// throw new Error('Parameter order_id OR order_link_id is required');
// }
return this.requestWrapper.post('v2/private/order/replace', params);
}
/**
* @deprecated use replaceActiveOrder()
*/
replaceActiveOrderOld(params: any): GenericAPIResponse {
// if (!params.order_id && !params.order_link_id) {
// throw new Error('Parameter order_id OR order_link_id is required');
// }
return this.requestWrapper.post('open-api/order/replace', params);
}
queryActiveOrder(params: {
order_id?: string;
order_link_id?: string;
symbol: string;
}): GenericAPIResponse {
// if (!params.order_id && !params.order_link_id) {
// throw new Error('Parameter order_id OR order_link_id is required');
// }
return this.requestWrapper.get('v2/private/order', params);
}
placeConditionalOrder(params: {
side: string;
symbol: string;
order_type: string;
qty: string;
price?: string;
base_price: string;
stop_px: string;
time_in_force: string;
trigger_by?: string;
close_on_trigger?: boolean;
order_link_id?: string;
}): GenericAPIResponse {
// if (params.order_type === 'Limit' && !params.price) {
// throw new Error('Parameter price is required for limit orders');
// }
return this.requestWrapper.post('v2/private/stop-order/create', params);
}
/**
* @deprecated use placeConditionalOrder
*/
placeConditionalOrderOld(params: any): GenericAPIResponse {
// if (params.order_type === 'Limit' && !params.price) {
// throw new Error('Parameter price is required for limit orders');
// }
return this.requestWrapper.post('open-api/stop-order/create', params);
}
getConditionalOrder(params: {
symbol: string;
stop_order_status?: string;
direction?: string;
limit?: number;
cursor?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/stop-order/list', params);
}
/**
* @deprecated use placeConditionalOrder
*/
getConditionalOrderOld(params: any): GenericAPIResponse {
return this.requestWrapper.get('open-api/stop-order/list', params);
}
cancelConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
// if (!params.stop_order_id && !params.order_link_id) {
// throw new Error('Parameter stop_order_id OR order_link_id is required');
// }
return this.requestWrapper.post('v2/private/stop-order/cancel', params);
}
/**
* @deprecated use cancelConditionalOrder
*/
cancelConditionalOrderOld(params: any): GenericAPIResponse {
// if (!params.stop_order_id && !params.order_link_id) {
// throw new Error('Parameter stop_order_id OR order_link_id is required');
// }
return this.requestWrapper.post('open-api/stop-order/cancel', params);
}
cancelAllConditionalOrders(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/stop-order/cancelAll', params);
}
replaceConditionalOrder(params: {
stop_order_id?: string;
order_link_id?: string;
symbol: string;
p_r_qty?: number;
p_r_price?: string;
p_r_trigger_price?: string;
}): GenericAPIResponse {
// if (!params.stop_order_id && !params.order_link_id) {
// throw new Error('Parameter stop_order_id OR order_link_id is required');
// }
return this.requestWrapper.post('v2/private/stop-order/replace', params);
}
/**
* @deprecated use replaceConditionalOrder
*/
replaceConditionalOrderOld(params: any): GenericAPIResponse {
return this.requestWrapper.post('open-api/stop-order/replace', params);
}
queryConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
// if (!params.stop_order_id && !params.order_link_id) {
// throw new Error('Parameter stop_order_id OR order_link_id is required');
// }
return this.requestWrapper.get('v2/private/stop-order', params);
}
/**
* @deprecated use getPosition() instead
*/
getUserLeverage(): GenericAPIResponse {
return this.requestWrapper.get('user/leverage');
}
getPosition(params?: {
symbol?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/position/list', params);
}
/**
* @deprecated use getPosition() instead
*/
getPositions(): GenericAPIResponse {
return this.requestWrapper.get('position/list');
}
changePositionMargin(params: {
symbol: string;
margin: string;
}): GenericAPIResponse {
return this.requestWrapper.post('position/change-position-margin', params);
}
setTradingStop(params: {
symbol: string;
take_profit?: number;
stop_loss?: number;
tp_trigger_by?: string;
sl_trigger_by?: string;
new_trailing_active?: number;
}): GenericAPIResponse {
return this.requestWrapper.post('open-api/position/trading-stop', params);
}
setUserLeverage(params: {
symbol: string;
leverage: number;
}): GenericAPIResponse {
return this.requestWrapper.post('user/leverage/save', params);
}
/**
* @deprecated use setUserLeverage() instead
*/
changeUserLeverage(params: any): GenericAPIResponse {
return this.setUserLeverage(params);
}
getTradeRecords(params: {
order_id?: string;
symbol: string;
start_time?: number;
page?: number;
limit?: number;
order?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/execution/list', params);
}
getClosedPnl(params: {
symbol: string;
start_time?: number;
end_time?: number;
exec_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/trade/closed-pnl/list', params);
}
getRiskLimitList(): GenericAPIResponse {
return this.requestWrapper.get('open-api/wallet/risk-limit/list');
}
setRiskLimit(params: {
symbol: string;
risk_id: string;
}): GenericAPIResponse {
return this.requestWrapper.post('open-api/wallet/risk-limit', params);
}
getLastFundingRate(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('open-api/funding/prev-funding-rate', params);
}
getMyLastFundingFee(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('open-api/funding/prev-funding', params);
}
getPredictedFunding(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('open-api/funding/predicted-funding', params);
}
getApiKeyInfo(): GenericAPIResponse {
return this.requestWrapper.get('open-api/api-key');
}
getLcpInfo(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/lcp', params);
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(params?: {
coin?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/balance', params);
}
getWalletFundRecords(params?: {
start_date?: string;
end_date?: string;
currency?: string;
coin?: string;
wallet_fund_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('open-api/wallet/fund/records', params);
}
getWithdrawRecords(params: {
start_date?: string;
end_date?: string;
coin?: string;
status?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('open-api/wallet/withdraw/list', params);
}
getAssetExchangeRecords(params?: {
limit?: number;
from?: number;
direction?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/time');
}
getApiAnnouncements(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/announcement');
}
async getTimeOffset(): Promise<number> {
const start = Date.now();
return this.getServerTime().then(result => {
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
});
}
};

59
src/util/requestUtils.ts Normal file
View File

@@ -0,0 +1,59 @@
import { createHmac } from 'crypto';
export interface RestClientInverseOptions {
// 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;
}
export type GenericAPIResponse = Promise<any>;
export function signMessage(message: string, secret: string): string {
return createHmac('sha256', secret)
.update(message)
.digest('hex');
};
export function serializeParams(params: object = {}, strict_validation = false): string {
return Object.keys(params)
.sort()
.map(key => {
const value = params[key];
if (strict_validation === true && typeof value === 'undefined') {
throw new Error('Failed to sign API request due to undefined parameter');
}
return `${key}=${value}`;
})
.join('&');
};
export function getBaseRESTInverseUrl(useLivenet?: boolean, restInverseOptions?: RestClientInverseOptions) {
const baseUrlsInverse = {
livenet: 'https://api.bybit.com',
testnet: 'https://api-testnet.bybit.com'
};
if (restInverseOptions?.baseUrl) {
return restInverseOptions.baseUrl;
}
if (useLivenet === true) {
return baseUrlsInverse.livenet;
}
return baseUrlsInverse.testnet;
}

193
src/util/requestWrapper.ts Normal file
View File

@@ -0,0 +1,193 @@
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { signMessage, serializeParams, RestClientInverseOptions, GenericAPIResponse } from './requestUtils';
export default class RequestUtil {
private timeOffset: number | null;
private syncTimePromise: null | Promise<any>;
private options: RestClientInverseOptions;
private baseUrl: string;
private globalRequestOptions: AxiosRequestConfig;
private key: string | undefined;
private secret: string | undefined;
constructor(
key: string | undefined,
secret: string | undefined,
baseUrl: string,
options: RestClientInverseOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
this.timeOffset = null;
this.syncTimePromise = null;
this.options = {
recv_window: 5000,
// how often to sync time drift with bybit servers
sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined
strict_param_validation: false,
...options
};
this.globalRequestOptions = {
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5,
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...requestOptions,
headers: {
'x-referer': 'bybitapinode'
},
};
this.baseUrl = baseUrl;
if (key && !secret) {
throw new Error('API Key & Secret are both required for private enpoints')
}
if (this.options.disable_time_sync !== true) {
this.syncTime();
setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms!);
}
this.key = key;
this.secret = secret;
}
// TODO: type check that endpoint never starts with forward slash??
get(endpoint: string, params?: any): GenericAPIResponse {
return this._call('GET', endpoint, params);
}
post(endpoint: string, params?: any): GenericAPIResponse {
return this._call('POST', endpoint, params);
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
async _call(method: Method, endpoint: string, params?: any): GenericAPIResponse {
const publicEndpoint = endpoint.startsWith('v2/public');
if (!publicEndpoint) {
if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set');
}
if (this.timeOffset === null) {
await this.syncTime();
}
params = this.signRequest(params);
}
const options = {
...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join('/'),
method: method,
json: true
};
if (method === 'GET') {
options.params = params;
} else {
options.data = params;
}
return axios(options).then(response => {
if (response.status == 200) {
return response.data;
}
throw response;
}).catch(this.parseException);
}
/**
* @private generic handler to parse request exceptions
*/
parseException(e: any): unknown {
if (this.options.parse_exceptions === false) {
throw e;
}
// Something happened in setting up the request that triggered an Error
if (!e.response) {
if (!e.request) {
throw e.message;
}
// request made but no response received
throw e;
}
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const response: AxiosResponse = e.response;
throw {
code: response.status,
message: response.statusText,
body: response.data,
headers: response.headers,
requestOptions: this.options
};
}
/**
* @private sign request and set recv window
*/
signRequest(data: any): any {
const params = {
...data,
api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0)
};
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) {
params.recv_window = this.options.recv_window;
}
if (this.key && this.secret) {
const serializedParams = serializeParams(params, this.options.strict_param_validation);
params.sign = signMessage(serializedParams, this.secret);
}
return params;
}
/**
* @private trigger time sync and store promise
*/
syncTime(): GenericAPIResponse {
if (this.options.disable_time_sync === true) {
return Promise.resolve(false);
}
if (this.syncTimePromise !== null) {
return this.syncTimePromise;
}
this.syncTimePromise = this.getTimeOffset().then(offset => {
this.timeOffset = offset;
this.syncTimePromise = null;
});
return this.syncTimePromise;
}
/**
* @deprecated move this somewhere else, because v2/public/time shouldn't be hardcoded here
*
* @returns {Promise<number>}
* @memberof RequestUtil
*/
async getTimeOffset(): Promise<number> {
const start = Date.now();
const result = await this.get('v2/public/time');
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
}
};

View File

@@ -1,9 +1,10 @@
const { EventEmitter } = require('events');
const WebSocket = require('ws');
import { EventEmitter } from 'events';
const defaultLogger = require('./logger');
const RestClient = require('./rest-client');
const { signMessage, serializeParams } = require('./util/requestUtils');
import { RestClient } from './rest-client';
import { DefaultLogger } from './logger';
import { signMessage, serializeParams } from './util/requestUtils';
// import WebSocket from 'ws';
import WebSocket from 'isomorphic-ws';
const wsUrls = {
livenet: 'wss://stream.bybit.com/realtime',
@@ -16,15 +17,39 @@ const READY_STATE_CONNECTED = 2;
const READY_STATE_CLOSING = 3;
const READY_STATE_RECONNECTING = 4;
module.exports = class WebsocketClient extends EventEmitter {
constructor(options, logger) {
export interface WebsocketClientOptions {
key?: string;
secret?: string;
livenet?: boolean;
pongTimeout?: number;
pingInterval?: number;
reconnectTimeout?: number;
restOptions?: any;
requestOptions?: any;
wsUrl?: string;
};
type Logger = typeof DefaultLogger;
export class WebsocketClient extends EventEmitter {
private logger: Logger;
private readyState: number;
private pingInterval?: number | undefined;
private pongTimeout?: number | undefined;
private client: RestClient;
private _subscriptions: Set<unknown>;
private ws: WebSocket;
private options: WebsocketClientOptions;
constructor(options: WebsocketClientOptions, logger?: Logger) {
super();
this.logger = logger || defaultLogger;
this.logger = logger || DefaultLogger;
this.readyState = READY_STATE_INITIAL;
this.pingInterval = null;
this.pongTimeout = null;
this.pingInterval = undefined;
this.pongTimeout = undefined;
this.options = {
livenet: false,
@@ -34,7 +59,7 @@ module.exports = class WebsocketClient extends EventEmitter {
...options
};
this.client = new RestClient(null, null, this.options.livenet, this.options.restOptions, this.options.requestOptions);
this.client = new RestClient(undefined, undefined, this.options.livenet, this.options.restOptions, this.options.requestOptions);
this._subscriptions = new Set();
this._connect();
@@ -78,14 +103,17 @@ module.exports = class WebsocketClient extends EventEmitter {
const authParams = await this._authenticate();
const url = this._getWsUrl() + authParams;
this.ws = new WebSocket(url);
const ws = new WebSocket(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;
this.ws.on('open', this._wsOpenHandler.bind(this));
this.ws.on('message', this._wsMessageHandler.bind(this));
this.ws.on('error', this._wsOnErrorHandler.bind(this));
this.ws.on('close', this._wsCloseHandler.bind(this));
} catch (err) {
this.logger.error('Connection failed', err);
this.logger.error('Connection failed: ', err);
this._reconnect(this.options.reconnectTimeout);
}
}
@@ -96,7 +124,7 @@ module.exports = class WebsocketClient extends EventEmitter {
const timeOffset = await this.client.getTimeOffset();
const params = {
const params: any = {
api_key: this.options.key,
expires: (Date.now() + timeOffset + 5000)
};
@@ -127,8 +155,8 @@ module.exports = class WebsocketClient extends EventEmitter {
}
_ping() {
clearTimeout(this.pongTimeout);
this.pongTimeout = null;
clearTimeout(this.pongTimeout!);
delete this.pongTimeout;
this.logger.silly('Sending ping', { category: 'bybit-ws' });
this.ws.send(JSON.stringify({op: 'ping'}));
@@ -136,7 +164,9 @@ module.exports = class WebsocketClient extends EventEmitter {
this.pongTimeout = setTimeout(() => {
this.logger.info('Pong timeout', { category: 'bybit-ws' });
this._teardown();
this.ws.terminate();
// this.ws.terminate();
// TODO: does this work?
this.ws.close();
}, this.options.pongTimeout);
}
@@ -144,8 +174,8 @@ module.exports = class WebsocketClient extends EventEmitter {
if (this.pingInterval) clearInterval(this.pingInterval);
if (this.pongTimeout) clearTimeout(this.pongTimeout);
this.pongTimeout = null;
this.pingInterval = null;
this.pongTimeout = undefined;
this.pingInterval = undefined;
}
_wsOpenHandler() {
@@ -164,7 +194,7 @@ module.exports = class WebsocketClient extends EventEmitter {
}
_wsMessageHandler(message) {
const msg = JSON.parse(message);
const msg = JSON.parse(message && message.data || message);
if ('success' in msg) {
this._handleResponse(msg);

30
tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compileOnSave": true,
"compilerOptions": {
"allowJs": true,
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"removeComments": false,
"noEmitOnError": true,
"noImplicitAny": false,
"strictNullChecks": true,
"skipLibCheck": true,
"esModuleInterop": true,
"lib": ["es2017","dom"],
"baseUrl": ".",
"outDir": "lib",
"paths": {
"@src/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"**/node_modules/*",
"coverage",
"doc"
]
}

69
webpack/webpack.config.js Normal file
View File

@@ -0,0 +1,69 @@
const webpack = require('webpack');
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
function generateConfig(name) {
var config = {
entry: './lib/index.js',
output: {
path: path.resolve(__dirname, '../dist'),
filename: name + '.js',
sourceMapFilename: name + '.map',
library: 'bybitapi',
libraryTarget: 'umd'
},
devtool: "source-map",
mode: 'production',
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
fallback: {
"crypto": require.resolve("crypto-browserify"),
"buffer": require.resolve("buffer/"),
"stream": require.resolve("stream-browserify")
}
},
module: {
rules: [
// All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
{ test: /\.tsx?$/, loader: "ts-loader" },
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ test: /\.js$/, loader: "source-map-loader" },
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components|samples|lib|test|coverage)/,
use: {
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env', {
'targets': {
'node': 'current'
}
}]]
}
}
}
]
}
};
config.plugins = [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}),
new BundleAnalyzerPlugin({
defaultSizes: 'stat',
analyzerMode: 'static',
reportFilename: '../doc/bundleReport.html',
openAnalyzer: false,
})
];
return config;
}
module.exports = generateConfig('bybitapi');