Time synchronization with server
- wrapper for /v2/public/time rest endpoint - synchronize time before trying to authenticate a request in order to avoid invalid authentication because prevented replay attacks. Resolves #1
This commit is contained in:
@@ -64,6 +64,14 @@ If you only use the [public endpoints](#public-endpoints) you can ommit key and
|
|||||||
#### async getLatestInformation()
|
#### async getLatestInformation()
|
||||||
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#latest-information-for-symbol)
|
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#latest-information-for-symbol)
|
||||||
|
|
||||||
|
#### async getServerTime()
|
||||||
|
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#server-time)
|
||||||
|
|
||||||
|
#### 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
|
## Example
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const assert = require('assert');
|
|||||||
|
|
||||||
const request = require('request');
|
const request = require('request');
|
||||||
|
|
||||||
const {signMessage} = require('./utility.js');
|
const {signMessage, getServerTimeOffset} = require('./utility.js');
|
||||||
|
|
||||||
const baseUrls = {
|
const baseUrls = {
|
||||||
livenet: 'https://api.bybit.com',
|
livenet: 'https://api.bybit.com',
|
||||||
@@ -14,9 +14,13 @@ module.exports = class Request {
|
|||||||
|
|
||||||
constructor(key, secret, livenet=false) {
|
constructor(key, secret, livenet=false) {
|
||||||
this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet'];
|
this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet'];
|
||||||
|
this._timeOffset = null;
|
||||||
|
|
||||||
if(key) assert(secret, 'Secret is required for private enpoints');
|
if(key) assert(secret, 'Secret is required for private enpoints');
|
||||||
|
|
||||||
|
this._syncTime();
|
||||||
|
setInterval(this._syncTime.bind(this), 3600000);
|
||||||
|
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.secret = secret;
|
this.secret = secret;
|
||||||
}
|
}
|
||||||
@@ -34,12 +38,22 @@ module.exports = class Request {
|
|||||||
return result;
|
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) - start + ((end - start) / 2));
|
||||||
|
}
|
||||||
|
|
||||||
async _call(method, endpoint, params) {
|
async _call(method, endpoint, params) {
|
||||||
const publicEndpoint = endpoint.startsWith('/v2/public');
|
const publicEndpoint = endpoint.startsWith('/v2/public');
|
||||||
|
|
||||||
if(!publicEndpoint) {
|
if(!publicEndpoint) {
|
||||||
if(!this.key || !this.secret) throw new Error('Private endpoints require api and private keys set');
|
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);
|
params = this._signRequest(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +87,7 @@ module.exports = class Request {
|
|||||||
const params = {
|
const params = {
|
||||||
...data,
|
...data,
|
||||||
api_key: this.key,
|
api_key: this.key,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now() + this._timeOffset
|
||||||
};
|
};
|
||||||
|
|
||||||
if(this.key && this.secret) {
|
if(this.key && this.secret) {
|
||||||
@@ -89,4 +103,8 @@ module.exports = class Request {
|
|||||||
.map(key => `${key}=${params[key]}`)
|
.map(key => `${key}=${params[key]}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _syncTime() {
|
||||||
|
this._timeOffset = await this.getTimeOffset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,4 +121,12 @@ module.exports = class RestClient {
|
|||||||
async getLatestInformation() {
|
async getLatestInformation() {
|
||||||
return await this.request.get('/v2/public/tickers');
|
return await this.request.get('/v2/public/tickers');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getServerTime() {
|
||||||
|
return await this.request.get('/v2/public/time');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeOffset() {
|
||||||
|
return await this.request.getTimeOffset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const {EventEmitter} = require('events');
|
|||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
const defaultLogger = require('./logger.js');
|
const defaultLogger = require('./logger.js');
|
||||||
|
const RestClient = require('./rest-client.js');
|
||||||
const {signMessage} = require('./utility.js');
|
const {signMessage} = require('./utility.js');
|
||||||
|
|
||||||
const wsUrls = {
|
const wsUrls = {
|
||||||
@@ -33,6 +34,8 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.client = new RestClient(null, null, this.options.livenet);
|
||||||
|
|
||||||
this._connect();
|
this._connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +57,11 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
this.ws.close();
|
this.ws.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
_connect() {
|
async _connect() {
|
||||||
if(this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING;
|
if(this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING;
|
||||||
|
|
||||||
const url = wsUrls[this.options.livenet ? 'livenet' : 'testnet'] + this._authenticate();
|
const authParams = await this._authenticate();
|
||||||
|
const url = wsUrls[this.options.livenet ? 'livenet' : 'testnet'] + authParams;
|
||||||
|
|
||||||
this.ws = new WebSocket(url);
|
this.ws = new WebSocket(url);
|
||||||
|
|
||||||
@@ -67,12 +71,15 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
this.ws.on('close', this._wsCloseHandler.bind(this));
|
this.ws.on('close', this._wsCloseHandler.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
_authenticate() {
|
async _authenticate() {
|
||||||
if(this.options.key && this.options.secret) {
|
if(this.options.key && this.options.secret) {
|
||||||
this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'});
|
this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'});
|
||||||
|
|
||||||
|
const timeOffset = await this.client.getTimeOffset();
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
api_key: this.options.key,
|
api_key: this.options.key,
|
||||||
expires: (Date.now() + 10000)
|
expires: (Date.now() + timeOffset + 5000)
|
||||||
};
|
};
|
||||||
|
|
||||||
params.signature = signMessage('GET/realtime' + params.expires, this.options.secret);
|
params.signature = signMessage('GET/realtime' + params.expires, this.options.secret);
|
||||||
|
|||||||
Reference in New Issue
Block a user