Comparing version 0.0.4 to 0.0.5
@@ -91,3 +91,3 @@ import { AxiosRequestConfig } from 'axios'; | ||
getPositions(showAveragePrice?: boolean): GenericAPIResponse; | ||
setAccountLeverage(leverage: any): GenericAPIResponse; | ||
setAccountLeverage(leverage: number): GenericAPIResponse; | ||
/** | ||
@@ -129,5 +129,3 @@ * | ||
}): GenericAPIResponse; | ||
getSavedAddresses(params?: { | ||
coin?: string; | ||
}): GenericAPIResponse; | ||
getSavedAddresses(coin?: string): GenericAPIResponse; | ||
createSavedAddress(params: { | ||
@@ -158,5 +156,3 @@ coin: string; | ||
}): GenericAPIResponse; | ||
getTriggerOrderTriggers(params: { | ||
conditionalOrderId: string; | ||
}): GenericAPIResponse; | ||
getTriggerOrderTriggers(conditionalOrderId: string): GenericAPIResponse; | ||
getTriggerOrderHistory(params?: { | ||
@@ -174,3 +170,3 @@ market?: string; | ||
side: OrderSide; | ||
price: number; | ||
price: number | null; | ||
type: OrderType; | ||
@@ -177,0 +173,0 @@ size: number; |
@@ -151,4 +151,4 @@ "use strict"; | ||
} | ||
getSavedAddresses(params) { | ||
return this.requestWrapper.get('wallet/saved_addresses', params); | ||
getSavedAddresses(coin) { | ||
return this.requestWrapper.get('wallet/saved_addresses', { coin }); | ||
} | ||
@@ -177,4 +177,4 @@ createSavedAddress(params) { | ||
} | ||
getTriggerOrderTriggers(params) { | ||
return this.requestWrapper.get(`conditional_orders/${params.conditionalOrderId}/triggers`); | ||
getTriggerOrderTriggers(conditionalOrderId) { | ||
return this.requestWrapper.get(`conditional_orders/${conditionalOrderId}/triggers`); | ||
} | ||
@@ -181,0 +181,0 @@ getTriggerOrderHistory(params) { |
import { GenericAPIResponse } from './util/requestUtils'; | ||
import RequestWrapper from './util/requestWrapper'; | ||
export default class SharedEndpoints { | ||
export default abstract class SharedEndpoints { | ||
protected requestWrapper: RequestWrapper; | ||
@@ -5,0 +5,0 @@ /** |
@@ -10,7 +10,18 @@ export interface RestClientOptions { | ||
} | ||
export interface WSClientConfigurableOptions { | ||
key?: string; | ||
secret?: string; | ||
subAccountName?: string; | ||
pongTimeout?: number; | ||
pingInterval?: number; | ||
reconnectTimeout?: number; | ||
restOptions?: any; | ||
requestOptions?: any; | ||
wsUrl?: string; | ||
} | ||
export declare type GenericAPIResponse = Promise<any>; | ||
export declare function signMessage(message: string, secret: string): string; | ||
export declare function signWsAuthenticate(timestamp: number, secret: string): string; | ||
export declare function serializeParams(params?: object, strict_validation?: boolean): string; | ||
export declare function getRestBaseUrl(restClientOptions: 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.isWsPong = exports.isPublicEndpoint = exports.getRestBaseUrl = exports.serializeParams = exports.signMessage = void 0; | ||
exports.isPublicEndpoint = exports.getRestBaseUrl = exports.serializeParams = exports.signWsAuthenticate = exports.signMessage = void 0; | ||
const crypto_1 = require("crypto"); | ||
; | ||
function signMessage(message, secret) { | ||
@@ -12,2 +13,9 @@ return crypto_1.createHmac('sha256', secret) | ||
; | ||
function signWsAuthenticate(timestamp, secret) { | ||
return crypto_1.createHmac('sha256', secret) | ||
.update(timestamp + 'websocket_login') | ||
.digest('hex'); | ||
} | ||
exports.signWsAuthenticate = signWsAuthenticate; | ||
; | ||
function serializeParams(params = {}, strict_validation = false) { | ||
@@ -34,2 +42,3 @@ return Object.keys(params) | ||
exports.getRestBaseUrl = getRestBaseUrl; | ||
; | ||
function isPublicEndpoint(endpoint) { | ||
@@ -48,9 +57,3 @@ if (endpoint.startsWith('https')) { | ||
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 |
@@ -29,3 +29,5 @@ "use strict"; | ||
// in ms == 5 minutes by default | ||
timeout: 1000 * 60 * 5 }, requestOptions), { headers: { | ||
timeout: 1000 * 60 * 5 }, requestOptions), { | ||
// FTX requirements | ||
headers: { | ||
'FTX-KEY': key, | ||
@@ -32,0 +34,0 @@ } }); |
/// <reference types="node" /> | ||
import { WsConnectionState } from '../websocket-client'; | ||
import { WsConnectionState, WsTopic } from '../websocket-client'; | ||
import { DefaultLogger } from '../logger'; | ||
import WebSocket from 'isomorphic-ws'; | ||
declare type WsTopicList = Set<string>; | ||
declare type WsTopicList = Set<WsTopic>; | ||
declare type KeyedWsTopicLists = { | ||
@@ -33,5 +33,5 @@ [key: string]: WsTopicList; | ||
getTopicsByKey(): KeyedWsTopicLists; | ||
addTopic(key: string, topic: string): WsTopicList; | ||
deleteTopic(key: string, topic: string): boolean; | ||
addTopic(key: string, topic: WsTopic | string): any; | ||
deleteTopic(key: string, topic: WsTopic | string): any; | ||
} | ||
export {}; |
@@ -82,5 +82,11 @@ "use strict"; | ||
addTopic(key, topic) { | ||
if (typeof topic === 'string') { | ||
return this.addTopic(key, { channel: topic }); | ||
} | ||
return this.getTopics(key).add(topic); | ||
} | ||
deleteTopic(key, topic) { | ||
if (typeof topic === 'string') { | ||
return this.addTopic(key, { channel: topic }); | ||
} | ||
return this.getTopics(key).delete(topic); | ||
@@ -87,0 +93,0 @@ } |
/// <reference types="node" /> | ||
import { EventEmitter } from 'events'; | ||
import { DefaultLogger } from './logger'; | ||
import { WSClientConfigurableOptions } from './util/requestUtils'; | ||
import WebSocket from 'isomorphic-ws'; | ||
@@ -12,17 +13,3 @@ export declare enum WsConnectionState { | ||
} | ||
export interface WSClientConfigurableOptions { | ||
key?: string; | ||
secret?: string; | ||
livenet?: boolean; | ||
linear?: boolean; | ||
pongTimeout?: number; | ||
pingInterval?: number; | ||
reconnectTimeout?: number; | ||
restOptions?: any; | ||
requestOptions?: any; | ||
wsUrl?: string; | ||
} | ||
export interface WebsocketClientOptions extends WSClientConfigurableOptions { | ||
livenet: boolean; | ||
linear: boolean; | ||
pongTimeout: number; | ||
@@ -32,5 +19,4 @@ pingInterval: number; | ||
} | ||
export declare const wsKeyInverse = "inverse"; | ||
export declare const wsKeyLinearPrivate = "linearPrivate"; | ||
export declare const wsKeyLinearPublic = "linearPublic"; | ||
export declare const wsKeyGeneral = "ftx"; | ||
export declare const wsBaseUrl = "wss://ftx.com/ws/"; | ||
export declare interface WebsocketClient { | ||
@@ -44,2 +30,7 @@ on(event: 'open' | 'reconnected', listener: ({ wsKey: string, event: any }: { | ||
} | ||
export interface WsTopic { | ||
channel: string; | ||
grouping?: number; | ||
market?: string; | ||
} | ||
export declare class WebsocketClient extends EventEmitter { | ||
@@ -52,12 +43,10 @@ private logger; | ||
isLivenet(): boolean; | ||
isLinear(): boolean; | ||
isInverse(): boolean; | ||
/** | ||
* Add topic/topics to WS subscription list | ||
*/ | ||
subscribe(wsTopics: string[] | string): void; | ||
subscribe(wsTopics: WsTopic[] | WsTopic | string[] | string): void; | ||
/** | ||
* Remove topic/topics from WS subscription list | ||
*/ | ||
unsubscribe(wsTopics: string[] | string): void; | ||
unsubscribe(wsTopics: WsTopic[] | WsTopic | string[] | string): void; | ||
close(wsKey: string): void; | ||
@@ -69,2 +58,3 @@ /** | ||
private connect; | ||
private requestTryAuthenticate; | ||
private parseWsError; | ||
@@ -71,0 +61,0 @@ /** |
@@ -15,3 +15,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.WebsocketClient = exports.wsKeyLinearPublic = exports.wsKeyLinearPrivate = exports.wsKeyInverse = exports.WsConnectionState = void 0; | ||
exports.WebsocketClient = exports.wsBaseUrl = exports.wsKeyGeneral = exports.WsConnectionState = void 0; | ||
const events_1 = require("events"); | ||
@@ -23,19 +23,4 @@ const rest_client_1 = require("./rest-client"); | ||
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 wsMessages_1 = require("./util/wsMessages"); | ||
const loggerCategory = { category: 'ftx-ws' }; | ||
const READY_STATE_INITIAL = 0; | ||
@@ -56,13 +41,6 @@ const READY_STATE_CONNECTING = 1; | ||
; | ||
exports.wsKeyGeneral = 'ftx'; | ||
exports.wsBaseUrl = 'wss://ftx.com/ws/'; | ||
; | ||
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 { | ||
@@ -73,14 +51,8 @@ constructor(options, logger) { | ||
this.wsStore = new WsStore_1.default(this.logger); | ||
this.options = Object.assign({ livenet: false, linear: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500 }, options); | ||
this.options = Object.assign({ pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500 }, options); | ||
this.restClient = new rest_client_1.RestClient(undefined, undefined, this.options.restOptions, this.options.requestOptions); | ||
} | ||
isLivenet() { | ||
return this.options.livenet === true; | ||
return true; | ||
} | ||
isLinear() { | ||
return this.options.linear === true; | ||
} | ||
isInverse() { | ||
return !this.isLinear(); | ||
} | ||
/** | ||
@@ -129,8 +101,3 @@ * Add topic/topics to WS subscription list | ||
connectAll() { | ||
if (this.isInverse()) { | ||
return [this.connect(exports.wsKeyInverse)]; | ||
} | ||
if (this.isLinear()) { | ||
return [this.connect(exports.wsKeyLinearPublic), this.connect(exports.wsKeyLinearPrivate)]; | ||
} | ||
return [this.connect(exports.wsKeyGeneral)]; | ||
} | ||
@@ -152,4 +119,3 @@ connect(wsKey) { | ||
} | ||
const authParams = yield this.getAuthParams(wsKey); | ||
const url = this.getWsUrl(wsKey) + authParams; | ||
const url = this.getWsUrl(wsKey); | ||
const ws = this.connectToWsUrl(url, wsKey); | ||
@@ -164,2 +130,12 @@ return this.wsStore.setWs(wsKey, ws); | ||
} | ||
requestTryAuthenticate(wsKey) { | ||
const { key, secret } = this.options; | ||
if (!key || !secret) { | ||
this.logger.debug(`Connection "${wsKey}" will remain unauthenticated due to missing key/secret`); | ||
return; | ||
} | ||
const timestamp = new Date().getTime(); | ||
const authMsg = wsMessages_1.getWsAuthMessage(key, requestUtils_1.signWsAuthenticate(timestamp, secret), timestamp); | ||
this.tryWsSend(wsKey, JSON.stringify(authMsg)); | ||
} | ||
parseWsError(context, error, wsKey) { | ||
@@ -185,3 +161,3 @@ if (!error.message) { | ||
const { key, secret } = this.options; | ||
if (key && secret && wsKey !== exports.wsKeyLinearPublic) { | ||
if (key && secret) { | ||
this.logger.debug('Getting auth\'d request params', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
@@ -194,3 +170,3 @@ const timeOffset = yield this.restClient.getTimeOffset(); | ||
params.signature = requestUtils_1.signMessage('GET/realtime' + params.expires, secret); | ||
return '?' + requestUtils_1.serializeParams(params); | ||
return params; | ||
} | ||
@@ -250,7 +226,7 @@ else if (!key || !secret) { | ||
requestSubscribeTopics(wsKey, topics) { | ||
const wsMessage = JSON.stringify({ | ||
op: 'subscribe', | ||
args: topics | ||
const market = ''; | ||
topics.forEach(topic => { | ||
const wsMessage = JSON.stringify(Object.assign({ op: 'subscribe' }, topic)); | ||
this.tryWsSend(wsKey, wsMessage); | ||
}); | ||
this.tryWsSend(wsKey, wsMessage); | ||
} | ||
@@ -261,7 +237,6 @@ /** | ||
requestUnsubscribeTopics(wsKey, topics) { | ||
const wsMessage = JSON.stringify({ | ||
op: 'unsubscribe', | ||
args: topics | ||
topics.forEach(topic => { | ||
const wsMessage = JSON.stringify(Object.assign({ op: 'unsubscribe' }, topic)); | ||
this.tryWsSend(wsKey, wsMessage); | ||
}); | ||
this.tryWsSend(wsKey, wsMessage); | ||
} | ||
@@ -292,3 +267,3 @@ tryWsSend(wsKey, wsMessage) { | ||
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.logger.info('Websocket connected', Object.assign(Object.assign({}, loggerCategory), { wsKey, livenet: this.isLivenet() })); | ||
this.emit('open', { wsKey, event }); | ||
@@ -301,2 +276,3 @@ } | ||
this.setWsState(wsKey, READY_STATE_CONNECTED); | ||
this.requestTryAuthenticate(wsKey); | ||
this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]); | ||
@@ -307,10 +283,8 @@ this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval); | ||
const msg = JSON.parse(event && event.data || event); | ||
if ('success' in msg) { | ||
this.onWsMessageResponse(msg, wsKey); | ||
} | ||
else if (msg.topic) { | ||
if (msg.channel) { | ||
this.onWsMessageUpdate(msg); | ||
} | ||
else { | ||
this.logger.warning('Got unhandled ws message', Object.assign(Object.assign({}, loggerCategory), { message: msg, event, wsKey })); | ||
this.logger.debug('Websocket event: ', event.data || event); | ||
this.onWsMessageResponse(msg, wsKey); | ||
} | ||
@@ -336,3 +310,3 @@ } | ||
onWsMessageResponse(response, wsKey) { | ||
if (requestUtils_1.isWsPong(response)) { | ||
if (wsMessages_1.isWsPong(response)) { | ||
this.logger.silly('Received pong', Object.assign(Object.assign({}, loggerCategory), { wsKey })); | ||
@@ -358,6 +332,6 @@ this.clearPongTimer(wsKey); | ||
} | ||
return 'wss://ftx.com/ws/'; | ||
return exports.wsBaseUrl; | ||
} | ||
getWsKeyForTopic(topic) { | ||
return this.isInverse() ? exports.wsKeyInverse : getLinearWsKeyForTopic(topic); | ||
return exports.wsKeyGeneral; | ||
} | ||
@@ -364,0 +338,0 @@ } |
{ | ||
"name": "ftx-api", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"description": "Node.js connector for FTX's REST APIs and WebSockets", | ||
@@ -22,4 +22,3 @@ "main": "lib/index.js", | ||
"author": "Tiago Siebler (https://github.com/tiagosiebler)", | ||
"contributors": [ | ||
], | ||
"contributors": [], | ||
"dependencies": { | ||
@@ -26,0 +25,0 @@ "axios": "^0.21.0", |
@@ -8,4 +8,2 @@ # ftx-api | ||
Warning: this connector is still in early beta. REST APIs should be fully functional but websockets still need work. | ||
Node.js connector for the FTX APIs and WebSockets, with TypeScript & browser support. | ||
@@ -30,2 +28,3 @@ | ||
- [dist](./dist) - the packed bundle of the project for use in browser environments. | ||
- [examples](./examples) - demonstrations on various workflows using this library | ||
@@ -40,3 +39,5 @@ --- | ||
Import and instance the `RestClient` to access all REST API methods. All methods return promises. | ||
Import and instance the `RestClient` to access all REST API methods. | ||
- All methods return promises. | ||
- Supports subaccounts. | ||
@@ -106,4 +107,9 @@ ### Example | ||
## WebSockets | ||
<details><summary>Inverse & linear WebSockets can be used via a shared `WebsocketClient`. Click here to expand and see full sample:</summary> | ||
- Automatically connect to FTX websockets | ||
- Automatically authenticate, if key & secret are provided. | ||
- Automatically checks connection integrity. If connection stale (no response to pings), automatically reconnects, re-authenticates and resubscribes to previous topics. | ||
- Supports subaccounts. | ||
<details><summary>WebSocket channels can be subscribed to via the `WebsocketClient`. Click here to expand and see full sample:</summary> | ||
```javascript | ||
@@ -123,2 +129,5 @@ const { WebsocketClient } = require('ftx-api'); | ||
// Subaccount nickname | ||
// subAccountName: 'sub1', | ||
// how long to wait (in ms) before deciding the connection should be terminated & reconnected | ||
@@ -146,7 +155,26 @@ // pongTimeout: 1000, | ||
// subscribe to multiple topics at once | ||
ws.subscribe(['ticker', 'markets']); | ||
ws.subscribe(['fills', 'orders']); | ||
// and/or subscribe to individual topics on demand | ||
ws.subscribe('trades'); | ||
ws.subscribe('fills'); | ||
// and/or subscribe to complex topics on demand, one at a time | ||
ws.subscribe({ | ||
channel: 'trades', | ||
market: 'BTC-PERP' | ||
}); | ||
// or as a list of complex topics | ||
ws.subscribe([ | ||
{ | ||
channel: 'trades', | ||
market: 'BTC-PERP' | ||
}, | ||
{ | ||
channel: 'orderbookGrouped', | ||
market: 'BTC-PERP', | ||
grouping: 500 | ||
} | ||
]); | ||
// Listen to events coming from websockets. This is the primary data source | ||
@@ -158,4 +186,4 @@ ws.on('update', data => { | ||
// 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); | ||
ws.on('open', ({ event }) => { | ||
console.log('connection opened'); | ||
}); | ||
@@ -162,0 +190,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
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
110127
31
1834
250