diff --git a/README.md b/README.md index c9da03f..3b337c0 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The version on npm is the output from the `build` command and can be used in pro Each REST API group has a dedicated REST client. To avoid confusion, here are the available REST clients and the corresponding API groups: | Class | Description | |:------------------------------------------------------------------: |:----------------------------------------------------------------------------------------------------------------------------: | -| [ **Derivatives v3** ] | The Derivatves v3 APIs (successor to the Futures V2 APIs) | +| [ **Derivatives v3** ] | The Derivatives v3 APIs (successor to the Futures V2 APIs) | | [UnifiedMarginClient](src/unified-margin-client.ts) | [Derivatives (v3) Unified Margin APIs](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-introduction) | | [ContractClient](src/contract-client.ts) | [Derivatives (v3) Contract APIs](https://bybit-exchange.github.io/docs/derivativesV3/contract). | | [ **Futures v2** ] | The Futures v2 APIs | diff --git a/examples/rest-spot-tpsl.ts b/examples/rest-spot-tpsl.ts new file mode 100644 index 0000000..1cd29e8 --- /dev/null +++ b/examples/rest-spot-tpsl.ts @@ -0,0 +1,42 @@ +import { SpotClientV3 } from '../src/index'; + +// or +// import { SpotClientV3 } from 'bybit-api'; + +const symbol = 'BTCUSDT'; +const key = process.env.API_KEY_COM; +const secret = process.env.API_SECRET_COM; + +const client = new SpotClientV3({ + key, + secret, + strict_param_validation: true, +}); + +(async () => { + try { + const orderId = undefined; + const ordersPerPage = undefined; + + const orders = await client.getOpenOrders(symbol); + console.log('orders 1:', orders); + + const normalOrders = await client.getOpenOrders( + symbol, + orderId, + ordersPerPage, + 0 + ); + console.log('normal orders:', normalOrders); + + const tpSlOrders = await client.getOpenOrders( + symbol, + orderId, + ordersPerPage, + 1 + ); + console.log('tpSlOrders:', tpSlOrders); + } catch (e) { + console.error('request failed: ', e); + } +})(); diff --git a/package-lock.json b/package-lock.json index 61cfaeb..deed753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bybit-api", - "version": "3.1.1", + "version": "3.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bybit-api", - "version": "3.1.1", + "version": "3.1.3", "license": "MIT", "dependencies": { "axios": "^0.21.0", @@ -5897,9 +5897,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -12313,9 +12313,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" diff --git a/package.json b/package.json index 7b7e1dc..2d7cf31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "3.1.2", + "version": "3.1.3", "description": "Complete & robust node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/constants/enum.ts b/src/constants/enum.ts index db6d1e6..3ac4d79 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -17,6 +17,7 @@ export const API_ERROR_CODE = { /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, INVALID_API_KEY_OR_PERMISSIONS: 10003, + SIGNATURE_NOT_VALID: 10004, INCORRECT_API_KEY_PERMISSIONS: 10005, INCORRECT_PRIVATE_OPERATIONS: 3303001, /** Account not unified margin, update required */ diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 8e47583..248fe33 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -108,7 +108,7 @@ export default abstract class BaseRestClient { } } - private isSpotClient() { + private isSpotV1Client() { return this.clientType === REST_CLIENT_TYPE_ENUM.spot; } @@ -233,7 +233,7 @@ export default abstract class BaseRestClient { isPublicApi ); - if (method === 'GET' || this.isSpotClient()) { + if (method === 'GET' || this.isSpotV1Client()) { return { ...options, params: signResult.paramsWithSign, @@ -343,13 +343,20 @@ export default abstract class BaseRestClient { // usdc is different for some reason if (signMethod === 'usdc') { + const sortProperties = false; const signRequestParams = method === 'GET' - ? serializeParams(res.originalParams, strictParamValidation) + ? serializeParams( + res.originalParams, + strictParamValidation, + sortProperties + ) : JSON.stringify(res.originalParams); const paramsStr = timestamp + key + recvWindow + signRequestParams; res.sign = await signMessage(paramsStr, this.secret); + + // console.log('sign req: ', paramsStr); return res; } @@ -360,16 +367,18 @@ export default abstract class BaseRestClient { // Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen. if (recvWindow) { - if (this.isSpotClient()) { + if (this.isSpotV1Client()) { res.originalParams.recvWindow = recvWindow; } else { res.originalParams.recv_window = recvWindow; } } + const sortProperties = true; res.serializedParams = serializeParams( res.originalParams, - strictParamValidation + strictParamValidation, + sortProperties ); res.sign = await signMessage(res.serializedParams, this.secret); res.paramsWithSign = { diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index 5c0bf61..bf5ca66 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -30,12 +30,23 @@ export interface RestClientOptions { parse_exceptions?: boolean; } +/** + * Serialise a (flat) object into a query string + * @param params the object to serialise + * @param strict_validation throw if any properties are undefined + * @param sortProperties sort properties alphabetically before building a query string + * @returns the params object as a serialised string key1=value1&key2=value2&etc + */ export function serializeParams( params: object = {}, - strict_validation = false + strict_validation = false, + sortProperties = true ): string { - return Object.keys(params) - .sort() + const properties = sortProperties + ? Object.keys(params).sort() + : Object.keys(params); + + return properties .map((key) => { const value = params[key]; if (strict_validation === true && typeof value === 'undefined') { diff --git a/test/response.util.ts b/test/response.util.ts index 2804b66..1044b09 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -12,10 +12,10 @@ export function successResponseList(successMsg: string | null = 'OK') { export function successResponseListV3() { return { + ...successEmptyResponseObjectV3(), result: { list: expect.any(Array), }, - ...successEmptyResponseObjectV3(), }; } @@ -36,8 +36,8 @@ export function successResponseObjectV3() { export function successEmptyResponseObjectV3() { return { - retCode: API_ERROR_CODE.SUCCESS, retMsg: expect.stringMatching(SUCCESS_MSG_REGEX), + retCode: API_ERROR_CODE.SUCCESS, }; } diff --git a/test/spot/private.v3.read.test.ts b/test/spot/private.v3.read.test.ts index 6db17ae..8c4fa37 100644 --- a/test/spot/private.v3.read.test.ts +++ b/test/spot/private.v3.read.test.ts @@ -35,6 +35,28 @@ describe('Private Spot REST API GET Endpoints', () => { expect(await api.getOpenOrders()).toMatchObject(successResponseListV3()); }); + it('getOpenOrders() with symbol', async () => { + expect(await api.getOpenOrders(symbol)).toMatchObject( + successResponseListV3() + ); + }); + + it('getOpenOrders() with order category', async () => { + const orderId = undefined; + const ordersPerPage = undefined; + + // all these should succeed + expect( + await api.getOpenOrders(symbol, orderId, ordersPerPage) + ).toMatchObject(successResponseListV3()); + expect( + await api.getOpenOrders(symbol, orderId, ordersPerPage, 0) + ).toMatchObject(successResponseListV3()); + expect( + await api.getOpenOrders(symbol, orderId, ordersPerPage, 1) + ).toMatchObject(successResponseListV3()); + }); + it('getPastOrders()', async () => { expect(await api.getPastOrders()).toMatchObject(successResponseListV3()); });