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/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..9204cda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "3.2.0", + "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", 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..468d3be --- /dev/null +++ b/src/types/response/unified-margin.ts @@ -0,0 +1,70 @@ +export interface UMPaginatedResult { + nextPageCursor: string; + category: string; + list: List[]; +} + +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 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 6ce1201..730bcc7 100644 --- a/src/unified-margin-client.ts +++ b/src/unified-margin-client.ts @@ -26,6 +26,9 @@ import { InternalTransferRequest, UMExchangeCoinsRequest, UMBorrowHistoryRequest, + UMPaginatedResult, + UMHistoricOrder, + UMInstrumentInfo, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -78,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); } @@ -167,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/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' diff --git a/test/inverse-futures/private.write.test.ts b/test/inverse-futures/private.write.test.ts index b327aaa..7573ca5 100644 --- a/test/inverse-futures/private.write.test.ts +++ b/test/inverse-futures/private.write.test.ts @@ -128,11 +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( + /orderID or orderLinkID invalid|order not exists/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,