okx-v5-ws
Version:
This is a non-official OKX V5 websocket SDK for nodejs.
506 lines • 18.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OkxV5Ws = void 0;
const hmac_sha256_1 = __importDefault(require("crypto-js/hmac-sha256"));
const enc_base64_1 = __importDefault(require("crypto-js/enc-base64"));
const WSConnector_1 = require("./WSConnector");
const ErrorCodes_1 = require("./ErrorCodes");
const uuid_1 = require("uuid");
const util_1 = require("./util");
const events_1 = __importDefault(require("events"));
/**
* Provide service level function to user
*/
class OkxV5Ws {
#serverBaseUrl;
#profileConfig;
#options;
// ws connection handling instance
#wsConnector;
// events connector
#eventEmitter = new events_1.default();
/**
* Queues for piping operation one by one
*/
#operationQueue = [];
/**
* Queues for handling response for event types
*/
#loginReqs = [];
#subChannelReqs = [];
#tradeReqsMap = new Map();
#channelTopicMessageHandlersMap = new Map();
/**
* map of Trade OP codes
*/
static #tradeOps = {
order: true,
'batch-orders': true,
'cancel-order': true,
'batch-cancel-orders': true,
'amend-order': true,
'batch-amend-orders': true,
};
static PUBLIC_ENDPOINT = 'wss://wsaws.okx.com:8443/ws/v5/public';
static PRIVATE_ENDPOINT = 'wss://wsaws.okx.com:8443/ws/v5/private';
static DEMO_PUBLIC_ENDPOINT = 'wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999';
static DEMO_PRIVATE_ENDPOINT = 'wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999';
/**
* constructor
*/
constructor({ serverBaseUrl, profileConfig, options, messageHandler, }) {
this.#serverBaseUrl = serverBaseUrl;
this.#profileConfig = profileConfig;
this.#options = {
autoLogin: options?.autoLogin ?? true,
logLoginMessage: options?.logLoginMessage ?? true,
logSubscriptionMessage: options?.logSubscriptionMessage ?? true,
logChannelTopicMessage: options?.logChannelTopicMessage ?? false,
logTradeMessage: options?.logTradeMessage ?? true,
};
/**
* initialize WSConnector
*/
this.#wsConnector = new WSConnector_1.WSConnector({
serverBaseUrl: this.#serverBaseUrl,
afterConnected: this.#afterConnected,
});
/**
* connecting the events
*/
this.#wsConnector.event.on('message', this.#onMessage);
this.#wsConnector.event.on('connect', (code, desc) => {
this.#eventEmitter.emit('connect', code, desc);
});
this.#wsConnector.event.on('reconnecting', () => {
this.#eventEmitter.emit('reconnecting');
});
this.#wsConnector.event.on('close', (code, desc) => {
this.#eventEmitter.emit('close', code, desc);
});
this.#wsConnector.event.on('closed', (code, desc) => {
this.#eventEmitter.emit('closed', code, desc);
});
this.#wsConnector.event.on('error', (error) => this.#eventEmitter.emit('error', error));
if (messageHandler) {
console.warn('messageHandler is deprecated. Please use okxV5Ws.event on message event');
this.#eventEmitter.on('message', messageHandler);
}
}
/**
* Get the event emitter
*/
get event() {
return this.#eventEmitter;
}
/**
* start connecting to server
*/
async connect() {
await this.#wsConnector.connect();
}
/**
* sending message payload to server
*
* @param payload
*/
async send(payload) {
this.#checkConnection();
await this.#wsConnector.send(JSON.stringify(payload));
}
/**
* subscribe channel topic
*
* @param subscriptionTopic
* @returns
*/
async subscribeChannel(subscriptionTopic) {
return this.#waitOperationQueue(this.#operationQueue, 'subscribe', async () => {
const topic = (0, util_1.normalizeSubscriptionTopic)(subscriptionTopic);
console.log(`subscribe channel ${JSON.stringify(topic)}`);
this.#checkConnection();
return new Promise((resolve, reject) => {
this.send({
op: 'subscribe',
args: [topic],
});
this.#subChannelReqs.push({ resolve, reject });
});
}).catch((e) => {
console.error(e);
throw e;
});
}
/**
* unsubscribe channel topic
*
* @param subscriptionTopic
* @returns
*/
async unsubscribeChannel(subscriptionTopic) {
return this.#waitOperationQueue(this.#operationQueue, 'unsubscribe', async () => {
const topic = (0, util_1.normalizeSubscriptionTopic)(subscriptionTopic);
console.log(`unsubscribe channel ${JSON.stringify(topic)}`);
this.#checkConnection();
return new Promise((resolve, reject) => {
this.send({
op: 'unsubscribe',
args: [topic],
});
this.#subChannelReqs.push({ resolve, reject });
});
}).catch((e) => {
console.error(e);
throw e;
});
}
/**
* Add channel topic message handler
*
* @param subscriptionTopic
* @param channelMessageHandler
*/
addChannelMessageHandler(subscriptionTopic, channelMessageHandler) {
const topic = (0, util_1.normalizeSubscriptionTopic)(subscriptionTopic);
console.log(`add ChannelMessageHandler for ${JSON.stringify(topic)}`);
const key = JSON.stringify(topic);
let messageHandlers = this.#channelTopicMessageHandlersMap.get(key);
if (!messageHandlers) {
messageHandlers = [channelMessageHandler];
this.#channelTopicMessageHandlersMap.set(key, messageHandlers);
}
else {
messageHandlers.push(channelMessageHandler);
}
}
/**
* Remove channel topic message handler
*
* @param subscriptionTopic
* @param channelMessageHandler
*/
removeChannelMessageHandler(subscriptionTopic, channelMessageHandler) {
const topic = (0, util_1.normalizeSubscriptionTopic)(subscriptionTopic);
console.log(`remove ChannelMessageHandler for ${JSON.stringify(topic)}`);
const key = JSON.stringify(topic);
let messageHandlers = this.#channelTopicMessageHandlersMap.get(key);
if (messageHandlers) {
messageHandlers = messageHandlers.filter((handler) => handler !== channelMessageHandler);
if (messageHandlers.length > 0) {
this.#channelTopicMessageHandlersMap.set(key, messageHandlers);
}
else {
this.#channelTopicMessageHandlersMap.delete(key);
}
}
}
/**
* Remove that channel topic's ALL message handler
*
* @param subscriptionTopic
*/
removeAllChannelMessageHandler(subscriptionTopic) {
const topic = (0, util_1.normalizeSubscriptionTopic)(subscriptionTopic);
console.log(`removeAll ChannelMessageHandler for ${JSON.stringify(topic)}`);
const key = JSON.stringify(topic);
this.#channelTopicMessageHandlersMap.delete(key);
}
/**
* Send trade op message
*
* @param payload
* @returns
*/
async trade(payload) {
const op = payload.op;
if (!OkxV5Ws.#tradeOps[op]) {
throw new Error('Unknown OP');
}
// add id if omitted
if (payload.id === undefined) {
payload = { id: (0, uuid_1.v4)().replaceAll('-', ''), ...payload };
}
// append req queue and wait for reply
let opReqsMap = this.#tradeReqsMap.get(op);
if (opReqsMap === undefined) {
opReqsMap = new Map();
this.#tradeReqsMap.set(op, opReqsMap);
}
let opReqs = opReqsMap.get(payload.id);
if (opReqs === undefined) {
opReqs = new Array();
}
opReqsMap.set(payload.id, opReqs);
return new Promise((resolve, reject) => {
this.send(payload);
opReqs.push({ resolve, reject });
}).catch((e) => {
console.error(e);
throw e;
});
}
/**
* Waiting the operation in a single processing queue
* @param operationQueue
* @param op
* @param process
* @returns
*/
async #waitOperationQueue(operationQueue, op, process) {
return new Promise((resolve, reject) => {
const isEmptyQueue = operationQueue.length === 0;
operationQueue.push({
op,
execute: async () => {
return process()
.then((data) => {
resolve(data);
})
.catch((e) => {
reject(e);
})
.finally(() => {
return this.#pullNextOperation(operationQueue);
});
},
});
if (isEmptyQueue) {
operationQueue[0].execute();
}
});
}
/**
* Remove the first item in queue. And pull next operation to run.
* @param operationQueue
* @returns
*/
async #pullNextOperation(operationQueue) {
if (operationQueue.length === 0) {
return;
}
operationQueue.shift(); // remove the first one as it is finished
if (operationQueue.length > 0) {
await operationQueue[0].execute(); // trigger next
return;
}
return;
}
/**
* check connection connected
*/
#checkConnection() {
if (!this.#wsConnector.connected) {
throw new Error('Connection not available');
}
}
/**
* Do login authentication handshake
*/
async #authentication() {
return this.#waitOperationQueue(this.#operationQueue, 'login', async () => {
this.#checkConnection();
const timestamp = ('' + Date.now()).slice(0, -3);
const payload = `${timestamp}GET/users/self/verify`;
const sign = enc_base64_1.default.stringify((0, hmac_sha256_1.default)(payload, this.#profileConfig?.secretKey ?? ''));
return new Promise((resolve, reject) => {
this.send({
op: 'login',
args: [
{
apiKey: this.#profileConfig?.apiKey ?? '',
passphrase: this.#profileConfig?.passPhrase ?? '',
timestamp: timestamp,
sign: sign,
},
],
});
this.#loginReqs.push({
resolve,
reject,
});
});
});
}
/**
* After-connected-logic, probably do auto login
*/
#afterConnected = async () => {
// reset queues
this.#operationQueue = [];
this.#loginReqs = [];
this.#subChannelReqs = [];
this.#tradeReqsMap = new Map();
this.#channelTopicMessageHandlersMap = new Map();
if (this.#options.autoLogin && this.#profileConfig?.apiKey) {
await this.#authentication();
}
};
/**
* Handle when receiving server side messages
* @param message
* @returns
*/
#onMessage = (message) => {
this.#eventEmitter.emit('message', message);
if (message === 'pong') {
return;
}
let messageObj = null;
try {
messageObj = JSON.parse(message);
let eventType = messageObj?.event;
// channel messages
if (!eventType && messageObj?.arg?.channel) {
const topicKey = JSON.stringify(messageObj.arg);
const handlers = this.#channelTopicMessageHandlersMap.get(topicKey);
if (this.#options.logChannelTopicMessage) {
console.debug(`Received Topic Msg: '${message}'`);
}
if (handlers) {
for (const handler of handlers) {
handler(messageObj);
}
}
return;
}
const code = messageObj?.code || '';
let isError = false;
if (typeof messageObj?.code === 'string') {
isError = messageObj?.code !== '0';
}
else if (messageObj?.event === 'error') {
isError = true;
}
const msg = messageObj?.msg ?? '';
const op = messageObj?.op ?? '';
if (isError && code === ErrorCodes_1.ErrorCodes.INVALID_REQUEST) {
const match = messageObj.msg?.match(/Invalid request: {"op": "([a-zA-Z0-9\\-]+)"/);
if (match && match[1]) {
eventType = match[1];
}
}
/**
* Some common error for login operation
*/
if (isError &&
(code === ErrorCodes_1.ErrorCodes.LOGIN_FAILED ||
code === ErrorCodes_1.ErrorCodes.BULK_LOGIN_PARTIALLY_SUCCEEDED ||
code === ErrorCodes_1.ErrorCodes.INVALID_SIGN ||
code === ErrorCodes_1.ErrorCodes.INVALID_OK_ACCESS_KEY)) {
eventType = 'login';
}
// channel subscribe -> does not exist
if (isError && code === ErrorCodes_1.ErrorCodes.DOES_NOT_EXIST && msg.startsWith('channel:')) {
eventType = 'subscribe';
}
// channel not supported for public/private error
if (isError && code === ErrorCodes_1.ErrorCodes.ENDPOINT_NOT_SUPPORT_SUBSCRIBE_CHANNEL) {
eventType = 'subscribe';
}
// cannot interpret as error actual event type
if (isError && eventType === 'error') {
if (this.#operationQueue.length > 0) {
eventType = this.#operationQueue[0].op;
}
}
// op code for trade operations
if (op && OkxV5Ws.#tradeOps[op]) {
eventType = 'trade';
}
/**
* Handle for "login" message response
*/
if (eventType === 'login') {
if (this.#options.logLoginMessage) {
console.debug(`Received Login response: '${message}'`);
}
const response = this.#loginReqs.shift();
if (response) {
if (isError) {
response.reject(messageObj);
}
else {
response.resolve(messageObj);
}
}
return;
}
/**
* Handle for "subscribe" message response
*/
if (eventType === 'subscribe') {
if (this.#options.logLoginMessage) {
console.debug(`Received Sub response: '${message}'`);
}
const response = this.#subChannelReqs.shift();
if (response) {
if (isError) {
response.reject(messageObj);
}
else {
response.resolve(messageObj);
}
}
return;
}
/**
* Handle for "unsubscribe" message response
*/
if (eventType === 'unsubscribe') {
if (this.#options.logLoginMessage) {
console.debug(`Received Unsub response: '${message}'`);
}
const response = this.#subChannelReqs.shift();
if (response) {
if (isError) {
response.reject(messageObj);
}
else {
response.resolve(messageObj);
}
}
return;
}
/**
* Handle for "trade" message response
*/
if (eventType === 'trade') {
const id = messageObj.id;
if (this.#options.logTradeMessage) {
console.debug(`Received Trade response for op ${op}, id ${id}: '${message}'`);
}
if (id) {
const opReqsMap = this.#tradeReqsMap.get(op);
const responses = opReqsMap?.get(id);
const response = responses?.shift();
if (responses?.length === 0) {
opReqsMap?.delete(id);
}
if (response) {
if (isError) {
response.reject(messageObj);
}
else {
response.resolve(messageObj);
}
}
}
return;
}
}
catch (e) {
console.error(e);
}
};
/**
* close connection
*/
close() {
this.#wsConnector.close();
}
}
exports.OkxV5Ws = OkxV5Ws;
//# sourceMappingURL=OkxV5Ws.js.map