Merge pull request #198 from tiagosiebler/cursor

v3.3.0: fix rare sign error when using cursors in private unified margin API calls. expand unified margin response types. expand tests.
This commit is contained in:
Tiago
2022-11-15 16:31:56 +00:00
committed by GitHub
10 changed files with 191 additions and 7 deletions

View File

@@ -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);
}
})();

View File

@@ -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);
}
})();

View File

@@ -1,6 +1,6 @@
{ {
"name": "bybit-api", "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.", "description": "Complete & robust node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@@ -1,3 +1,4 @@
export * from './shared'; export * from './shared';
export * from './spot'; export * from './spot';
export * from './usdt-perp'; export * from './usdt-perp';
export * from './unified-margin';

View File

@@ -0,0 +1,70 @@
export interface UMPaginatedResult<List = any> {
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;
}

View File

@@ -26,6 +26,9 @@ import {
InternalTransferRequest, InternalTransferRequest,
UMExchangeCoinsRequest, UMExchangeCoinsRequest,
UMBorrowHistoryRequest, UMBorrowHistoryRequest,
UMPaginatedResult,
UMHistoricOrder,
UMInstrumentInfo,
} from './types'; } from './types';
import { REST_CLIENT_TYPE_ENUM } from './util'; import { REST_CLIENT_TYPE_ENUM } from './util';
import BaseRestClient from './util/BaseRestClient'; 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 */ /** Get trading rules per symbol/contract, incl price/amount/value/leverage filters */
getInstrumentInfo( getInstrumentInfo(
params: UMInstrumentInfoRequest params: UMInstrumentInfoRequest
): Promise<APIResponseV3<any>> { ): Promise<APIResponseV3<UMPaginatedResult<UMInstrumentInfo>>> {
return this.get('/derivatives/v3/public/instruments-info', params); 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() */ /** 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( getHistoricOrders(
params: UMHistoricOrdersRequest params: UMHistoricOrdersRequest
): Promise<APIResponseV3<any>> { ): Promise<APIResponseV3<UMPaginatedResult<UMHistoricOrder>>> {
return this.getPrivate('/unified/v3/private/order/list', params); return this.getPrivate('/unified/v3/private/order/list', params);
} }

View File

@@ -79,6 +79,8 @@ export default abstract class BaseRestClient {
enable_time_sync: false, enable_time_sync: false,
/** How often to sync time drift with bybit servers (if time sync is enabled) */ /** How often to sync time drift with bybit servers (if time sync is enabled) */
sync_interval_ms: 3600000, 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, ...restOptions,
}; };
@@ -337,6 +339,7 @@ export default abstract class BaseRestClient {
const recvWindow = const recvWindow =
res.originalParams.recv_window || this.options.recv_window || 5000; res.originalParams.recv_window || this.options.recv_window || 5000;
const strictParamValidation = this.options.strict_param_validation; 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) // In case the parent function needs it (e.g. USDC uses a header)
res.recvWindow = recvWindow; res.recvWindow = recvWindow;
@@ -349,7 +352,8 @@ export default abstract class BaseRestClient {
? serializeParams( ? serializeParams(
res.originalParams, res.originalParams,
strictParamValidation, strictParamValidation,
sortProperties sortProperties,
encodeSerialisedValues
) )
: JSON.stringify(res.originalParams); : JSON.stringify(res.originalParams);
@@ -378,7 +382,8 @@ export default abstract class BaseRestClient {
res.serializedParams = serializeParams( res.serializedParams = serializeParams(
res.originalParams, res.originalParams,
strictParamValidation, strictParamValidation,
sortProperties sortProperties,
encodeSerialisedValues
); );
res.sign = await signMessage(res.serializedParams, this.secret); res.sign = await signMessage(res.serializedParams, this.secret);
res.paramsWithSign = { res.paramsWithSign = {

View File

@@ -20,6 +20,13 @@ export interface RestClientOptions {
/** Default: false. If true, we'll throw errors if any params are undefined */ /** Default: false. If true, we'll throw errors if any params are undefined */
strict_param_validation?: boolean; 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 * Optionally override API protocol + domain
* e.g baseUrl: 'https://api.bytick.com' * e.g baseUrl: 'https://api.bytick.com'
@@ -35,12 +42,14 @@ export interface RestClientOptions {
* @param params the object to serialise * @param params the object to serialise
* @param strict_validation throw if any properties are undefined * @param strict_validation throw if any properties are undefined
* @param sortProperties sort properties alphabetically before building a query string * @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 * @returns the params object as a serialised string key1=value1&key2=value2&etc
*/ */
export function serializeParams( export function serializeParams(
params: object = {}, params: object = {},
strict_validation = false, strict_validation = false,
sortProperties = true sortProperties = true,
encodeSerialisedValues = true
): string { ): string {
const properties = sortProperties const properties = sortProperties
? Object.keys(params).sort() ? Object.keys(params).sort()
@@ -48,7 +57,10 @@ export function serializeParams(
return properties return properties
.map((key) => { .map((key) => {
const value = params[key]; const value = encodeSerialisedValues
? encodeURI(params[key])
: params[key];
if (strict_validation === true && typeof value === 'undefined') { if (strict_validation === true && typeof value === 'undefined') {
throw new Error( throw new Error(
'Failed to sign API request due to undefined parameter' 'Failed to sign API request due to undefined parameter'

View File

@@ -128,11 +128,15 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => {
expect( expect(
await api.replaceConditionalOrder({ await api.replaceConditionalOrder({
symbol, symbol,
order_link_id: 'fakeOrderId',
p_r_price: '50000', p_r_price: '50000',
p_r_qty: 1, p_r_qty: 1,
}) })
).toMatchObject({ ).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: expect.stringMatching(
/orderID or orderLinkID invalid|order not exists/gim
),
}); });
}); });

View File

@@ -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 () => { it('getPositions()', async () => {
expect(await api.getPositions({ category })).toMatchObject({ expect(await api.getPositions({ category })).toMatchObject({
retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED,