Skip to content

Commit

Permalink
feat(): use web crypto API by default for sign, expose param to injec…
Browse files Browse the repository at this point in the history
…t custom sign function
  • Loading branch information
tiagosiebler committed Jan 21, 2025
1 parent 13cc5dd commit 13cd799
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 55 deletions.
183 changes: 183 additions & 0 deletions examples/fasterHmacSign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { createHmac } from 'crypto';
import { DefaultLogger, RestClientV5, WebsocketClient } from '../src/index';

// or
// import { createHmac } from 'crypto';
// import { DefaultLogger, RestClientV5, WebsocketClient } from 'bybit-api';

/**
* Injecting a custom signMessage function.
*
* As of version 4.0.0 of the bybit-api Node.js/TypeScript/JavaScript
* SDK for Bybit, the SDK uses the Web Crypto API for signing requests.
* While it is compatible with Node and Browser environments, it is
* slightly slower than using Node's native crypto module (only
* available in backend Node environments).
*
* For latency sensitive users, you can inject the previous node crypto sign
* method (or your own even faster-implementation), if this change affects you.
*
* This example demonstrates how to inject a custom sign function, to achieve
* the same peformance as seen before the Web Crypto API was introduced.
*
* For context on standard usage, the "signMessage" function is used:
* - During every single API call
* - After opening a new private WebSocket connection
*
*/

const key = process.env.API_KEY_COM;
const secret = process.env.API_SECRET_COM;

const restClient = new RestClientV5({
key: key,
secret: secret,
parseAPIRateLimits: true,
/**
* Set this to true to enable demo trading:
*/
demoTrading: true,
/**
* Overkill in almost every case, but if you need any optimisation available,
* you can inject a faster sign mechanism such as node's native createHmac:
*/
customSignMessageFn: async (message, secret) => {
return createHmac('sha256', secret).update(message).digest('hex');
},
});

// Optional, uncomment the "silly" override to log a lot more info about what the WS client is doing
const customLogger = {
...DefaultLogger,
// silly: (...params) => console.log('trace', ...params),
};

const wsClient = new WebsocketClient(
{
key: key,
secret: secret,
/**
* Set this to true to enable demo trading for the private account data WS
* Topics: order,execution,position,wallet,greeks
*/
demoTrading: true,
/**
* Overkill in almost every case, but if you need any optimisation available,
* you can inject a faster sign mechanism such as node's native createHmac:
*/
customSignMessageFn: async (message, secret) => {
return createHmac('sha256', secret).update(message).digest('hex');
},
},
customLogger,
);

function setWsClientEventListeners(
websocketClient: WebsocketClient,
accountRef: string,
): Promise<void> {
return new Promise((resolve) => {
websocketClient.on('update', (data) => {
console.log(new Date(), accountRef, 'data ', JSON.stringify(data));
// console.log('raw message received ', JSON.stringify(data, null, 2));
});

websocketClient.on('open', (data) => {
console.log(
new Date(),
accountRef,
'connection opened open:',
data.wsKey,
);
});
websocketClient.on('response', (data) => {
console.log(
new Date(),
accountRef,
'log response: ',
JSON.stringify(data, null, 2),
);

if (typeof data.req_id === 'string') {
const topics = data.req_id.split(',');
if (topics.length) {
console.log(new Date(), accountRef, 'Subscribed to topics: ', topics);
return resolve();
}
}
});
websocketClient.on('reconnect', ({ wsKey }) => {
console.log(
new Date(),
accountRef,
'ws automatically reconnecting.... ',
wsKey,
);
});
websocketClient.on('reconnected', (data) => {
console.log(new Date(), accountRef, 'ws has reconnected ', data?.wsKey);
});
websocketClient.on('error', (data) => {
console.error(new Date(), accountRef, 'ws exception: ', data);
});
});
}

(async () => {
try {
const onSubscribed = setWsClientEventListeners(wsClient, 'demoAcc');

wsClient.subscribeV5(['position', 'execution', 'wallet'], 'linear');

// Simple promise to ensure we're subscribed before trying anything else
await onSubscribed;

// Start trading
const balResponse1 = await restClient.getWalletBalance({
accountType: 'UNIFIED',
});
console.log('balResponse1: ', JSON.stringify(balResponse1, null, 2));

const demoFunds = await restClient.requestDemoTradingFunds();
console.log('requested demo funds: ', demoFunds);

const balResponse2 = await restClient.getWalletBalance({
accountType: 'UNIFIED',
});
console.log('balResponse2: ', JSON.stringify(balResponse2, null, 2));

/** Simple examples for private REST API calls with bybit's V5 REST APIs */
const response = await restClient.getPositionInfo({
category: 'linear',
symbol: 'BTCUSDT',
});

console.log('response:', response);

// Trade USDT linear perps
const buyOrderResult = await restClient.submitOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderType: 'Market',
qty: '1',
side: 'Buy',
});
console.log('buyOrderResult:', buyOrderResult);

const sellOrderResult = await restClient.submitOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderType: 'Market',
qty: '1',
side: 'Sell',
});
console.log('sellOrderResult:', sellOrderResult);

const balResponse3 = await restClient.getWalletBalance({
accountType: 'UNIFIED',
});
console.log('balResponse2: ', JSON.stringify(balResponse3, null, 2));
} catch (e) {
console.error('request failed: ', e);
}
})();
7 changes: 0 additions & 7 deletions src/types/websockets/ws-general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,6 @@ export interface WSClientConfigurableOptions {
* Look in the examples folder for a demonstration on using node's createHmac instead.
*/
customSignMessageFn?: (message: string, secret: string) => Promise<string>;

/**
* If you authenticated the WS API before, automatically try to
* re-authenticate the WS API if you're disconnected/reconnected for any reason.
*/
reauthWSAPIOnReconnect?: boolean;
}

/**
Expand All @@ -158,5 +152,4 @@ export interface WebsocketClientOptions extends WSClientConfigurableOptions {
recvWindow: number;
authPrivateConnectionsOnConnect: boolean;
authPrivateRequests: boolean;
reauthWSAPIOnReconnect: boolean;
}
29 changes: 26 additions & 3 deletions src/util/BaseRestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
parseRateLimitHeaders,
serializeParams,
} from './requestUtils';
import { signMessage } from './node-support';
import { SignAlgorithm, SignEncodeMethod, signMessage } from './webCryptoAPI';

const ENABLE_HTTP_TRACE =
typeof process === 'object' &&
Expand Down Expand Up @@ -394,6 +394,18 @@ export default abstract class BaseRestClient {
};
}

private async signMessage(
paramsStr: string,
secret: string,
method: SignEncodeMethod,
algorithm: SignAlgorithm,
): Promise<string> {
if (typeof this.options.customSignMessageFn === 'function') {
return this.options.customSignMessageFn(paramsStr, secret);
}
return await signMessage(paramsStr, secret, method, algorithm);
}

/**
* @private sign request and set recv window
*/
Expand Down Expand Up @@ -441,7 +453,13 @@ export default abstract class BaseRestClient {

const paramsStr = timestamp + key + recvWindow + signRequestParams;

res.sign = await signMessage(paramsStr, this.secret);
res.sign = await this.signMessage(
paramsStr,
this.secret,
'hex',
'SHA-256',
);

res.serializedParams = signRequestParams;

// console.log('sign req: ', {
Expand Down Expand Up @@ -473,7 +491,12 @@ export default abstract class BaseRestClient {
sortProperties,
encodeValues,
);
res.sign = await signMessage(res.serializedParams, this.secret);
res.sign = await this.signMessage(
res.serializedParams,
this.secret,
'hex',
'SHA-256',
);

// @ts-ignore
res.paramsWithSign = {
Expand Down
2 changes: 0 additions & 2 deletions src/util/BaseWSClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,6 @@ export abstract class BaseWebsocketClient<
authPrivateConnectionsOnConnect: true,
// Individual requests do not require a signature, so this is disabled.
authPrivateRequests: false,
// Automatically re-authenticate the WS API connection, if previously authenticated. TODO:
reauthWSAPIOnReconnect: true,
...options,
};

Expand Down
29 changes: 0 additions & 29 deletions src/util/browser-support.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/util/node-support.ts

This file was deleted.

7 changes: 7 additions & 0 deletions src/util/requestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export interface RestClientOptions {

/** Default: false. Enable to throw error if rate limit parser fails */
throwOnFailedRateLimitParse?: boolean;

/**
* Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method
*
* Look in the examples folder for a demonstration on using node's createHmac instead.
*/
customSignMessageFn?: (message: string, secret: string) => Promise<string>;
}

/**
Expand Down
Loading

0 comments on commit 13cd799

Please sign in to comment.