feat(): use web crypto API by default for sign, expose param to inject custom sign function
This commit is contained in:
183
examples/fasterHmacSign.ts
Normal file
183
examples/fasterHmacSign.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -138,12 +138,6 @@ export interface WSClientConfigurableOptions {
|
|||||||
* Look in the examples folder for a demonstration on using node's createHmac instead.
|
* Look in the examples folder for a demonstration on using node's createHmac instead.
|
||||||
*/
|
*/
|
||||||
customSignMessageFn?: (message: string, secret: string) => Promise<string>;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,5 +152,4 @@ export interface WebsocketClientOptions extends WSClientConfigurableOptions {
|
|||||||
recvWindow: number;
|
recvWindow: number;
|
||||||
authPrivateConnectionsOnConnect: boolean;
|
authPrivateConnectionsOnConnect: boolean;
|
||||||
authPrivateRequests: boolean;
|
authPrivateRequests: boolean;
|
||||||
reauthWSAPIOnReconnect: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
parseRateLimitHeaders,
|
parseRateLimitHeaders,
|
||||||
serializeParams,
|
serializeParams,
|
||||||
} from './requestUtils';
|
} from './requestUtils';
|
||||||
import { signMessage } from './node-support';
|
import { SignAlgorithm, SignEncodeMethod, signMessage } from './webCryptoAPI';
|
||||||
|
|
||||||
const ENABLE_HTTP_TRACE =
|
const ENABLE_HTTP_TRACE =
|
||||||
typeof process === 'object' &&
|
typeof process === 'object' &&
|
||||||
@@ -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
|
* @private sign request and set recv window
|
||||||
*/
|
*/
|
||||||
@@ -441,7 +453,13 @@ export default abstract class BaseRestClient {
|
|||||||
|
|
||||||
const paramsStr = timestamp + key + recvWindow + signRequestParams;
|
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;
|
res.serializedParams = signRequestParams;
|
||||||
|
|
||||||
// console.log('sign req: ', {
|
// console.log('sign req: ', {
|
||||||
@@ -473,7 +491,12 @@ export default abstract class BaseRestClient {
|
|||||||
sortProperties,
|
sortProperties,
|
||||||
encodeValues,
|
encodeValues,
|
||||||
);
|
);
|
||||||
res.sign = await signMessage(res.serializedParams, this.secret);
|
res.sign = await this.signMessage(
|
||||||
|
res.serializedParams,
|
||||||
|
this.secret,
|
||||||
|
'hex',
|
||||||
|
'SHA-256',
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
res.paramsWithSign = {
|
res.paramsWithSign = {
|
||||||
|
|||||||
@@ -167,8 +167,6 @@ export abstract class BaseWebsocketClient<
|
|||||||
authPrivateConnectionsOnConnect: true,
|
authPrivateConnectionsOnConnect: true,
|
||||||
// Individual requests do not require a signature, so this is disabled.
|
// Individual requests do not require a signature, so this is disabled.
|
||||||
authPrivateRequests: false,
|
authPrivateRequests: false,
|
||||||
// Automatically re-authenticate the WS API connection, if previously authenticated. TODO:
|
|
||||||
reauthWSAPIOnReconnect: true,
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
export async function signMessage(
|
|
||||||
message: string,
|
|
||||||
secret: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const key = await window.crypto.subtle.importKey(
|
|
||||||
'raw',
|
|
||||||
encoder.encode(secret),
|
|
||||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
|
||||||
false,
|
|
||||||
['sign'],
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const signature = await window.crypto.subtle.sign(
|
|
||||||
'HMAC',
|
|
||||||
key,
|
|
||||||
encoder.encode(message),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Array.prototype.map
|
|
||||||
.call(
|
|
||||||
new Uint8Array(signature),
|
|
||||||
(x: { toString: (arg0: number) => string }) =>
|
|
||||||
('00' + x.toString(16)).slice(-2),
|
|
||||||
)
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createHmac } from 'crypto';
|
|
||||||
|
|
||||||
/** This is async because the browser version uses a promise (browser-support) */
|
|
||||||
export async function signMessage(
|
|
||||||
message: string,
|
|
||||||
secret: string,
|
|
||||||
): Promise<string> {
|
|
||||||
return createHmac('sha256', secret).update(message).digest('hex');
|
|
||||||
}
|
|
||||||
@@ -77,6 +77,13 @@ export interface RestClientOptions {
|
|||||||
|
|
||||||
/** Default: false. Enable to throw error if rate limit parser fails */
|
/** Default: false. Enable to throw error if rate limit parser fails */
|
||||||
throwOnFailedRateLimitParse?: boolean;
|
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
84
src/util/webCryptoAPI.ts
Normal file
84
src/util/webCryptoAPI.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { neverGuard } from './websockets';
|
||||||
|
|
||||||
|
function bufferToB64(buffer: ArrayBuffer): string {
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return globalThis.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SignEncodeMethod = 'hex' | 'base64';
|
||||||
|
export type SignAlgorithm = 'SHA-256' | 'SHA-512';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to node crypto's `createHash()` function
|
||||||
|
*/
|
||||||
|
export async function hashMessage(
|
||||||
|
message: string,
|
||||||
|
method: SignEncodeMethod,
|
||||||
|
algorithm: SignAlgorithm,
|
||||||
|
): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const buffer = await globalThis.crypto.subtle.digest(
|
||||||
|
algorithm,
|
||||||
|
encoder.encode(message),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'hex': {
|
||||||
|
return Array.from(new Uint8Array(buffer))
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
case 'base64': {
|
||||||
|
return bufferToB64(buffer);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw neverGuard(method, `Unhandled sign method: "${method}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message, with a secret, using the Web Crypto API
|
||||||
|
*/
|
||||||
|
export async function signMessage(
|
||||||
|
message: string,
|
||||||
|
secret: string,
|
||||||
|
method: SignEncodeMethod,
|
||||||
|
algorithm: SignAlgorithm,
|
||||||
|
): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const key = await globalThis.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(secret),
|
||||||
|
{ name: 'HMAC', hash: algorithm },
|
||||||
|
false,
|
||||||
|
['sign'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buffer = await globalThis.crypto.subtle.sign(
|
||||||
|
'HMAC',
|
||||||
|
key,
|
||||||
|
encoder.encode(message),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'hex': {
|
||||||
|
return Array.from(new Uint8Array(buffer))
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
case 'base64': {
|
||||||
|
return bufferToB64(buffer);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw neverGuard(method, `Unhandled sign method: "${method}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
isWsPong,
|
isWsPong,
|
||||||
neverGuard,
|
neverGuard,
|
||||||
} from './util';
|
} from './util';
|
||||||
import { signMessage } from './util/node-support';
|
|
||||||
import {
|
import {
|
||||||
BaseWebsocketClient,
|
BaseWebsocketClient,
|
||||||
EmittableEvent,
|
EmittableEvent,
|
||||||
@@ -42,6 +41,7 @@ import {
|
|||||||
WsOperation,
|
WsOperation,
|
||||||
WsRequestOperationBybit,
|
WsRequestOperationBybit,
|
||||||
} from './types/websockets/ws-api';
|
} from './types/websockets/ws-api';
|
||||||
|
import { SignAlgorithm, signMessage } from './util/webCryptoAPI';
|
||||||
|
|
||||||
const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
|
const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
|
||||||
|
|
||||||
@@ -527,6 +527,18 @@ export class WebsocketClient extends BaseWebsocketClient<
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async signMessage(
|
||||||
|
paramsStr: string,
|
||||||
|
secret: string,
|
||||||
|
method: 'hex' | 'base64',
|
||||||
|
algorithm: SignAlgorithm,
|
||||||
|
): Promise<string> {
|
||||||
|
if (typeof this.options.customSignMessageFn === 'function') {
|
||||||
|
return this.options.customSignMessageFn(paramsStr, secret);
|
||||||
|
}
|
||||||
|
return await signMessage(paramsStr, secret, method, algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
protected async getWsAuthRequestEvent(wsKey: WsKey): Promise<any> {
|
protected async getWsAuthRequestEvent(wsKey: WsKey): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
||||||
@@ -566,9 +578,11 @@ export class WebsocketClient extends BaseWebsocketClient<
|
|||||||
|
|
||||||
const signatureExpiresAt = Date.now() + this.getTimeOffsetMs() + recvWindow;
|
const signatureExpiresAt = Date.now() + this.getTimeOffsetMs() + recvWindow;
|
||||||
|
|
||||||
const signature = await signMessage(
|
const signature = await this.signMessage(
|
||||||
'GET/realtime' + signatureExpiresAt,
|
'GET/realtime' + signatureExpiresAt,
|
||||||
secret,
|
secret,
|
||||||
|
'hex',
|
||||||
|
'SHA-256',
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -951,7 +965,7 @@ export class WebsocketClient extends BaseWebsocketClient<
|
|||||||
*
|
*
|
||||||
* Returned promise is rejected if an exception is detected in the reply OR the connection disconnects for any reason (even if automatic reconnect will happen).
|
* Returned promise is rejected if an exception is detected in the reply OR the connection disconnects for any reason (even if automatic reconnect will happen).
|
||||||
*
|
*
|
||||||
* After a fresh connection, you should always send a login request first.
|
* Authentication is automatic. If you didn't request authentication yourself, there might be a small delay after your first request, while the SDK automatically authenticates.
|
||||||
*
|
*
|
||||||
* If you authenticated once and you're reconnected later (e.g. connection temporarily lost), the SDK will by default automatically:
|
* If you authenticated once and you're reconnected later (e.g. connection temporarily lost), the SDK will by default automatically:
|
||||||
* - Detect you were authenticated to the WS API before
|
* - Detect you were authenticated to the WS API before
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ function generateConfig(name) {
|
|||||||
// Add '.ts' and '.tsx' as resolvable extensions.
|
// Add '.ts' and '.tsx' as resolvable extensions.
|
||||||
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
|
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
|
||||||
alias: {
|
alias: {
|
||||||
[path.resolve(__dirname, "../lib/util/node-support.js")]:
|
|
||||||
path.resolve(__dirname, "../lib/util/browser-support.js"),
|
|
||||||
process: "process/browser",
|
process: "process/browser",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user