From c46645713e85968244e95a1802df2a2535795452 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 15 Nov 2022 15:26:57 +0000 Subject: [PATCH 1/5] v3.2.1: add instrument info response type. add cursor example with unified margin request --- examples/rest-unified-margin-public-cursor.ts | 36 ++++++++++++++++++ package.json | 2 +- src/types/response/index.ts | 1 + src/types/response/unified-margin.ts | 38 +++++++++++++++++++ src/unified-margin-client.ts | 3 +- 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 examples/rest-unified-margin-public-cursor.ts create mode 100644 src/types/response/unified-margin.ts diff --git a/examples/rest-unified-margin-public-cursor.ts b/examples/rest-unified-margin-public-cursor.ts new file mode 100644 index 0000000..8d8aab6 --- /dev/null +++ b/examples/rest-unified-margin-public-cursor.ts @@ -0,0 +1,36 @@ +import { UnifiedMarginClient } from '../src/index'; + +// or +// import { UnifiedMarginClient } from 'bybit-api'; + +const client = new UnifiedMarginClient({ + strict_param_validation: true, +}); + +(async () => { + try { + // page 1 + const historicOrders1 = await client.getInstrumentInfo({ + category: 'linear', + limit: '2', + }); + console.log('page 1:', JSON.stringify(historicOrders1, null, 2)); + + // page 2 + const historicOrders2 = await client.getInstrumentInfo({ + category: 'linear', + limit: '2', + cursor: historicOrders1.result.nextPageCursor, + }); + console.log('page 2:', JSON.stringify(historicOrders2, null, 2)); + + // page 1 & 2 in one request (for comparison) + const historicOrdersBoth = await client.getInstrumentInfo({ + category: 'linear', + limit: '4', + }); + console.log('both pages', JSON.stringify(historicOrdersBoth, null, 2)); + } catch (e) { + console.error('request failed: ', e); + } +})(); diff --git a/package.json b/package.json index 418972e..ef52a83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "3.2.0", + "version": "3.2.1", "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/types/response/index.ts b/src/types/response/index.ts index 23d9753..390e4da 100644 --- a/src/types/response/index.ts +++ b/src/types/response/index.ts @@ -1,3 +1,4 @@ export * from './shared'; export * from './spot'; export * from './usdt-perp'; +export * from './unified-margin'; diff --git a/src/types/response/unified-margin.ts b/src/types/response/unified-margin.ts new file mode 100644 index 0000000..bd0cf12 --- /dev/null +++ b/src/types/response/unified-margin.ts @@ -0,0 +1,38 @@ +export interface UMLeverageFilter { + minLeverage: string; + maxLeverage: string; + leverageStep: string; +} + +export interface UMPriceFilter { + minPrice: string; + maxPrice: string; + tickSize: string; +} + +export interface UMLotSizeFilter { + maxTradingQty: string; + minTradingQty: string; + qtyStep: string; +} + +export interface UMInstrumentInfo { + symbol: string; + contractType: string; + status: string; + baseCoin: string; + quoteCoin: string; + launchTime: string; + deliveryTime: string; + deliveryFeeRate: string; + priceScale: string; + leverageFilter: UMLeverageFilter; + priceFilter: UMPriceFilter; + lotSizeFilter: UMLotSizeFilter; +} + +export interface UMInstrumentInfoResult { + category: string; + list: UMInstrumentInfo[]; + nextPageCursor: string; +} diff --git a/src/unified-margin-client.ts b/src/unified-margin-client.ts index 6ce1201..ca4e733 100644 --- a/src/unified-margin-client.ts +++ b/src/unified-margin-client.ts @@ -26,6 +26,7 @@ import { InternalTransferRequest, UMExchangeCoinsRequest, UMBorrowHistoryRequest, + UMInstrumentInfoResult, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -78,7 +79,7 @@ export class UnifiedMarginClient extends BaseRestClient { /** Get trading rules per symbol/contract, incl price/amount/value/leverage filters */ getInstrumentInfo( params: UMInstrumentInfoRequest - ): Promise> { + ): Promise> { return this.get('/derivatives/v3/public/instruments-info', params); } From 226952d5d2d2ec7f859ab33fbda3f4dd78de19c6 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 15 Nov 2022 16:19:32 +0000 Subject: [PATCH 2/5] v3.2.1: add another UM response type. --- .../rest-unified-margin-private-cursor.ts | 44 +++++++++++++++++++ src/types/response/unified-margin.ts | 40 +++++++++++++++-- src/unified-margin-client.ts | 8 ++-- test/inverse-futures/private.write.test.ts | 1 + test/unified-margin/private.read.test.ts | 9 ++++ 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 examples/rest-unified-margin-private-cursor.ts diff --git a/examples/rest-unified-margin-private-cursor.ts b/examples/rest-unified-margin-private-cursor.ts new file mode 100644 index 0000000..5860424 --- /dev/null +++ b/examples/rest-unified-margin-private-cursor.ts @@ -0,0 +1,44 @@ +import { UnifiedMarginClient } from '../src/index'; + +// or +// import { UnifiedMarginClient } from 'bybit-api'; + +const key = process.env.API_KEY_COM; +const secret = process.env.API_SECRET_COM; + +const client = new UnifiedMarginClient({ + key, + secret, + strict_param_validation: true, +}); + +(async () => { + try { + // page 1 + const historicOrders1 = await client.getHistoricOrders({ + category: 'linear', + limit: 1, + // cursor, + }); + console.log('page 1:', JSON.stringify(historicOrders1, null, 2)); + + // page 2 + const historicOrders2 = await client.getHistoricOrders({ + category: 'linear', + limit: 1, + cursor: historicOrders1.result.nextPageCursor, + }); + console.log('page 2:', JSON.stringify(historicOrders2, null, 2)); + + const historicOrdersBoth = await client.getHistoricOrders({ + category: 'linear', + limit: 2, + }); + console.log( + 'both to compare:', + JSON.stringify(historicOrdersBoth, null, 2) + ); + } catch (e) { + console.error('request failed: ', e); + } +})(); diff --git a/src/types/response/unified-margin.ts b/src/types/response/unified-margin.ts index bd0cf12..468d3be 100644 --- a/src/types/response/unified-margin.ts +++ b/src/types/response/unified-margin.ts @@ -1,3 +1,9 @@ +export interface UMPaginatedResult { + nextPageCursor: string; + category: string; + list: List[]; +} + export interface UMLeverageFilter { minLeverage: string; maxLeverage: string; @@ -31,8 +37,34 @@ export interface UMInstrumentInfo { lotSizeFilter: UMLotSizeFilter; } -export interface UMInstrumentInfoResult { - category: string; - list: UMInstrumentInfo[]; - nextPageCursor: string; +export interface UMHistoricOrder { + symbol: string; + orderType: string; + orderLinkId: string; + orderId: string; + stopOrderType: string; + orderStatus: string; + takeProfit: string; + cumExecValue: string; + blockTradeId: string; + rejectReason: string; + price: string; + createdTime: number; + tpTriggerBy: string; + timeInForce: string; + basePrice: string; + leavesValue: string; + updatedTime: number; + side: string; + triggerPrice: string; + cumExecFee: string; + slTriggerBy: string; + leavesQty: string; + closeOnTrigger: boolean; + cumExecQty: string; + reduceOnly: boolean; + qty: string; + stopLoss: string; + triggerBy: string; + orderIM: string; } diff --git a/src/unified-margin-client.ts b/src/unified-margin-client.ts index ca4e733..730bcc7 100644 --- a/src/unified-margin-client.ts +++ b/src/unified-margin-client.ts @@ -26,7 +26,9 @@ import { InternalTransferRequest, UMExchangeCoinsRequest, UMBorrowHistoryRequest, - UMInstrumentInfoResult, + UMPaginatedResult, + UMHistoricOrder, + UMInstrumentInfo, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -79,7 +81,7 @@ export class UnifiedMarginClient extends BaseRestClient { /** Get trading rules per symbol/contract, incl price/amount/value/leverage filters */ getInstrumentInfo( params: UMInstrumentInfoRequest - ): Promise> { + ): Promise>> { return this.get('/derivatives/v3/public/instruments-info', params); } @@ -168,7 +170,7 @@ export class UnifiedMarginClient extends BaseRestClient { /** Query order history. As order creation/cancellation is asynchronous, the data returned from the interface may be delayed. To access order information in real-time, call getActiveOrders() */ getHistoricOrders( params: UMHistoricOrdersRequest - ): Promise> { + ): Promise>> { return this.getPrivate('/unified/v3/private/order/list', params); } diff --git a/test/inverse-futures/private.write.test.ts b/test/inverse-futures/private.write.test.ts index b327aaa..ce38716 100644 --- a/test/inverse-futures/private.write.test.ts +++ b/test/inverse-futures/private.write.test.ts @@ -133,6 +133,7 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + // ret_msg: expect.stringMatching(/OK/gim), }); }); diff --git a/test/unified-margin/private.read.test.ts b/test/unified-margin/private.read.test.ts index 9deeced..be2ef35 100644 --- a/test/unified-margin/private.read.test.ts +++ b/test/unified-margin/private.read.test.ts @@ -31,6 +31,15 @@ describe('Private Unified Margin REST API GET Endpoints', () => { }); }); + it('getHistoricOrders() with cursor', async () => { + const cursor = + 'fb56c285-02ac-424e-a6b1-d10413b65fab%3A1668178953132%2Cfb56c285-02ac-424e-a6b1-d10413b65fab%3A1668178953132'; + expect(await api.getHistoricOrders({ category, cursor })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + retMsg: expect.stringMatching(/not.*unified margin/gim), + }); + }); + it('getPositions()', async () => { expect(await api.getPositions({ category })).toMatchObject({ retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, From 7b10ae08a20f9d7080bf891c6c73e9523029b694 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 15 Nov 2022 16:22:04 +0000 Subject: [PATCH 3/5] v3.2.1: fix sign error when using cursor containing percent symbols. URI encode request parameters by default when serialising during sign. --- src/util/BaseRestClient.ts | 9 +++++++-- src/util/requestUtils.ts | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 248fe33..a83d687 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -79,6 +79,8 @@ export default abstract class BaseRestClient { enable_time_sync: false, /** How often to sync time drift with bybit servers (if time sync is enabled) */ sync_interval_ms: 3600000, + /** Request parameter values are now URI encoded by default during signing. Set to false to override this behaviour. */ + encodeSerialisedValues: true, ...restOptions, }; @@ -337,6 +339,7 @@ export default abstract class BaseRestClient { const recvWindow = res.originalParams.recv_window || this.options.recv_window || 5000; const strictParamValidation = this.options.strict_param_validation; + const encodeSerialisedValues = this.options.encodeSerialisedValues; // In case the parent function needs it (e.g. USDC uses a header) res.recvWindow = recvWindow; @@ -349,7 +352,8 @@ export default abstract class BaseRestClient { ? serializeParams( res.originalParams, strictParamValidation, - sortProperties + sortProperties, + encodeSerialisedValues ) : JSON.stringify(res.originalParams); @@ -378,7 +382,8 @@ export default abstract class BaseRestClient { res.serializedParams = serializeParams( res.originalParams, strictParamValidation, - sortProperties + sortProperties, + encodeSerialisedValues ); res.sign = await signMessage(res.serializedParams, this.secret); res.paramsWithSign = { diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index bf5ca66..7c60b01 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -20,6 +20,13 @@ export interface RestClientOptions { /** Default: false. If true, we'll throw errors if any params are undefined */ strict_param_validation?: boolean; + /** + * Default: true. + * If true, request parameters will be URI encoded during the signing process. + * New behaviour introduced in v3.2.1 to fix rare parameter-driven sign errors with unified margin cursors containing "%". + */ + encodeSerialisedValues?: boolean; + /** * Optionally override API protocol + domain * e.g baseUrl: 'https://api.bytick.com' @@ -35,12 +42,14 @@ export interface RestClientOptions { * @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 + * @param encodeSerialisedValues URL encode value before serialising * @returns the params object as a serialised string key1=value1&key2=value2&etc */ export function serializeParams( params: object = {}, strict_validation = false, - sortProperties = true + sortProperties = true, + encodeSerialisedValues = true ): string { const properties = sortProperties ? Object.keys(params).sort() @@ -48,7 +57,10 @@ export function serializeParams( return properties .map((key) => { - const value = params[key]; + const value = encodeSerialisedValues + ? encodeURI(params[key]) + : params[key]; + if (strict_validation === true && typeof value === 'undefined') { throw new Error( 'Failed to sign API request due to undefined parameter' From 45373a1d1c749deb69aee26069f8a48220a17063 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 15 Nov 2022 16:24:43 +0000 Subject: [PATCH 4/5] v3.3.0: fix rare sign issue when using cursor with private unified margin requests. URI encode serialised request values by default. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef52a83..9204cda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "3.2.1", + "version": "3.3.0", "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", From 22849818e74b893a754960f576e422fdbc1e95f3 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 15 Nov 2022 16:27:16 +0000 Subject: [PATCH 5/5] add order ID to test --- test/inverse-futures/private.write.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/inverse-futures/private.write.test.ts b/test/inverse-futures/private.write.test.ts index ce38716..7573ca5 100644 --- a/test/inverse-futures/private.write.test.ts +++ b/test/inverse-futures/private.write.test.ts @@ -128,12 +128,15 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { expect( await api.replaceConditionalOrder({ symbol, + order_link_id: 'fakeOrderId', p_r_price: '50000', p_r_qty: 1, }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - // ret_msg: expect.stringMatching(/OK/gim), + ret_msg: expect.stringMatching( + /orderID or orderLinkID invalid|order not exists/gim + ), }); });