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:
44
examples/rest-unified-margin-private-cursor.ts
Normal file
44
examples/rest-unified-margin-private-cursor.ts
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
36
examples/rest-unified-margin-public-cursor.ts
Normal file
36
examples/rest-unified-margin-public-cursor.ts
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
70
src/types/response/unified-margin.ts
Normal file
70
src/types/response/unified-margin.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user