From 63201b465ce2b1ef9aac57a1605dabc71e569ede Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 13:39:07 +0100 Subject: [PATCH 01/74] add account asset & USDC options clients --- README.md | 27 +- src/account-asset-client.ts | 173 ++++++++++++ src/index.ts | 2 + src/inverse-client.ts | 4 +- src/types/request/account-asset.ts | 94 +++++++ src/types/request/index.ts | 1 + src/types/shared.ts | 2 +- src/usdc-options-client.ts | 306 +++++++++++++++++++++ src/util/BaseRestClient.ts | 212 +++++++++++--- src/util/index.ts | 3 + src/util/requestUtils.ts | 13 +- test/account-asset/private.read.test.ts | 67 +++++ test/account-asset/public.read.test.ts | 20 ++ test/inverse-futures/private.write.test.ts | 11 - test/inverse/private.read.test.ts | 2 +- test/inverse/private.write.test.ts | 8 - test/response.util.ts | 8 + test/usdc/options/private.read.test.ts | 82 ++++++ test/usdc/options/public.read.test.ts | 54 ++++ 19 files changed, 1009 insertions(+), 80 deletions(-) create mode 100644 src/account-asset-client.ts create mode 100644 src/types/request/account-asset.ts create mode 100644 src/usdc-options-client.ts create mode 100644 src/util/index.ts create mode 100644 test/account-asset/private.read.test.ts create mode 100644 test/account-asset/public.read.test.ts create mode 100644 test/usdc/options/private.read.test.ts create mode 100644 test/usdc/options/public.read.test.ts diff --git a/README.md b/README.md index 352e6b5..afa49d0 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,18 @@ Most methods accept JS objects. These can be populated using parameters specifie ## REST Clients Each REST API category has a dedicated REST client. Here are the REST clients and their API group: -| Class | Description | -|:-----------------------------------------------------: |:------------------------------: | -| [InverseClient](src/inverse-client.ts) | Inverse Perpetual Futures (v2) | -| [LinearClient](src/linear-client.ts) | USDT Perpetual Futures (v2) | -| [InverseFuturesClient](src/inverse-futures-client.ts) | Inverse Futures (v2) | -| [SpotClient](src/spot-client.ts) | Spot Markets | -| USDC Options & Perpetual Contracts | Under Development | -| Derivatives V3 unified margin | Under Development | +| Class | Description | +|:-----------------------------------------------------: |:-----------------------------------------------------------------------------------------------------: | +| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2)](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | +| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2)](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | +| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2)](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | +| [SpotClient](src/spot-client.ts) | [Spot Markets](https://bybit-exchange.github.io/docs/spot/#t-introduction) | +| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset API](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | +| USDC Options & Perpetual Contracts | Under Development | +| Derivatives V3 unified margin | Under Development | +| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | + +Examples for using each client can be found in the [examples](./examples) folder and the [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples) repository. ## Structure The connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bybit-api). This connector is fully compatible with both TypeScript and pure JavaScript projects. @@ -60,18 +64,13 @@ The connector is written in TypeScript. A pure JavaScript version can be built u --- -# Usage +## Usage Create API credentials at Bybit - [Livenet](https://bybit.com/app/user/api-management?affiliate_id=9410&language=en-US&group_id=0&group_type=1) - [Testnet](https://testnet.bybit.com/app/user/api-management) ## REST API Clients -There are three REST API modules as there are some differences in each contract type. -1. `InverseClient` for inverse perpetual -2. `InverseFuturesClient` for inverse futures -3. `LinearClient` for linear perpetual - ### REST Inverse To use the inverse REST APIs, import the `InverseClient`: diff --git a/src/account-asset-client.ts b/src/account-asset-client.ts new file mode 100644 index 0000000..f7330d7 --- /dev/null +++ b/src/account-asset-client.ts @@ -0,0 +1,173 @@ +import { AxiosRequestConfig } from 'axios'; +import { + AccountAssetInformationRequest, + APIResponseWithTime, + DepositRecordsRequest, + EnableUniversalTransferRequest, + InternalTransferRequest, + SubAccountTransferRequest, + SupportedDepositListRequest, + TransferQueryRequest, + UniversalTransferRequest, + WithdrawalRecordsRequest, + WithdrawalRequest, +} from './types'; +import { + RestClientOptions, + getRestBaseUrl, + REST_CLIENT_TYPE_ENUM, +} from './util'; +import BaseRestClient from './util/BaseRestClient'; + +export class AccountAssetClient extends BaseRestClient { + /** + * @public Creates an instance of the Account Asset REST API client. + * + * @param {string} key - your API key + * @param {string} secret - your API secret + * @param {boolean} [useLivenet=false] + * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity + * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios + */ + constructor( + key?: string | undefined, + secret?: string | undefined, + useLivenet: boolean = false, + restClientOptions: RestClientOptions = {}, + requestOptions: AxiosRequestConfig = {} + ) { + super( + key, + secret, + getRestBaseUrl(useLivenet, restClientOptions), + restClientOptions, + requestOptions, + REST_CLIENT_TYPE_ENUM.accountAsset + ); + return this; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Transfer Data Endpoints + * + */ + + createInternalTransfer( + params: InternalTransferRequest + ): Promise> { + return this.postPrivate('/asset/v1/private/transfer', params); + } + + createSubAccountTransfer( + params: SubAccountTransferRequest + ): Promise> { + return this.postPrivate('/asset/v1/private/sub-member/transfer', params); + } + + getInternalTransfers( + params?: TransferQueryRequest + ): Promise> { + return this.getPrivate('/asset/v1/private/transfer/list', params); + } + + getSubAccountTransfers( + params?: TransferQueryRequest + ): Promise> { + return this.getPrivate( + '/asset/v1/private/sub-member/transfer/list', + params + ); + } + + getSubAccounts(): Promise> { + return this.getPrivate('/asset/v1/private/sub-member/member-ids'); + } + + enableUniversalTransfer( + params?: EnableUniversalTransferRequest + ): Promise> { + return this.postPrivate('/asset/v1/private/transferable-subs/save', params); + } + + createUniversalTransfer( + params: UniversalTransferRequest + ): Promise> { + return this.postPrivate('/asset/v1/private/universal/transfer', params); + } + + getUniversalTransfers( + params?: TransferQueryRequest + ): Promise> { + return this.getPrivate('/asset/v1/private/universal/transfer/list', params); + } + + /** + * + * Wallet & Deposit Endpoints + * + */ + + getSupportedDepositList( + params?: SupportedDepositListRequest + ): Promise> { + return this.get('/asset/v1/public/deposit/allowed-deposit-list', params); + } + + getDepositRecords( + params?: DepositRecordsRequest + ): Promise> { + return this.getPrivate('/asset/v1/private/deposit/record/query', params); + } + + getWithdrawRecords( + params?: WithdrawalRecordsRequest + ): Promise> { + return this.getPrivate('/asset/v1/private/withdraw/record/query', params); + } + + getCoinInformation(coin?: string): Promise> { + return this.getPrivate('/asset/v1/private/coin-info/query', { coin }); + } + + getAssetInformation( + params?: AccountAssetInformationRequest + ): Promise> { + return this.getPrivate('/asset/v1/private/asset-info/query', params); + } + + submitWithdrawal( + params: WithdrawalRequest + ): Promise> { + return this.postPrivate('/asset/v1/private/withdraw', params); + } + + cancelWithdrawal(withdrawalId: number): Promise> { + return this.postPrivate('/asset/v1/private/withdraw/cancel', { + id: withdrawalId, + }); + } + + getDepositAddress(coin: string): Promise> { + return this.getPrivate('/asset/v1/private/deposit/address', { coin }); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } + + getApiAnnouncements(): Promise> { + return this.get('/v2/public/announcement'); + } +} diff --git a/src/index.ts b/src/index.ts index 97c7ede..c7e61c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ +export * from './account-asset-client'; export * from './inverse-client'; export * from './inverse-futures-client'; export * from './linear-client'; export * from './spot-client'; +export * from './usdc-options-client'; export * from './websocket-client'; export * from './logger'; export * from './types'; diff --git a/src/inverse-client.ts b/src/inverse-client.ts index b597c68..491bbc3 100644 --- a/src/inverse-client.ts +++ b/src/inverse-client.ts @@ -3,7 +3,7 @@ import { getRestBaseUrl, RestClientOptions, REST_CLIENT_TYPE_ENUM, -} from './util/requestUtils'; +} from './util'; import { APIResponseWithTime, AssetExchangeRecordsReq, @@ -15,7 +15,7 @@ import { SymbolPeriodLimitParam, WalletFundRecordsReq, WithdrawRecordsReq, -} from './types/shared'; +} from './types'; import BaseRestClient from './util/BaseRestClient'; export class InverseClient extends BaseRestClient { diff --git a/src/types/request/account-asset.ts b/src/types/request/account-asset.ts new file mode 100644 index 0000000..86d418b --- /dev/null +++ b/src/types/request/account-asset.ts @@ -0,0 +1,94 @@ +export type TransferAccountType = + | 'CONTRACT' + | 'SPOT' + | 'INVESTMENT' + | 'OPTION' + | 'UNIFIED'; + +export type TransferType = 'IN' | 'OUT'; + +export type TransferStatus = 'SUCCESS' | 'PENDING' | 'FAILED'; + +export type PageDirection = 'Prev' | 'Next'; + +export interface InternalTransferRequest { + transfer_id: string; + coin: string; + amount: string; + from_account_type: TransferAccountType; + to_account_type: TransferAccountType; +} + +export interface SubAccountTransferRequest { + transfer_id: string; + coin: string; + amount: string; + sub_user_id: string; + type: TransferType; +} + +export interface TransferQueryRequest { + transfer_id?: string; + coin?: string; + status?: TransferStatus; + start_time?: number; + end_time?: number; + direction?: PageDirection; + limit?: number; + cursor?: string; +} + +export interface EnableUniversalTransferRequest { + /** A comma-separated list of subaccount UIDs, for example "123,45,14,26,46" */ + transferable_sub_ids?: string; +} + +export interface UniversalTransferRequest { + transfer_id: string; + coin: string; + amount: string; + from_member_id: string; + to_member_id: string; + from_account_type: TransferAccountType; + to_account_type: TransferAccountType; +} + +export interface SupportedDepositListRequest { + coin?: string; + chain?: string; + page_index?: number; + page_size?: number; +} + +export interface DepositRecordsRequest { + start_time?: number; + end_time?: number; + coin?: string; + cursor?: string; + direction?: PageDirection; + limit?: number; +} + +export interface WithdrawalRecordsRequest { + withdraw_id?: number; + start_time?: number; + end_time?: number; + coin?: string; + cursor?: string; + direction?: PageDirection; + limit?: number; +} + +export interface AccountAssetInformationRequest { + /** Account type. Default value: ACCOUNT_TYPE_SPOT */ + account_type?: string; + coin?: string; +} + +export interface WithdrawalRequest { + address: string; + amount: string; + coin: string; + chain: string; + tag?: string; +} diff --git a/src/types/request/index.ts b/src/types/request/index.ts index 747a7f0..bebf291 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1 +1,2 @@ +export * from './account-asset'; export * from './usdt-perp'; diff --git a/src/types/shared.ts b/src/types/shared.ts index 0b41875..23c0908 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -25,7 +25,7 @@ export interface APIResponse { result: T; } -export interface APIResponseWithTime extends APIResponse { +export interface APIResponseWithTime extends APIResponse { /** UTC timestamp */ time_now: numberInString; } diff --git a/src/usdc-options-client.ts b/src/usdc-options-client.ts new file mode 100644 index 0000000..edfa3df --- /dev/null +++ b/src/usdc-options-client.ts @@ -0,0 +1,306 @@ +import { AxiosRequestConfig } from 'axios'; +import { APIResponseWithTime } from './types'; +import { + RestClientOptions, + getRestBaseUrl, + REST_CLIENT_TYPE_ENUM, +} from './util'; +import BaseRestClient from './util/BaseRestClient'; + +export class USDCOptionsClient extends BaseRestClient { + /** + * @public Creates an instance of the USDC Options REST API client. + * + * @param {string} key - your API key + * @param {string} secret - your API secret + * @param {boolean} [useLivenet=false] uses testnet by default + * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity + * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios + */ + constructor( + key?: string | undefined, + secret?: string | undefined, + useLivenet: boolean = false, + restClientOptions: RestClientOptions = {}, + requestOptions: AxiosRequestConfig = {} + ) { + super( + key, + secret, + getRestBaseUrl(useLivenet, restClientOptions), + restClientOptions, + requestOptions, + REST_CLIENT_TYPE_ENUM.usdcOptions + ); + return this; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Market Data Endpoints + * + */ + + /** Query order book info. Each side has a depth of 25 orders. */ + getOrderBook(symbol: string): Promise> { + return this.get('/option/usdc/openapi/public/v1/order-book', { symbol }); + } + + /** Fetch trading rules (such as min/max qty). Query for all if blank. */ + getContractInfo(params?: unknown): Promise> { + return this.get('/option/usdc/openapi/public/v1/symbols', params); + } + + /** Get a symbol price/statistics ticker */ + getSymbolTicker(symbol: string): Promise> { + return this.get('/option/usdc/openapi/public/v1/tick', { symbol }); + } + + /** Get delivery information */ + getDeliveryPrice(params?: unknown): Promise> { + return this.get('/option/usdc/openapi/public/v1/delivery-price', params); + } + + /** Returned records are Taker Buy in default. */ + getLastTrades(params: unknown): Promise> { + return this.get( + '/option/usdc/openapi/public/v1/query-trade-latest', + params + ); + } + + /** + * The data is in hourly. + * If time field is not passed, it returns the recent 1 hour data by default. + * It could be any timeframe by inputting startTime & endTime, but it must satisfy [endTime - startTime] <= 30 days. + * It returns all data in 2 years when startTime & endTime are not passed. + * Both startTime & endTime entered together or both are left blank + */ + getHistoricalVolatility(params?: unknown): Promise> { + return this.get( + '/option/usdc/openapi/public/v1/query-historical-volatility', + params + ); + } + + /** + * + * Account Data Endpoints + * + */ + + /** -> Order API */ + + /** + * Place an order using the USDC Derivatives Account. + * The request status can be queried in real-time. + * The response parameters must be queried through a query or a WebSocket response. + */ + submitOrder(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/place-order', + params + ); + } + + /** + * Each request supports a max. of four orders. The reduceOnly parameter should be separate and unique for each order in the request. + */ + batchSubmitOrders( + orderRequest: unknown[] + ): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/batch-place-orders', + { orderRequest } + ); + } + + /** For Options, at least one of the three parameters — price, quantity or implied volatility — must be input. */ + modifyOrder(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/replace-order', + params + ); + } + + /** Each request supports a max. of four orders. The reduceOnly parameter should be separate and unique for each order in the request. */ + batchModifyOrders( + replaceOrderRequest: unknown[] + ): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/batch-replace-orders', + { replaceOrderRequest } + ); + } + + /** Cancel order */ + cancelOrder(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/cancel-order', + params + ); + } + + /** Batch cancel orders */ + batchCancelOrders( + cancelRequest: unknown[] + ): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/batch-cancel-orders', + { cancelRequest } + ); + } + + /** This is used to cancel all active orders. The real-time response indicates whether the request is successful, depending on retCode. */ + cancelActiveOrders(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/cancel-all', + params + ); + } + + /** Query Unfilled/Partially Filled Orders(real-time), up to last 7 days of partially filled/unfilled orders */ + getActiveRealtimeOrders(params?: unknown): Promise> { + return this.getPrivate( + '/option/usdc/openapi/private/v1/trade/query-active-orders', + params + ); + } + + /** Query Unfilled/Partially Filled Orders */ + getActiveOrders(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-active-orders', + params + ); + } + + /** Query order history. The endpoint only supports up to 30 days of queried records */ + getHistoricOrders(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-order-history', + params + ); + } + + /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ + getOrderExecutionHistory(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/execution-list', + params + ); + } + + /** -> Account API */ + + /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ + getTransactionLog(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-transaction-log', + params + ); + } + + /** Wallet info for USDC account. */ + getBalance(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-wallet-balance', + params + ); + } + + /** Asset Info */ + getAssetInfo(baseCoin?: string): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-asset-info', + { baseCoin } + ); + } + + /** + * If USDC derivatives account balance is greater than X, you can open PORTFOLIO_MARGIN, and if it is less than Y, it will automatically close PORTFOLIO_MARGIN and change back to REGULAR_MARGIN. X and Y will be adjusted according to operational requirements. + * Rest API returns the result of checking prerequisites. You could get the real status of margin mode change by subscribing margin mode. + */ + setMarginMode( + newMarginMode: 'REGULAR_MARGIN' | 'PORTFOLIO_MARGIN' + ): Promise> { + return this.postPrivate( + '/option/usdc/private/asset/account/setMarginMode', + { setMarginMode: newMarginMode } + ); + } + + /** Query margin mode for USDC account. */ + getMarginMode(): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-margin-info' + ); + } + + /** -> Positions API */ + + /** Query my positions */ + getPositions(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-position', + params + ); + } + + /** Query Delivery History */ + getDeliveryHistory(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-delivery-list', + params + ); + } + + /** Query Positions Info Upon Expiry */ + getPositionsInfoUponExpiry( + params?: unknown + ): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-position-exp-date', + params + ); + } + + /** -> Market Maker Protection */ + + /** modifyMMP */ + modifyMMP(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/mmp-modify', + params + ); + } + + /** resetMMP */ + resetMMP(currency: string): Promise> { + return this.postPrivate('/option/usdc/openapi/private/v1/mmp-reset', { + currency, + }); + } + + /** queryMMPState */ + queryMMPState(baseCoin: string): Promise> { + return this.postPrivate('/option/usdc/openapi/private/v1/get-mmp-state', { + baseCoin, + }); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } +} diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 022c23e..dd13600 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -20,7 +20,7 @@ import { // }); interface SignedRequestContext { - timestamp: number; + timestamp?: number; api_key?: string; recv_window?: number; // spot is diff from the rest... @@ -30,9 +30,19 @@ interface SignedRequestContext { interface SignedRequest { originalParams: T & SignedRequestContext; paramsWithSign?: T & SignedRequestContext & { sign: string }; + serializedParams: string; sign: string; + timestamp: number; + recvWindow: number; } +interface UnsignedRequest { + originalParams: T; + paramsWithSign: T; +} + +type SignMethod = 'keyInBody' | 'usdc'; + export default abstract class BaseRestClient { private timeOffset: number | null; private syncTimePromise: null | Promise; @@ -61,8 +71,7 @@ export default abstract class BaseRestClient { this.options = { recv_window: 5000, - - /** Throw errors if any params are undefined */ + /** Throw errors if any request params are empty */ strict_param_validation: false, /** Disable time sync by default */ enable_time_sync: false, @@ -102,6 +111,10 @@ export default abstract class BaseRestClient { return this.clientType === REST_CLIENT_TYPE_ENUM.spot; } + private isUSDCClient() { + return this.clientType === REST_CLIENT_TYPE_ENUM.usdcOptions; + } + get(endpoint: string, params?: any) { return this._call('GET', endpoint, params, true); } @@ -122,7 +135,24 @@ export default abstract class BaseRestClient { return this._call('DELETE', endpoint, params, false); } - private async prepareSignParams(params?: any, isPublicApi?: boolean) { + private async prepareSignParams( + method: Method, + signMethod: SignMethod, + params?: TParams, + isPublicApi?: true + ): Promise>; + private async prepareSignParams( + method: Method, + signMethod: SignMethod, + params?: TParams, + isPublicApi?: false | undefined + ): Promise>; + private async prepareSignParams( + method: Method, + signMethod: SignMethod, + params?: TParams, + isPublicApi?: boolean + ) { if (isPublicApi) { return { originalParams: params, @@ -138,7 +168,85 @@ export default abstract class BaseRestClient { await this.syncTime(); } - return this.signRequest(params); + return this.signRequest(params, method, signMethod); + } + + /** Returns an axios request object. Handles signing process automatically if this is a private API call */ + private async buildRequest( + method: Method, + url: string, + params?: any, + isPublicApi?: boolean + ): Promise { + const options: AxiosRequestConfig = { + ...this.globalRequestOptions, + url: url, + method: method, + }; + + for (const key in params) { + if (typeof params[key] === 'undefined') { + delete params[key]; + } + } + + if (isPublicApi) { + return { + ...options, + params: params, + }; + } + + // USDC Options uses a different way of authenticating requests (headers instead of params) + if (this.isUSDCClient()) { + if (!options.headers) { + options.headers = {}; + } + + const signResult = await this.prepareSignParams( + method, + 'usdc', + params, + isPublicApi + ); + + options.headers['X-BAPI-SIGN-TYPE'] = 2; + options.headers['X-BAPI-API-KEY'] = this.key; + options.headers['X-BAPI-TIMESTAMP'] = signResult.timestamp; + options.headers['X-BAPI-SIGN'] = signResult.sign; + options.headers['X-BAPI-RECV-WINDOW'] = signResult.recvWindow; + + if (method === 'GET') { + return { + ...options, + params: signResult.originalParams, + }; + } + + return { + ...options, + data: signResult.originalParams, + }; + } + + const signResult = await this.prepareSignParams( + method, + 'keyInBody', + params, + isPublicApi + ); + + if (method === 'GET' || this.isSpotClient()) { + return { + ...options, + params: signResult.paramsWithSign, + }; + } + + return { + ...options, + data: signResult.paramsWithSign, + }; } /** @@ -150,27 +258,20 @@ export default abstract class BaseRestClient { params?: any, isPublicApi?: boolean ): Promise { - const options = { - ...this.globalRequestOptions, - url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'), - method: method, - json: true, - }; + // Sanity check to make sure it's only ever signed by + const requestUrl = [this.baseUrl, endpoint].join( + endpoint.startsWith('/') ? '' : '/' + ); - for (const key in params) { - if (typeof params[key] === 'undefined') { - delete params[key]; - } - } - - const signResult = await this.prepareSignParams(params, isPublicApi); - - if (method === 'GET' || this.isSpotClient()) { - options.params = signResult.paramsWithSign; - } else { - options.data = signResult.paramsWithSign; - } + // Build a request and handle signature process + const options = await this.buildRequest( + method, + requestUrl, + params, + isPublicApi + ); + // Dispatch request return axios(options) .then((response) => { if (response.status == 200) { @@ -215,37 +316,70 @@ export default abstract class BaseRestClient { /** * @private sign request and set recv window */ - private async signRequest( - data: T & SignedRequestContext + private async signRequest( + data: T, + method: Method, + signMethod: SignMethod ): Promise> { + const timestamp = Date.now() + (this.timeOffset || 0); + const res: SignedRequest = { originalParams: { ...data, - api_key: this.key, - timestamp: Date.now() + (this.timeOffset || 0), }, sign: '', + timestamp, + recvWindow: 0, + serializedParams: '', }; - // Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen. - if (this.options.recv_window && !res.originalParams.recv_window) { - if (this.isSpotClient()) { - res.originalParams.recvWindow = this.options.recv_window; - } else { - res.originalParams.recv_window = this.options.recv_window; - } + if (!this.key || !this.secret) { + return res; + } + const key = this.key; + const recvWindow = + res.originalParams.recv_window || this.options.recv_window || 5000; + const strictParamValidation = this.options.strict_param_validation; + + // In case the parent function needs it (e.g. USDC uses a header) + res.recvWindow = recvWindow; + + // usdc is different for some reason + if (signMethod === 'usdc') { + const signRequestParams = + method === 'GET' + ? serializeParams(res.originalParams, strictParamValidation) + : JSON.stringify(res.originalParams); + + const paramsStr = timestamp + key + recvWindow + signRequestParams; + res.sign = await signMessage(paramsStr, this.secret); + return res; } - if (this.key && this.secret) { - const serializedParams = serializeParams( + // spot/v2 derivatives + if (signMethod === 'keyInBody') { + res.originalParams.api_key = key; + res.originalParams.timestamp = timestamp; + + // Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen. + if (recvWindow) { + if (this.isSpotClient()) { + res.originalParams.recvWindow = recvWindow; + } else { + res.originalParams.recv_window = recvWindow; + } + } + + res.serializedParams = serializeParams( res.originalParams, - this.options.strict_param_validation + strictParamValidation ); - res.sign = await signMessage(serializedParams, this.secret); + res.sign = await signMessage(res.serializedParams, this.secret); res.paramsWithSign = { ...res.originalParams, sign: res.sign, }; + return res; } return res; diff --git a/src/util/index.ts b/src/util/index.ts new file mode 100644 index 0000000..90c60e3 --- /dev/null +++ b/src/util/index.ts @@ -0,0 +1,3 @@ +export * from './BaseRestClient'; +export * from './requestUtils'; +export * from './WsStore'; diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index f8add05..610f074 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -45,8 +45,8 @@ export function serializeParams( export function getRestBaseUrl( useLivenet: boolean, restInverseOptions: RestClientOptions -) { - const baseUrlsInverse = { +): string { + const exchangeBaseUrls = { livenet: 'https://api.bybit.com', testnet: 'https://api-testnet.bybit.com', }; @@ -56,9 +56,9 @@ export function getRestBaseUrl( } if (useLivenet === true) { - return baseUrlsInverse.livenet; + return exchangeBaseUrls.livenet; } - return baseUrlsInverse.testnet; + return exchangeBaseUrls.testnet; } export function isPublicEndpoint(endpoint: string): boolean { @@ -92,11 +92,16 @@ export function isWsPong(response: any) { export const agentSource = 'bybitapinode'; +/** + * Used to switch how authentication/requests work under the hood (primarily for SPOT since it's different there) + */ export const REST_CLIENT_TYPE_ENUM = { + accountAsset: 'accountAsset', inverse: 'inverse', inverseFutures: 'inverseFutures', linear: 'linear', spot: 'spot', + usdcOptions: 'usdcOptions', } as const; export type RestClientType = diff --git a/test/account-asset/private.read.test.ts b/test/account-asset/private.read.test.ts new file mode 100644 index 0000000..c060274 --- /dev/null +++ b/test/account-asset/private.read.test.ts @@ -0,0 +1,67 @@ +import { AccountAssetClient } from '../../src/'; +import { successResponseObject } from '../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new AccountAssetClient(API_KEY, API_SECRET, useLivenet); + + it('getInternalTransfers()', async () => { + expect(await api.getInternalTransfers()).toMatchObject( + successResponseObject() + ); + }); + + it('getSubAccountTransfers()', async () => { + expect(await api.getSubAccountTransfers()).toMatchObject( + successResponseObject() + ); + }); + + it('getSubAccounts()', async () => { + expect(await api.getSubAccounts()).toMatchObject(successResponseObject()); + }); + + it('getUniversalTransfers()', async () => { + expect(await api.getInternalTransfers()).toMatchObject( + successResponseObject() + ); + }); + + it('getDepositRecords()', async () => { + expect(await api.getDepositRecords()).toMatchObject( + successResponseObject() + ); + }); + + it('getWithdrawRecords()', async () => { + expect(await api.getWithdrawRecords()).toMatchObject( + successResponseObject() + ); + }); + + it('getCoinInformation()', async () => { + expect(await api.getCoinInformation()).toMatchObject( + successResponseObject() + ); + }); + + it('getAssetInformation()', async () => { + expect(await api.getAssetInformation()).toMatchObject( + successResponseObject() + ); + }); + + it('getDepositAddress()', async () => { + expect(await api.getDepositAddress('BTC')).toMatchObject( + successResponseObject() + ); + }); +}); diff --git a/test/account-asset/public.read.test.ts b/test/account-asset/public.read.test.ts new file mode 100644 index 0000000..e6e9f0b --- /dev/null +++ b/test/account-asset/public.read.test.ts @@ -0,0 +1,20 @@ +import { AccountAssetClient } from '../../src'; +import { successResponseObject } from '../response.util'; + +describe('Public Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = undefined; + const API_SECRET = undefined; + + const api = new AccountAssetClient(API_KEY, API_SECRET, useLivenet); + + it('getSupportedDepositList()', async () => { + expect(await api.getSupportedDepositList()).toMatchObject( + successResponseObject() + ); + }); + + it('getServerTime()', async () => { + expect(await api.getServerTime()).toMatchObject(successResponseObject()); + }); +}); diff --git a/test/inverse-futures/private.write.test.ts b/test/inverse-futures/private.write.test.ts index e57ab37..d3c1cab 100644 --- a/test/inverse-futures/private.write.test.ts +++ b/test/inverse-futures/private.write.test.ts @@ -30,7 +30,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE, - ret_msg: 'position idx not match position mode', }); }); @@ -41,7 +40,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'order not exists or too late to cancel', }); }); @@ -63,7 +61,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'order not exists or too late to replace', }); }); @@ -82,7 +79,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE, - ret_msg: 'position idx not match position mode', }); }); @@ -94,7 +90,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'Order not exists', }); }); @@ -115,7 +110,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'order not exists or too late to replace', }); }); @@ -127,7 +121,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE, - ret_msg: 'position idx not match position mode', }); }); @@ -139,7 +132,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE, - ret_msg: 'position idx not match position mode', }); }); @@ -152,7 +144,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED, - ret_msg: 'leverage not modified', }); }); @@ -164,7 +155,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.POSITION_MODE_NOT_MODIFIED, - ret_msg: 'position mode not modified', }); }); @@ -178,7 +168,6 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED, - ret_msg: 'Isolated not modified', }); }); }); diff --git a/test/inverse/private.read.test.ts b/test/inverse/private.read.test.ts index e1b847c..4a3f076 100644 --- a/test/inverse/private.read.test.ts +++ b/test/inverse/private.read.test.ts @@ -1,4 +1,4 @@ -import { InverseClient } from '../../src/inverse-client'; +import { InverseClient } from '../../src/'; import { successResponseList, successResponseObject } from '../response.util'; describe('Private Inverse REST API Endpoints', () => { diff --git a/test/inverse/private.write.test.ts b/test/inverse/private.write.test.ts index 1271612..52a2b19 100644 --- a/test/inverse/private.write.test.ts +++ b/test/inverse/private.write.test.ts @@ -40,7 +40,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'order not exists or too late to cancel', }); }); @@ -62,7 +61,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'order not exists or too late to replace', }); }); @@ -81,7 +79,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE, - ret_msg: 'Insufficient wallet balance', }); }); @@ -93,7 +90,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: expect.any(String), }); }); @@ -114,7 +110,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, - ret_msg: 'order not exists or too late to replace', }); }); @@ -126,7 +121,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.POSITION_IS_CROSS_MARGIN, - ret_msg: expect.any(String), }); }); @@ -138,7 +132,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.CANNOT_SET_TRADING_STOP_FOR_ZERO_POS, - ret_msg: 'can not set tp/sl/ts for zero position', }); }); @@ -162,7 +155,6 @@ describe('Private Inverse REST API Endpoints', () => { }) ).toMatchObject({ ret_code: API_ERROR_CODE.SAME_SLTP_MODE, - ret_msg: 'same tp sl mode2', }); }); diff --git a/test/response.util.ts b/test/response.util.ts index fa9313c..5f09791 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -14,6 +14,14 @@ export function successResponseObject(successMsg: string | null = 'OK') { }; } +export function successUSDCResponseObject() { + return { + result: expect.any(Object), + retCode: 0, + retMsg: expect.stringMatching(/OK|SUCCESS|success|success\./gim), + }; +} + export function errorResponseObject( result: null | any = null, ret_code: number, diff --git a/test/usdc/options/private.read.test.ts b/test/usdc/options/private.read.test.ts new file mode 100644 index 0000000..f0d477b --- /dev/null +++ b/test/usdc/options/private.read.test.ts @@ -0,0 +1,82 @@ +import { USDCOptionsClient } from '../../../src'; +import { + successResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + const symbol = 'BTC-30SEP22-400000-C'; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); + const category = 'OPTION'; + + it('getActiveRealtimeOrders()', async () => { + expect(await api.getActiveRealtimeOrders()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getActiveOrders()', async () => { + expect(await api.getActiveOrders({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getHistoricOrders()', async () => { + expect(await api.getHistoricOrders({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getOrderExecutionHistory()', async () => { + expect(await api.getOrderExecutionHistory({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getTransactionLog()', async () => { + expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getBalance()', async () => { + expect(await api.getBalance()).toMatchObject(successUSDCResponseObject()); + }); + + it('getAssetInfo()', async () => { + expect(await api.getAssetInfo()).toMatchObject(successUSDCResponseObject()); + }); + + it('getMarginMode()', async () => { + expect(await api.getMarginMode()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getPositions()', async () => { + expect(await api.getPositions({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getDeliveryHistory()', async () => { + expect(await api.getDeliveryHistory({ symbol })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getPositionsInfoUponExpiry()', async () => { + expect(await api.getPositionsInfoUponExpiry()).toMatchObject( + successUSDCResponseObject() + ); + }); +}); diff --git a/test/usdc/options/public.read.test.ts b/test/usdc/options/public.read.test.ts new file mode 100644 index 0000000..c09754b --- /dev/null +++ b/test/usdc/options/public.read.test.ts @@ -0,0 +1,54 @@ +import { USDCOptionsClient } from '../../../src'; +import { + successResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Public USDC Options REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = undefined; + const API_SECRET = undefined; + + const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); + const symbol = 'BTC-30SEP22-400000-C'; + + it('getOrderBook()', async () => { + expect(await api.getOrderBook(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getContractInfo()', async () => { + expect(await api.getContractInfo()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getSymbolTicker()', async () => { + expect(await api.getSymbolTicker(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getDeliveryPrice()', async () => { + expect(await api.getDeliveryPrice()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLastTrades()', async () => { + expect(await api.getLastTrades({ category: 'OPTION' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getHistoricalVolatility()', async () => { + expect(await api.getHistoricalVolatility()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getServerTime()', async () => { + expect(await api.getServerTime()).toMatchObject(successResponseObject()); + }); +}); From 88bd4fd9afdd3854dc087647e52aaa727e4057ab Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 14:08:46 +0100 Subject: [PATCH 02/74] USDC Options test coverage --- src/constants/enum.ts | 7 + test/response.util.ts | 12 +- test/usdc/options/private.write.test.ts | 169 ++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 test/usdc/options/private.write.test.ts diff --git a/src/constants/enum.ts b/src/constants/enum.ts index efa97af..18b23d9 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -13,6 +13,7 @@ export const positionTpSlModeEnum = { export const API_ERROR_CODE = { BALANCE_INSUFFICIENT_SPOT: -1131, ORDER_NOT_FOUND_OR_TOO_LATE_SPOT: -2013, + SUCCESS: 0, /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, ORDER_NOT_FOUND_OR_TOO_LATE: 20001, @@ -39,6 +40,12 @@ export const API_ERROR_CODE = { INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR: 130080, SAME_SLTP_MODE_LINEAR: 130150, RISK_ID_NOT_MODIFIED: 134026, + ORDER_NOT_EXIST: 3100136, + NO_ACTIVE_ORDER: 3100205, + /** E.g. USDC Options trading when the account hasn't been opened for USDC Options yet */ + ACCOUNT_NOT_EXIST: 3200200, + INCORRECT_MMP_PARAMETERS: 3500712, + INSTITION_MMP_PROFILE_NOT_FOUND: 3500713, } as const; /** diff --git a/test/response.util.ts b/test/response.util.ts index 5f09791..5d6ba7d 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -1,7 +1,9 @@ +import { API_ERROR_CODE } from '../src'; + export function successResponseList(successMsg: string | null = 'OK') { return { result: expect.any(Array), - ret_code: 0, + ret_code: API_ERROR_CODE.SUCCESS, ret_msg: successMsg, }; } @@ -9,7 +11,7 @@ export function successResponseList(successMsg: string | null = 'OK') { export function successResponseObject(successMsg: string | null = 'OK') { return { result: expect.any(Object), - ret_code: 0, + ret_code: API_ERROR_CODE.SUCCESS, ret_msg: successMsg, }; } @@ -17,8 +19,10 @@ export function successResponseObject(successMsg: string | null = 'OK') { export function successUSDCResponseObject() { return { result: expect.any(Object), - retCode: 0, - retMsg: expect.stringMatching(/OK|SUCCESS|success|success\./gim), + retCode: API_ERROR_CODE.SUCCESS, + retMsg: expect.stringMatching( + /OK|SUCCESS|success|success\.|Request accepted/gim + ), }; } diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts new file mode 100644 index 0000000..ed172fd --- /dev/null +++ b/test/usdc/options/private.write.test.ts @@ -0,0 +1,169 @@ +import { API_ERROR_CODE, USDCOptionsClient } from '../../../src'; +import { + successResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); + + const category = 'OPTION'; + const currency = 'USDC'; + const symbol = 'BTC-30SEP22-400000-C'; + + it('submitOrder()', async () => { + expect( + await api.submitOrder({ + symbol, + orderType: 'Limit', + side: 'Sell', + orderQty: '1000', + orderPrice: '40', + orderLinkId: Date.now().toString(), + timeInForce: 'GoodTillCancel', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_EXIST, + }); + }); + + it('batchSubmitOrders()', async () => { + expect( + await api.batchSubmitOrders([ + { + symbol, + orderType: 'Limit', + side: 'Sell', + orderQty: '1000', + orderPrice: '40', + orderLinkId: Date.now().toString(), + timeInForce: 'GoodTillCancel', + }, + { + symbol, + orderType: 'Limit', + side: 'Sell', + orderQty: '1000', + orderPrice: '40', + orderLinkId: Date.now().toString(), + timeInForce: 'GoodTillCancel', + }, + ]) + ).toMatchObject({ + result: [ + { errorCode: API_ERROR_CODE.ACCOUNT_NOT_EXIST }, + { errorCode: API_ERROR_CODE.ACCOUNT_NOT_EXIST }, + ], + }); + }); + + it('modifyOrder()', async () => { + expect( + await api.modifyOrder({ + symbol, + orderId: 'somethingFake', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_EXIST, + }); + }); + + it('batchModifyOrders()', async () => { + expect( + await api.batchModifyOrders([ + { + symbol, + orderId: 'somethingFake1', + }, + { + symbol, + orderId: 'somethingFake2', + }, + ]) + ).toMatchObject({ + result: [ + { errorCode: API_ERROR_CODE.ORDER_NOT_EXIST }, + { errorCode: API_ERROR_CODE.ORDER_NOT_EXIST }, + ], + }); + }); + + it('cancelOrder()', async () => { + expect( + await api.cancelOrder({ + symbol, + orderId: 'somethingFake1', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_EXIST, + }); + }); + + it('batchCancelOrders()', async () => { + expect( + await api.batchCancelOrders([ + { + symbol, + orderId: 'somethingFake1', + }, + { + symbol, + orderId: 'somethingFake2', + }, + ]) + ).toMatchObject({ + result: [ + { errorCode: API_ERROR_CODE.ORDER_NOT_EXIST }, + { errorCode: API_ERROR_CODE.ORDER_NOT_EXIST }, + ], + }); + }); + + it('cancelActiveOrders()', async () => { + expect(await api.cancelActiveOrders()).toMatchObject({ + retCode: API_ERROR_CODE.NO_ACTIVE_ORDER, + }); + }); + + it('setMarginMode()', async () => { + expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('modifyMMP()', async () => { + expect( + await api.modifyMMP({ + currency, + windowMs: 0, + frozenPeriodMs: 100, + qtyLimit: '100', + deltaLimit: '1', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.INCORRECT_MMP_PARAMETERS, + }); + }); + + it('resetMMP()', async () => { + expect(await api.resetMMP(currency)).toMatchObject({ + retCode: API_ERROR_CODE.INSTITION_MMP_PROFILE_NOT_FOUND, + }); + }); + + /** + + it('asdfasfasdfasdf()', async () => { + expect(await api.asadfasdfasdfasf()).toStrictEqual(''); + }); + */ +}); From 666720b27d563044380cee90028b5276b01040bd Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 14:31:03 +0100 Subject: [PATCH 03/74] deprecate redundant repeated constructors --- src/account-asset-client.ts | 37 ++++++--------------------------- src/inverse-client.ts | 37 ++++++--------------------------- src/inverse-futures-client.ts | 37 ++++++--------------------------- src/linear-client.ts | 37 ++++++--------------------------- src/spot-client.ts | 39 ++++++----------------------------- src/usdc-options-client.ts | 37 ++++++--------------------------- src/util/BaseRestClient.ts | 28 ++++++++++++++++++------- 7 files changed, 56 insertions(+), 196 deletions(-) diff --git a/src/account-asset-client.ts b/src/account-asset-client.ts index f7330d7..0e2963d 100644 --- a/src/account-asset-client.ts +++ b/src/account-asset-client.ts @@ -1,4 +1,3 @@ -import { AxiosRequestConfig } from 'axios'; import { AccountAssetInformationRequest, APIResponseWithTime, @@ -12,39 +11,15 @@ import { WithdrawalRecordsRequest, WithdrawalRequest, } from './types'; -import { - RestClientOptions, - getRestBaseUrl, - REST_CLIENT_TYPE_ENUM, -} from './util'; +import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; +/** + * REST API client for Account Asset APIs + */ export class AccountAssetClient extends BaseRestClient { - /** - * @public Creates an instance of the Account Asset REST API client. - * - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] - * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios - */ - constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - restClientOptions: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} - ) { - super( - key, - secret, - getRestBaseUrl(useLivenet, restClientOptions), - restClientOptions, - requestOptions, - REST_CLIENT_TYPE_ENUM.accountAsset - ); - return this; + getClientType() { + return REST_CLIENT_TYPE_ENUM.accountAsset; } async fetchServerTime(): Promise { diff --git a/src/inverse-client.ts b/src/inverse-client.ts index 491bbc3..1d3354f 100644 --- a/src/inverse-client.ts +++ b/src/inverse-client.ts @@ -1,9 +1,4 @@ -import { AxiosRequestConfig } from 'axios'; -import { - getRestBaseUrl, - RestClientOptions, - REST_CLIENT_TYPE_ENUM, -} from './util'; +import { REST_CLIENT_TYPE_ENUM } from './util'; import { APIResponseWithTime, AssetExchangeRecordsReq, @@ -18,32 +13,12 @@ import { } from './types'; import BaseRestClient from './util/BaseRestClient'; +/** + * REST API client for Inverse Perpetual Futures APIs (v2) + */ export class InverseClient extends BaseRestClient { - /** - * @public Creates an instance of the inverse REST API client. - * - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] - * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios - */ - constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - restClientOptions: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} - ) { - super( - key, - secret, - getRestBaseUrl(useLivenet, restClientOptions), - restClientOptions, - requestOptions, - REST_CLIENT_TYPE_ENUM.inverse - ); - return this; + getClientType() { + return REST_CLIENT_TYPE_ENUM.inverse; } async fetchServerTime(): Promise { diff --git a/src/inverse-futures-client.ts b/src/inverse-futures-client.ts index 9892dc7..7411522 100644 --- a/src/inverse-futures-client.ts +++ b/src/inverse-futures-client.ts @@ -1,9 +1,4 @@ -import { AxiosRequestConfig } from 'axios'; -import { - getRestBaseUrl, - RestClientOptions, - REST_CLIENT_TYPE_ENUM, -} from './util/requestUtils'; +import { REST_CLIENT_TYPE_ENUM } from './util/requestUtils'; import { APIResponseWithTime, AssetExchangeRecordsReq, @@ -18,32 +13,12 @@ import { } from './types/shared'; import BaseRestClient from './util/BaseRestClient'; +/** + * REST API client for Inverse Futures APIs (e.g. quarterly futures) (v2) + */ export class InverseFuturesClient extends BaseRestClient { - /** - * @public Creates an instance of the inverse futures REST API client. - * - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] - * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios - */ - constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - restClientOptions: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} - ) { - super( - key, - secret, - getRestBaseUrl(useLivenet, restClientOptions), - restClientOptions, - requestOptions, - REST_CLIENT_TYPE_ENUM.inverseFutures - ); - return this; + getClientType() { + return REST_CLIENT_TYPE_ENUM.inverseFutures; } async fetchServerTime(): Promise { diff --git a/src/linear-client.ts b/src/linear-client.ts index 4a64ab3..32b6754 100644 --- a/src/linear-client.ts +++ b/src/linear-client.ts @@ -1,9 +1,4 @@ -import { AxiosRequestConfig } from 'axios'; -import { - getRestBaseUrl, - RestClientOptions, - REST_CLIENT_TYPE_ENUM, -} from './util/requestUtils'; +import { REST_CLIENT_TYPE_ENUM } from './util/requestUtils'; import { APIResponse, APIResponseWithTime, @@ -25,32 +20,12 @@ import { import { linearPositionModeEnum, positionTpSlModeEnum } from './constants/enum'; import BaseRestClient from './util/BaseRestClient'; +/** + * REST API client for linear/USD perpetual futures APIs (v2) + */ export class LinearClient extends BaseRestClient { - /** - * @public Creates an instance of the linear (USD Perps) REST API client. - * - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] - * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios - */ - constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - restClientOptions: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} - ) { - super( - key, - secret, - getRestBaseUrl(useLivenet, restClientOptions), - restClientOptions, - requestOptions, - REST_CLIENT_TYPE_ENUM.linear - ); - return this; + getClientType() { + return REST_CLIENT_TYPE_ENUM.linear; } async fetchServerTime(): Promise { diff --git a/src/spot-client.ts b/src/spot-client.ts index a31b467..7753949 100644 --- a/src/spot-client.ts +++ b/src/spot-client.ts @@ -1,4 +1,3 @@ -import { AxiosRequestConfig } from 'axios'; import { NewSpotOrder, APIResponse, @@ -11,40 +10,14 @@ import { SpotSymbolInfo, } from './types'; import BaseRestClient from './util/BaseRestClient'; -import { - agentSource, - getRestBaseUrl, - RestClientOptions, - REST_CLIENT_TYPE_ENUM, -} from './util/requestUtils'; +import { agentSource, REST_CLIENT_TYPE_ENUM } from './util/requestUtils'; +/** + * REST API client for Spot APIs (v1) + */ export class SpotClient extends BaseRestClient { - /** - * @public Creates an instance of the Spot REST API client. - * - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] - * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios - */ - constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - restClientOptions: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} - ) { - super( - key, - secret, - getRestBaseUrl(useLivenet, restClientOptions), - restClientOptions, - requestOptions, - REST_CLIENT_TYPE_ENUM.spot - ); - - return this; + getClientType() { + return REST_CLIENT_TYPE_ENUM.spot; } fetchServerTime(): Promise { diff --git a/src/usdc-options-client.ts b/src/usdc-options-client.ts index edfa3df..0bb3f9a 100644 --- a/src/usdc-options-client.ts +++ b/src/usdc-options-client.ts @@ -1,38 +1,13 @@ -import { AxiosRequestConfig } from 'axios'; import { APIResponseWithTime } from './types'; -import { - RestClientOptions, - getRestBaseUrl, - REST_CLIENT_TYPE_ENUM, -} from './util'; +import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; +/** + * REST API client for USDC Options APIs + */ export class USDCOptionsClient extends BaseRestClient { - /** - * @public Creates an instance of the USDC Options REST API client. - * - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] uses testnet by default - * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios - */ - constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - restClientOptions: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} - ) { - super( - key, - secret, - getRestBaseUrl(useLivenet, restClientOptions), - restClientOptions, - requestOptions, - REST_CLIENT_TYPE_ENUM.usdcOptions - ); - return this; + getClientType() { + return REST_CLIENT_TYPE_ENUM.usdcOptions; } async fetchServerTime(): Promise { diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index dd13600..850cbc5 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -7,6 +7,7 @@ import { RestClientType, REST_CLIENT_TYPE_ENUM, agentSource, + getRestBaseUrl, } from './requestUtils'; // axios.interceptors.request.use((request) => { @@ -53,21 +54,32 @@ export default abstract class BaseRestClient { private secret: string | undefined; private clientType: RestClientType; - /** Function that calls exchange API to query & resolve server time, used by time sync */ + /** Function that calls exchange API to query & resolve server time, used by time sync, disabled by default */ abstract fetchServerTime(): Promise; + /** Defines the client type (affecting how requests & signatures behave) */ + abstract getClientType(): RestClientType; + + /** + * Create an instance of the REST client + * @param {string} key - your API key + * @param {string} secret - your API secret + * @param {boolean} [useLivenet=false] + * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity + * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios + */ constructor( - key: string | undefined, - secret: string | undefined, - baseUrl: string, + key?: string | undefined, + secret?: string | undefined, + useLivenet: boolean = false, options: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {}, - clientType: RestClientType + requestOptions: AxiosRequestConfig = {} ) { + const baseUrl = getRestBaseUrl(useLivenet, options); this.timeOffset = null; this.syncTimePromise = null; - this.clientType = clientType; + this.clientType = this.getClientType(); this.options = { recv_window: 5000, @@ -86,7 +98,7 @@ export default abstract class BaseRestClient { // custom request options based on axios specs - see: https://github.com/axios/axios#request-config ...requestOptions, headers: { - 'x-referer': 'bybitapinode', + 'x-referer': agentSource, }, }; From 518735087849c2705b9c13acc0021f6b35cbce7d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 16:48:33 +0100 Subject: [PATCH 04/74] add USDC perp client with tests --- src/index.ts | 1 + src/types/request/index.ts | 1 + src/types/request/usdc-perp.ts | 19 ++ src/types/shared.ts | 14 +- src/usdc-options-client.ts | 2 +- src/usdc-perpetual-client.ts | 287 ++++++++++++++++++++++ src/util/BaseRestClient.ts | 2 +- src/util/requestUtils.ts | 19 +- test/response.util.ts | 8 +- test/usdc/options/private.write.test.ts | 13 +- test/usdc/perpetual/private.read.test.ts | 74 ++++++ test/usdc/perpetual/private.write.test.ts | 85 +++++++ test/usdc/perpetual/public.read.test.ts | 95 +++++++ 13 files changed, 583 insertions(+), 37 deletions(-) create mode 100644 src/types/request/usdc-perp.ts create mode 100644 src/usdc-perpetual-client.ts create mode 100644 test/usdc/perpetual/private.read.test.ts create mode 100644 test/usdc/perpetual/private.write.test.ts create mode 100644 test/usdc/perpetual/public.read.test.ts diff --git a/src/index.ts b/src/index.ts index c7e61c8..7e4c856 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export * from './inverse-futures-client'; export * from './linear-client'; export * from './spot-client'; export * from './usdc-options-client'; +export * from './usdc-perpetual-client'; export * from './websocket-client'; export * from './logger'; export * from './types'; diff --git a/src/types/request/index.ts b/src/types/request/index.ts index bebf291..4194535 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,2 +1,3 @@ export * from './account-asset'; export * from './usdt-perp'; +export * from './usdc-perp'; diff --git a/src/types/request/usdc-perp.ts b/src/types/request/usdc-perp.ts new file mode 100644 index 0000000..dc2b209 --- /dev/null +++ b/src/types/request/usdc-perp.ts @@ -0,0 +1,19 @@ +export interface USDCKlineRequest { + symbol: string; + period: string; + startTime: number; + limit?: string; +} + +export interface USDCOpenInterestRequest { + symbol: string; + period: string; + limit?: number; +} + +export interface USDCLast500TradesRequest { + category: string; + symbol?: string; + baseCoin?: string; + limit?: string; +} diff --git a/src/types/shared.ts b/src/types/shared.ts index 23c0908..7312a58 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -25,6 +25,12 @@ export interface APIResponse { result: T; } +export interface USDCAPIResponse { + retCode: number; + retMsg: 'OK' | string; + result: T; +} + export interface APIResponseWithTime extends APIResponse { /** UTC timestamp */ time_now: numberInString; @@ -37,15 +43,15 @@ export interface SymbolParam { symbol: string; } -export interface SymbolLimitParam { +export interface SymbolLimitParam { symbol: string; - limit?: number; + limit?: TLimit; } -export interface SymbolPeriodLimitParam { +export interface SymbolPeriodLimitParam { symbol: string; period: string; - limit?: number; + limit?: TLimit; } export interface SymbolFromLimitParam { diff --git a/src/usdc-options-client.ts b/src/usdc-options-client.ts index 0bb3f9a..bfb190a 100644 --- a/src/usdc-options-client.ts +++ b/src/usdc-options-client.ts @@ -7,7 +7,7 @@ import BaseRestClient from './util/BaseRestClient'; */ export class USDCOptionsClient extends BaseRestClient { getClientType() { - return REST_CLIENT_TYPE_ENUM.usdcOptions; + return REST_CLIENT_TYPE_ENUM.usdc; } async fetchServerTime(): Promise { diff --git a/src/usdc-perpetual-client.ts b/src/usdc-perpetual-client.ts new file mode 100644 index 0000000..55b2743 --- /dev/null +++ b/src/usdc-perpetual-client.ts @@ -0,0 +1,287 @@ +import { + APIResponseWithTime, + SymbolLimitParam, + SymbolPeriodLimitParam, + USDCAPIResponse, + USDCKlineRequest, + USDCLast500TradesRequest, + USDCOpenInterestRequest, +} from './types'; +import { REST_CLIENT_TYPE_ENUM } from './util'; +import BaseRestClient from './util/BaseRestClient'; + +/** + * REST API client for USDC Perpetual APIs + */ +export class USDCPerpetualClient extends BaseRestClient { + getClientType() { + return REST_CLIENT_TYPE_ENUM.usdc; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Market Data Endpoints + * + */ + + getOrderBook(symbol: string): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/order-book', { symbol }); + } + + /** Fetch trading rules (such as min/max qty). Query for all if blank. */ + getContractInfo(params?: unknown): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/symbols', params); + } + + /** Get a symbol price/statistics ticker */ + getSymbolTicker(symbol: string): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/tick', { symbol }); + } + + getKline(params: USDCKlineRequest): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/kline/list', params); + } + + getMarkPrice(params: USDCKlineRequest): Promise> { + return this.get( + '/perpetual/usdc/openapi/public/v1/mark-price-kline', + params + ); + } + + getIndexPrice(params: USDCKlineRequest): Promise> { + return this.get( + '/perpetual/usdc/openapi/public/v1/index-price-kline', + params + ); + } + + getIndexPremium(params: USDCKlineRequest): Promise> { + return this.get( + '/perpetual/usdc/openapi/public/v1/premium-index-kline', + params + ); + } + + getOpenInterest( + params: USDCOpenInterestRequest + ): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/open-interest', params); + } + + getLargeOrders( + params: SymbolLimitParam + ): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/big-deal', params); + } + + getLongShortRatio( + params: SymbolPeriodLimitParam + ): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/account-ratio', params); + } + + getLast500Trades( + params: USDCLast500TradesRequest + ): Promise> { + return this.get( + '/option/usdc/openapi/public/v1/query-trade-latest', + params + ); + } + + /** + * + * Account Data Endpoints + * + */ + + /** -> Order API */ + + /** + * Place an order using the USDC Derivatives Account. + * The request status can be queried in real-time. + * The response parameters must be queried through a query or a WebSocket response. + */ + submitOrder(params: unknown): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/place-order', + params + ); + } + + /** Active order parameters (such as quantity, price) and stop order parameters cannot be modified in one request at the same time. Please request modification separately. */ + modifyOrder(params: unknown): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/replace-order', + params + ); + } + + /** Cancel order */ + cancelOrder(params: unknown): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/cancel-order', + params + ); + } + + /** Cancel all active orders. The real-time response indicates whether the request is successful, depending on retCode. */ + cancelActiveOrders( + symbol: string, + orderFilter: string + ): Promise> { + return this.postPrivate('/perpetual/usdc/openapi/private/v1/cancel-all', { + symbol, + orderFilter, + }); + } + + /** Query Unfilled/Partially Filled Orders */ + getActiveOrders(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-active-orders', + params + ); + } + + /** Query order history. The endpoint only supports up to 30 days of queried records */ + getHistoricOrders(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-order-history', + params + ); + } + + /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ + getOrderExecutionHistory(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/execution-list', + params + ); + } + + /** -> Account API */ + + /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ + getTransactionLog(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-transaction-log', + params + ); + } + + /** Wallet info for USDC account. */ + getBalance(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-wallet-balance', + params + ); + } + + /** Asset Info */ + getAssetInfo(baseCoin?: string): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-asset-info', + { baseCoin } + ); + } + + /** + * If USDC derivatives account balance is greater than X, you can open PORTFOLIO_MARGIN, and if it is less than Y, it will automatically close PORTFOLIO_MARGIN and change back to REGULAR_MARGIN. X and Y will be adjusted according to operational requirements. + * Rest API returns the result of checking prerequisites. You could get the real status of margin mode change by subscribing margin mode. + */ + setMarginMode( + newMarginMode: 'REGULAR_MARGIN' | 'PORTFOLIO_MARGIN' + ): Promise> { + return this.postPrivate( + '/option/usdc/private/asset/account/setMarginMode', + { setMarginMode: newMarginMode } + ); + } + + /** Query margin mode for USDC account. */ + getMarginMode(): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-margin-info' + ); + } + + /** -> Positions API */ + + /** Query my positions */ + getPositions(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-position', + params + ); + } + + /** Only for REGULAR_MARGIN */ + setLeverage(symbol: string, leverage: string): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/position/leverage/save', + { symbol, leverage } + ); + } + + /** Query Settlement History */ + getSettlementHistory(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/session-settlement', + params + ); + } + + /** -> Risk Limit API */ + + /** Query risk limit */ + getRiskLimit(symbol: string): Promise> { + return this.getPrivate( + '/perpetual/usdc/openapi/public/v1/risk-limit/list', + { + symbol, + } + ); + } + + /** Set risk limit */ + setRiskLimit(symbol: string, riskId: number): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/position/set-risk-limit', + { symbol, riskId } + ); + } + + /** -> Funding API */ + + /** Funding settlement occurs every 8 hours at 00:00 UTC, 08:00 UTC and 16:00 UTC. The current interval's fund fee settlement is based on the previous interval's fund rate. For example, at 16:00, the settlement is based on the fund rate generated at 8:00. The fund rate generated at 16:00 will be used at 0:00 the next day. */ + getLastFundingRate(symbol: string): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/prev-funding-rate', { + symbol, + }); + } + + /** Get predicted funding rate and my predicted funding fee */ + getPredictedFundingRate(symbol: string): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/predicted-funding', + { symbol } + ); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } +} diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 850cbc5..eb4dee5 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -124,7 +124,7 @@ export default abstract class BaseRestClient { } private isUSDCClient() { - return this.clientType === REST_CLIENT_TYPE_ENUM.usdcOptions; + return this.clientType === REST_CLIENT_TYPE_ENUM.usdc; } get(endpoint: string, params?: any) { diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index 610f074..9a79db6 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -61,23 +61,6 @@ export function getRestBaseUrl( return exchangeBaseUrls.testnet; } -export function isPublicEndpoint(endpoint: string): boolean { - const publicPrefixes = [ - 'v2/public', - 'public/linear', - 'spot/quote/v1', - 'spot/v1/symbols', - 'spot/v1/time', - ]; - - for (const prefix of publicPrefixes) { - if (endpoint.startsWith(prefix)) { - return true; - } - } - return false; -} - export function isWsPong(response: any) { if (response.pong || response.ping) { return true; @@ -101,7 +84,7 @@ export const REST_CLIENT_TYPE_ENUM = { inverseFutures: 'inverseFutures', linear: 'linear', spot: 'spot', - usdcOptions: 'usdcOptions', + usdc: 'usdc', } as const; export type RestClientType = diff --git a/test/response.util.ts b/test/response.util.ts index 5d6ba7d..18b3010 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -19,9 +19,15 @@ export function successResponseObject(successMsg: string | null = 'OK') { export function successUSDCResponseObject() { return { result: expect.any(Object), + ...successUSDCEmptyResponseObject(), + }; +} + +export function successUSDCEmptyResponseObject() { + return { retCode: API_ERROR_CODE.SUCCESS, retMsg: expect.stringMatching( - /OK|SUCCESS|success|success\.|Request accepted/gim + /OK|SUCCESS|success|success\.|Request accepted|/gim ), }; } diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts index ed172fd..d04e9f7 100644 --- a/test/usdc/options/private.write.test.ts +++ b/test/usdc/options/private.write.test.ts @@ -1,8 +1,5 @@ import { API_ERROR_CODE, USDCOptionsClient } from '../../../src'; -import { - successResponseObject, - successUSDCResponseObject, -} from '../../response.util'; +import { successUSDCResponseObject } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { const useLivenet = true; @@ -16,7 +13,6 @@ describe('Private Account Asset REST API Endpoints', () => { const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); - const category = 'OPTION'; const currency = 'USDC'; const symbol = 'BTC-30SEP22-400000-C'; @@ -159,11 +155,4 @@ describe('Private Account Asset REST API Endpoints', () => { retCode: API_ERROR_CODE.INSTITION_MMP_PROFILE_NOT_FOUND, }); }); - - /** - - it('asdfasfasdfasdf()', async () => { - expect(await api.asadfasdfasdfasf()).toStrictEqual(''); - }); - */ }); diff --git a/test/usdc/perpetual/private.read.test.ts b/test/usdc/perpetual/private.read.test.ts new file mode 100644 index 0000000..02f8a5c --- /dev/null +++ b/test/usdc/perpetual/private.read.test.ts @@ -0,0 +1,74 @@ +import { USDCPerpetualClient } from '../../../src'; +import { successUSDCResponseObject } from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + const category = 'PERPETUAL'; + + it('getActiveOrders()', async () => { + expect(await api.getActiveOrders({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getHistoricOrders()', async () => { + expect(await api.getHistoricOrders({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getOrderExecutionHistory()', async () => { + expect(await api.getOrderExecutionHistory({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getTransactionLog()', async () => { + expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getBalance()', async () => { + expect(await api.getBalance()).toMatchObject(successUSDCResponseObject()); + }); + + it('getAssetInfo()', async () => { + expect(await api.getAssetInfo()).toMatchObject(successUSDCResponseObject()); + }); + + it('getMarginMode()', async () => { + expect(await api.getMarginMode()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getPositions()', async () => { + expect(await api.getPositions({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getSettlementHistory()', async () => { + expect(await api.getSettlementHistory({ symbol })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getPredictedFundingRate()', async () => { + expect(await api.getPredictedFundingRate(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); +}); diff --git a/test/usdc/perpetual/private.write.test.ts b/test/usdc/perpetual/private.write.test.ts new file mode 100644 index 0000000..f2c11da --- /dev/null +++ b/test/usdc/perpetual/private.write.test.ts @@ -0,0 +1,85 @@ +import { API_ERROR_CODE, USDCPerpetualClient } from '../../../src'; +import { + successUSDCEmptyResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + + it('submitOrder()', async () => { + expect( + await api.submitOrder({ + symbol, + side: 'Sell', + orderType: 'Limit', + orderFilter: 'Order', + orderQty: '1', + orderPrice: '20000', + orderLinkId: Date.now().toString(), + timeInForce: 'GoodTillCancel', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST, + }); + }); + + it('modifyOrder()', async () => { + expect( + await api.modifyOrder({ + symbol, + orderId: 'somethingFake', + orderPrice: '20000', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + }); + }); + + it('cancelOrder()', async () => { + expect( + await api.cancelOrder({ + symbol, + orderId: 'somethingFake1', + orderFilter: 'Order', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + }); + }); + + it('cancelActiveOrders()', async () => { + expect(await api.cancelActiveOrders(symbol, 'Order')).toMatchObject( + successUSDCEmptyResponseObject() + ); + }); + + it('setMarginMode()', async () => { + expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('setLeverage()', async () => { + expect(await api.setLeverage(symbol, '10')).toMatchObject({ + retCode: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED, + }); + }); + + it('setRiskLimit()', async () => { + expect(await api.setRiskLimit(symbol, 1)).toMatchObject({ + retCode: API_ERROR_CODE.RISK_LIMIT_NOT_EXISTS, + }); + }); +}); diff --git a/test/usdc/perpetual/public.read.test.ts b/test/usdc/perpetual/public.read.test.ts new file mode 100644 index 0000000..4f90a62 --- /dev/null +++ b/test/usdc/perpetual/public.read.test.ts @@ -0,0 +1,95 @@ +import { USDCKlineRequest, USDCPerpetualClient } from '../../../src'; +import { + successResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Public USDC Options REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = undefined; + const API_SECRET = undefined; + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + const category = 'PERPETUAL'; + const startTime = Number((Date.now() / 1000).toFixed(0)); + + const candleRequest: USDCKlineRequest = { symbol, period: '1m', startTime }; + + it('getOrderBook()', async () => { + expect(await api.getOrderBook(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getContractInfo()', async () => { + expect(await api.getContractInfo()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getSymbolTicker()', async () => { + expect(await api.getSymbolTicker(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getKline()', async () => { + expect(await api.getKline(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getMarkPrice()', async () => { + expect(await api.getMarkPrice(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getIndexPrice()', async () => { + expect(await api.getIndexPrice(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getIndexPremium()', async () => { + expect(await api.getIndexPremium(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getOpenInterest()', async () => { + expect(await api.getOpenInterest({ symbol, period: '1m' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLargeOrders()', async () => { + expect(await api.getLargeOrders({ symbol })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLongShortRatio()', async () => { + expect(await api.getLongShortRatio({ symbol, period: '1m' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLast500Trades()', async () => { + expect(await api.getLast500Trades({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLastFundingRate()', async () => { + expect(await api.getLastFundingRate(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getServerTime()', async () => { + expect(await api.getServerTime()).toMatchObject(successResponseObject()); + }); +}); From 557ddc90f5f4ed05c0a2dfdf47f17feef6089b44 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 18:24:18 +0100 Subject: [PATCH 05/74] USDC request param typings --- src/types/request/index.ts | 2 + src/types/request/usdc-options.ts | 135 ++++++++++++++++++++++ src/types/request/usdc-perp.ts | 92 +++++++++++++-- src/types/request/usdc-shared.ts | 36 ++++++ src/usdc-options-client.ts | 117 +++++++++++++------ src/usdc-perpetual-client.ts | 53 ++++++--- test/usdc/options/public.read.test.ts | 4 +- test/usdc/perpetual/private.write.test.ts | 1 + 8 files changed, 382 insertions(+), 58 deletions(-) create mode 100644 src/types/request/usdc-options.ts create mode 100644 src/types/request/usdc-shared.ts diff --git a/src/types/request/index.ts b/src/types/request/index.ts index 4194535..2b797e3 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,3 +1,5 @@ export * from './account-asset'; export * from './usdt-perp'; export * from './usdc-perp'; +export * from './usdc-options'; +export * from './usdc-shared'; diff --git a/src/types/request/usdc-options.ts b/src/types/request/usdc-options.ts new file mode 100644 index 0000000..0ba1971 --- /dev/null +++ b/src/types/request/usdc-options.ts @@ -0,0 +1,135 @@ +import { OrderSide } from '../shared'; +import { USDCOrderFilter } from './usdc-perp'; +import { USDCAPICategory, USDCOrderType, USDCTimeInForce } from './usdc-shared'; + +export interface USDCOptionsContractInfoRequest { + symbol?: string; + status?: 'WAITING_ONLINE' | 'ONLINE' | 'DELIVERING' | 'OFFLINE'; + baseCoin?: string; + direction?: string; + limit?: string; + cursor?: string; +} + +export interface USDCOptionsDeliveryPriceRequest { + symbol?: string; + baseCoin?: string; + direction?: string; + limit?: string; + cursor?: string; +} + +export interface USDCOptionsRecentTradesRequest { + category: USDCAPICategory; + symbol?: string; + baseCoin?: string; + optionType?: 'Call' | 'Put'; + limit?: string; +} + +export interface USDCOptionsHistoricalVolatilityRequest { + baseCoin?: string; + period?: string; + startTime?: string; + endTime?: string; +} + +export interface USDCOptionsOrderRequest { + symbol: string; + orderType: USDCOrderType; + side: OrderSide; + orderPrice?: string; + orderQty: string; + iv?: string; + timeInForce?: USDCTimeInForce; + orderLinkId?: string; + reduceOnly?: boolean; +} + +export interface USDCOptionsModifyOrderRequest { + symbol: string; + orderId?: string; + orderLinkId?: string; + orderPrice?: string; + orderQty?: string; + iv?: string; +} + +export interface USDCOptionsCancelOrderRequest { + symbol: string; + orderId?: string; + orderLinkId?: string; +} + +export interface USDCOptionsCancelAllOrdersRequest { + symbol?: string; + baseCoin?: string; +} + +export interface USDCOptionsActiveOrdersRealtimeRequest { + orderId?: string; + orderLinkId?: string; + symbol?: string; + baseCoin?: string; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface USDCOptionsActiveOrdersRequest { + category: 'OPTION'; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface USDCOptionsHistoricOrdersRequest { + category: 'OPTION'; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + orderStatus?: string; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface USDCOptionsOrderExecutionRequest { + category: 'OPTION'; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + startTime?: string; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface USDCOptionsDeliveryHistoryRequest { + symbol: string; + expDate?: string; + direction?: string; + limit?: string; + cursor?: string; +} + +export interface USDCOptionsPositionsInfoExpiryRequest { + expDate?: string; + direction?: string; + limit?: string; + cursor?: string; +} + +export interface USDCOptionsModifyMMPRequest { + currency: string; + windowMs: number; + frozenPeriodMs: number; + qtyLimit: string; + deltaLimit: string; +} diff --git a/src/types/request/usdc-perp.ts b/src/types/request/usdc-perp.ts index dc2b209..6f77789 100644 --- a/src/types/request/usdc-perp.ts +++ b/src/types/request/usdc-perp.ts @@ -1,9 +1,5 @@ -export interface USDCKlineRequest { - symbol: string; - period: string; - startTime: number; - limit?: string; -} +import { OrderSide } from '../shared'; +import { USDCAPICategory, USDCOrderType, USDCTimeInForce } from './usdc-shared'; export interface USDCOpenInterestRequest { symbol: string; @@ -12,8 +8,90 @@ export interface USDCOpenInterestRequest { } export interface USDCLast500TradesRequest { - category: string; + category: USDCAPICategory; symbol?: string; baseCoin?: string; limit?: string; } + +export interface USDCSymbolDirectionLimit { + symbol?: string; + direction?: string; + limit?: string; +} + +export interface USDCSymbolDirectionLimitCursor { + symbol?: string; + direction?: string; + limit?: string; + cursor?: string; +} + +export type USDCOrderFilter = 'Order' | 'StopOrder'; + +export interface USDCPerpOrderRequest { + symbol: string; + orderType: USDCOrderType; + orderFilter: USDCOrderFilter; + side: OrderSide; + orderPrice?: string; + orderQty: string; + timeInForce?: USDCTimeInForce; + orderLinkId?: string; + reduceOnly?: boolean; + closeOnTrigger?: boolean; + takeProfit?: string; + stopLoss?: string; + tptriggerby?: string; + slTriggerBy?: string; + basePrice?: string; + triggerPrice?: string; + triggerBy?: string; + mmp?: boolean; +} + +export interface USDCPerpModifyOrderRequest { + symbol: string; + orderFilter: USDCOrderFilter; + orderId?: string; + orderLinkId?: string; + orderPrice?: string; + orderQty?: string; + takeProfit?: string; + stopLoss?: string; + tptriggerby?: string; + slTriggerBy?: string; + triggerPrice?: string; +} + +export interface USDCPerpCancelOrderRequest { + symbol: string; + orderFilter: USDCOrderFilter; + orderId?: string; + orderLinkId?: string; +} + +export interface USDCPerpActiveOrdersRequest { + category: 'PERPETUAL'; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + orderFilter?: USDCOrderFilter; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface USDCPerpHistoricOrdersRequest { + category: 'PERPETUAL'; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + orderStatus?: string; + orderFilter?: USDCOrderFilter; + direction?: string; + limit?: number; + cursor?: string; +} diff --git a/src/types/request/usdc-shared.ts b/src/types/request/usdc-shared.ts new file mode 100644 index 0000000..f8faf59 --- /dev/null +++ b/src/types/request/usdc-shared.ts @@ -0,0 +1,36 @@ +export type USDCAPICategory = 'PERPETUAL' | 'OPTION'; + +export type USDCOrderType = 'Limit' | 'Market'; +export type USDCTimeInForce = + | 'GoodTillCancel' + | 'ImmediateOrCancel' + | 'FillOrKill' + | 'PostOnly'; + +export interface USDCKlineRequest { + symbol: string; + period: string; + startTime: number; + limit?: string; +} + +export interface USDCTransactionLogRequest { + type: string; + baseCoin?: string; + startTime?: string; + endTime?: string; + direction?: string; + limit?: string; + cursor?: string; + category?: USDCAPICategory; +} + +export interface USDCPositionsRequest { + category: USDCAPICategory; + symbol?: string; + baseCoin?: string; + expDate?: string; + direction?: string; + limit?: string; + cursor?: string; +} diff --git a/src/usdc-options-client.ts b/src/usdc-options-client.ts index bfb190a..efddb67 100644 --- a/src/usdc-options-client.ts +++ b/src/usdc-options-client.ts @@ -1,4 +1,24 @@ -import { APIResponseWithTime } from './types'; +import { + APIResponseWithTime, + USDCAPIResponse, + USDCOptionsActiveOrdersRealtimeRequest, + USDCOptionsActiveOrdersRequest, + USDCOptionsCancelAllOrdersRequest, + USDCOptionsCancelOrderRequest, + USDCOptionsContractInfoRequest, + USDCOptionsDeliveryHistoryRequest, + USDCOptionsDeliveryPriceRequest, + USDCOptionsHistoricalVolatilityRequest, + USDCOptionsHistoricOrdersRequest, + USDCOptionsModifyMMPRequest, + USDCOptionsModifyOrderRequest, + USDCOptionsOrderExecutionRequest, + USDCOptionsOrderRequest, + USDCOptionsPositionsInfoExpiryRequest, + USDCOptionsRecentTradesRequest, + USDCPositionsRequest, + USDCTransactionLogRequest, +} from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -22,27 +42,33 @@ export class USDCOptionsClient extends BaseRestClient { */ /** Query order book info. Each side has a depth of 25 orders. */ - getOrderBook(symbol: string): Promise> { + getOrderBook(symbol: string): Promise> { return this.get('/option/usdc/openapi/public/v1/order-book', { symbol }); } /** Fetch trading rules (such as min/max qty). Query for all if blank. */ - getContractInfo(params?: unknown): Promise> { + getContractInfo( + params?: USDCOptionsContractInfoRequest + ): Promise> { return this.get('/option/usdc/openapi/public/v1/symbols', params); } /** Get a symbol price/statistics ticker */ - getSymbolTicker(symbol: string): Promise> { + getSymbolTicker(symbol: string): Promise> { return this.get('/option/usdc/openapi/public/v1/tick', { symbol }); } /** Get delivery information */ - getDeliveryPrice(params?: unknown): Promise> { + getDeliveryPrice( + params?: USDCOptionsDeliveryPriceRequest + ): Promise> { return this.get('/option/usdc/openapi/public/v1/delivery-price', params); } /** Returned records are Taker Buy in default. */ - getLastTrades(params: unknown): Promise> { + getLast500Trades( + params: USDCOptionsRecentTradesRequest + ): Promise> { return this.get( '/option/usdc/openapi/public/v1/query-trade-latest', params @@ -56,7 +82,9 @@ export class USDCOptionsClient extends BaseRestClient { * It returns all data in 2 years when startTime & endTime are not passed. * Both startTime & endTime entered together or both are left blank */ - getHistoricalVolatility(params?: unknown): Promise> { + getHistoricalVolatility( + params?: USDCOptionsHistoricalVolatilityRequest + ): Promise> { return this.get( '/option/usdc/openapi/public/v1/query-historical-volatility', params @@ -76,7 +104,7 @@ export class USDCOptionsClient extends BaseRestClient { * The request status can be queried in real-time. * The response parameters must be queried through a query or a WebSocket response. */ - submitOrder(params: unknown): Promise> { + submitOrder(params: USDCOptionsOrderRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/place-order', params @@ -87,8 +115,8 @@ export class USDCOptionsClient extends BaseRestClient { * Each request supports a max. of four orders. The reduceOnly parameter should be separate and unique for each order in the request. */ batchSubmitOrders( - orderRequest: unknown[] - ): Promise> { + orderRequest: USDCOptionsOrderRequest[] + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/batch-place-orders', { orderRequest } @@ -96,7 +124,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** For Options, at least one of the three parameters — price, quantity or implied volatility — must be input. */ - modifyOrder(params: unknown): Promise> { + modifyOrder( + params: USDCOptionsModifyOrderRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/replace-order', params @@ -105,8 +135,8 @@ export class USDCOptionsClient extends BaseRestClient { /** Each request supports a max. of four orders. The reduceOnly parameter should be separate and unique for each order in the request. */ batchModifyOrders( - replaceOrderRequest: unknown[] - ): Promise> { + replaceOrderRequest: USDCOptionsModifyOrderRequest[] + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/batch-replace-orders', { replaceOrderRequest } @@ -114,7 +144,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** Cancel order */ - cancelOrder(params: unknown): Promise> { + cancelOrder( + params: USDCOptionsCancelOrderRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/cancel-order', params @@ -123,8 +155,8 @@ export class USDCOptionsClient extends BaseRestClient { /** Batch cancel orders */ batchCancelOrders( - cancelRequest: unknown[] - ): Promise> { + cancelRequest: USDCOptionsCancelOrderRequest[] + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/batch-cancel-orders', { cancelRequest } @@ -132,7 +164,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** This is used to cancel all active orders. The real-time response indicates whether the request is successful, depending on retCode. */ - cancelActiveOrders(params?: unknown): Promise> { + cancelActiveOrders( + params?: USDCOptionsCancelAllOrdersRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/cancel-all', params @@ -140,7 +174,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** Query Unfilled/Partially Filled Orders(real-time), up to last 7 days of partially filled/unfilled orders */ - getActiveRealtimeOrders(params?: unknown): Promise> { + getActiveRealtimeOrders( + params?: USDCOptionsActiveOrdersRealtimeRequest + ): Promise> { return this.getPrivate( '/option/usdc/openapi/private/v1/trade/query-active-orders', params @@ -148,7 +184,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** Query Unfilled/Partially Filled Orders */ - getActiveOrders(params: unknown): Promise> { + getActiveOrders( + params: USDCOptionsActiveOrdersRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-active-orders', params @@ -156,7 +194,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** Query order history. The endpoint only supports up to 30 days of queried records */ - getHistoricOrders(params: unknown): Promise> { + getHistoricOrders( + params: USDCOptionsHistoricOrdersRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-order-history', params @@ -164,7 +204,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ - getOrderExecutionHistory(params: unknown): Promise> { + getOrderExecutionHistory( + params: USDCOptionsOrderExecutionRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/execution-list', params @@ -174,7 +216,9 @@ export class USDCOptionsClient extends BaseRestClient { /** -> Account API */ /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ - getTransactionLog(params: unknown): Promise> { + getTransactionLog( + params: USDCTransactionLogRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-transaction-log', params @@ -182,15 +226,14 @@ export class USDCOptionsClient extends BaseRestClient { } /** Wallet info for USDC account. */ - getBalance(params?: unknown): Promise> { + getBalance(): Promise> { return this.postPrivate( - '/option/usdc/openapi/private/v1/query-wallet-balance', - params + '/option/usdc/openapi/private/v1/query-wallet-balance' ); } /** Asset Info */ - getAssetInfo(baseCoin?: string): Promise> { + getAssetInfo(baseCoin?: string): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-asset-info', { baseCoin } @@ -203,7 +246,7 @@ export class USDCOptionsClient extends BaseRestClient { */ setMarginMode( newMarginMode: 'REGULAR_MARGIN' | 'PORTFOLIO_MARGIN' - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/private/asset/account/setMarginMode', { setMarginMode: newMarginMode } @@ -211,7 +254,7 @@ export class USDCOptionsClient extends BaseRestClient { } /** Query margin mode for USDC account. */ - getMarginMode(): Promise> { + getMarginMode(): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-margin-info' ); @@ -220,7 +263,7 @@ export class USDCOptionsClient extends BaseRestClient { /** -> Positions API */ /** Query my positions */ - getPositions(params: unknown): Promise> { + getPositions(params: USDCPositionsRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-position', params @@ -228,7 +271,9 @@ export class USDCOptionsClient extends BaseRestClient { } /** Query Delivery History */ - getDeliveryHistory(params: unknown): Promise> { + getDeliveryHistory( + params: USDCOptionsDeliveryHistoryRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-delivery-list', params @@ -237,8 +282,8 @@ export class USDCOptionsClient extends BaseRestClient { /** Query Positions Info Upon Expiry */ getPositionsInfoUponExpiry( - params?: unknown - ): Promise> { + params?: USDCOptionsPositionsInfoExpiryRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-position-exp-date', params @@ -248,7 +293,9 @@ export class USDCOptionsClient extends BaseRestClient { /** -> Market Maker Protection */ /** modifyMMP */ - modifyMMP(params?: unknown): Promise> { + modifyMMP( + params: USDCOptionsModifyMMPRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/mmp-modify', params @@ -256,14 +303,14 @@ export class USDCOptionsClient extends BaseRestClient { } /** resetMMP */ - resetMMP(currency: string): Promise> { + resetMMP(currency: string): Promise> { return this.postPrivate('/option/usdc/openapi/private/v1/mmp-reset', { currency, }); } /** queryMMPState */ - queryMMPState(baseCoin: string): Promise> { + queryMMPState(baseCoin: string): Promise> { return this.postPrivate('/option/usdc/openapi/private/v1/get-mmp-state', { baseCoin, }); diff --git a/src/usdc-perpetual-client.ts b/src/usdc-perpetual-client.ts index 55b2743..9c22d89 100644 --- a/src/usdc-perpetual-client.ts +++ b/src/usdc-perpetual-client.ts @@ -6,6 +6,16 @@ import { USDCKlineRequest, USDCLast500TradesRequest, USDCOpenInterestRequest, + USDCOrderFilter, + USDCPerpActiveOrdersRequest, + USDCPerpCancelOrderRequest, + USDCPerpHistoricOrdersRequest, + USDCPerpModifyOrderRequest, + USDCPerpOrderRequest, + USDCPositionsRequest, + USDCSymbolDirectionLimit, + USDCSymbolDirectionLimitCursor, + USDCTransactionLogRequest, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -34,7 +44,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Fetch trading rules (such as min/max qty). Query for all if blank. */ - getContractInfo(params?: unknown): Promise> { + getContractInfo( + params?: USDCSymbolDirectionLimit + ): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/symbols', params); } @@ -108,7 +120,7 @@ export class USDCPerpetualClient extends BaseRestClient { * The request status can be queried in real-time. * The response parameters must be queried through a query or a WebSocket response. */ - submitOrder(params: unknown): Promise> { + submitOrder(params: USDCPerpOrderRequest): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/place-order', params @@ -116,7 +128,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Active order parameters (such as quantity, price) and stop order parameters cannot be modified in one request at the same time. Please request modification separately. */ - modifyOrder(params: unknown): Promise> { + modifyOrder( + params: USDCPerpModifyOrderRequest + ): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/replace-order', params @@ -124,7 +138,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Cancel order */ - cancelOrder(params: unknown): Promise> { + cancelOrder( + params: USDCPerpCancelOrderRequest + ): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/cancel-order', params @@ -134,7 +150,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** Cancel all active orders. The real-time response indicates whether the request is successful, depending on retCode. */ cancelActiveOrders( symbol: string, - orderFilter: string + orderFilter: USDCOrderFilter ): Promise> { return this.postPrivate('/perpetual/usdc/openapi/private/v1/cancel-all', { symbol, @@ -143,7 +159,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Query Unfilled/Partially Filled Orders */ - getActiveOrders(params: unknown): Promise> { + getActiveOrders( + params: USDCPerpActiveOrdersRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-active-orders', params @@ -151,7 +169,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Query order history. The endpoint only supports up to 30 days of queried records */ - getHistoricOrders(params: unknown): Promise> { + getHistoricOrders( + params: USDCPerpHistoricOrdersRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-order-history', params @@ -159,7 +179,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ - getOrderExecutionHistory(params: unknown): Promise> { + getOrderExecutionHistory( + params: USDCPerpActiveOrdersRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/execution-list', params @@ -169,7 +191,9 @@ export class USDCPerpetualClient extends BaseRestClient { /** -> Account API */ /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ - getTransactionLog(params: unknown): Promise> { + getTransactionLog( + params: USDCTransactionLogRequest + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-transaction-log', params @@ -177,10 +201,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Wallet info for USDC account. */ - getBalance(params?: unknown): Promise> { + getBalance(): Promise> { return this.postPrivate( - '/option/usdc/openapi/private/v1/query-wallet-balance', - params + '/option/usdc/openapi/private/v1/query-wallet-balance' ); } @@ -215,7 +238,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** -> Positions API */ /** Query my positions */ - getPositions(params: unknown): Promise> { + getPositions(params: USDCPositionsRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-position', params @@ -231,7 +254,9 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Query Settlement History */ - getSettlementHistory(params?: unknown): Promise> { + getSettlementHistory( + params?: USDCSymbolDirectionLimitCursor + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/session-settlement', params diff --git a/test/usdc/options/public.read.test.ts b/test/usdc/options/public.read.test.ts index c09754b..043e739 100644 --- a/test/usdc/options/public.read.test.ts +++ b/test/usdc/options/public.read.test.ts @@ -36,8 +36,8 @@ describe('Public USDC Options REST API Endpoints', () => { ); }); - it('getLastTrades()', async () => { - expect(await api.getLastTrades({ category: 'OPTION' })).toMatchObject( + it('getLast500Trades()', async () => { + expect(await api.getLast500Trades({ category: 'OPTION' })).toMatchObject( successUSDCResponseObject() ); }); diff --git a/test/usdc/perpetual/private.write.test.ts b/test/usdc/perpetual/private.write.test.ts index f2c11da..ced7af4 100644 --- a/test/usdc/perpetual/private.write.test.ts +++ b/test/usdc/perpetual/private.write.test.ts @@ -41,6 +41,7 @@ describe('Private Account Asset REST API Endpoints', () => { symbol, orderId: 'somethingFake', orderPrice: '20000', + orderFilter: 'Order', }) ).toMatchObject({ retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, From c100c4af6fa703df95def556a081295fbd058e0b Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 18:34:49 +0100 Subject: [PATCH 06/74] rename option client. update docs --- README.md | 23 +++++++++++++---------- src/usdc-options-client.ts | 4 ++-- test/usdc/options/private.read.test.ts | 4 ++-- test/usdc/options/private.write.test.ts | 4 ++-- test/usdc/options/public.read.test.ts | 4 ++-- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index afa49d0..7474367 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,19 @@ Most methods accept JS objects. These can be populated using parameters specifie ## REST Clients Each REST API category has a dedicated REST client. Here are the REST clients and their API group: -| Class | Description | -|:-----------------------------------------------------: |:-----------------------------------------------------------------------------------------------------: | -| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2)](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | -| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2)](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | -| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2)](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | -| [SpotClient](src/spot-client.ts) | [Spot Markets](https://bybit-exchange.github.io/docs/spot/#t-introduction) | -| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset API](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | -| USDC Options & Perpetual Contracts | Under Development | -| Derivatives V3 unified margin | Under Development | -| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | +| Class | Description | +|:-----------------------------------------------------: |:-----------------------------------------------------------------------------------------------------------: | +| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | +| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | +| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | +| [SpotClient](src/spot-client.ts) | [Spot Market APIs](https://bybit-exchange.github.io/docs/spot/#t-introduction) | +| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | +| [USDC Perpetual](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | +| [USDC Option](src/usdc-options-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | +| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | +| Spot v3 | Under Development | +| Derivatives V3 unified margin | Under Development | +| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | Examples for using each client can be found in the [examples](./examples) folder and the [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples) repository. diff --git a/src/usdc-options-client.ts b/src/usdc-options-client.ts index efddb67..252d4d2 100644 --- a/src/usdc-options-client.ts +++ b/src/usdc-options-client.ts @@ -23,9 +23,9 @@ import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; /** - * REST API client for USDC Options APIs + * REST API client for USDC Option APIs */ -export class USDCOptionsClient extends BaseRestClient { +export class USDCOptionClient extends BaseRestClient { getClientType() { return REST_CLIENT_TYPE_ENUM.usdc; } diff --git a/test/usdc/options/private.read.test.ts b/test/usdc/options/private.read.test.ts index f0d477b..c682a55 100644 --- a/test/usdc/options/private.read.test.ts +++ b/test/usdc/options/private.read.test.ts @@ -1,4 +1,4 @@ -import { USDCOptionsClient } from '../../../src'; +import { USDCOptionClient } from '../../../src'; import { successResponseObject, successUSDCResponseObject, @@ -15,7 +15,7 @@ describe('Private Account Asset REST API Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCOptionClient(API_KEY, API_SECRET, useLivenet); const category = 'OPTION'; it('getActiveRealtimeOrders()', async () => { diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts index d04e9f7..2949d0e 100644 --- a/test/usdc/options/private.write.test.ts +++ b/test/usdc/options/private.write.test.ts @@ -1,4 +1,4 @@ -import { API_ERROR_CODE, USDCOptionsClient } from '../../../src'; +import { API_ERROR_CODE, USDCOptionClient } from '../../../src'; import { successUSDCResponseObject } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { @@ -11,7 +11,7 @@ describe('Private Account Asset REST API Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCOptionClient(API_KEY, API_SECRET, useLivenet); const currency = 'USDC'; const symbol = 'BTC-30SEP22-400000-C'; diff --git a/test/usdc/options/public.read.test.ts b/test/usdc/options/public.read.test.ts index 043e739..97ff6bd 100644 --- a/test/usdc/options/public.read.test.ts +++ b/test/usdc/options/public.read.test.ts @@ -1,4 +1,4 @@ -import { USDCOptionsClient } from '../../../src'; +import { USDCOptionClient } from '../../../src'; import { successResponseObject, successUSDCResponseObject, @@ -9,7 +9,7 @@ describe('Public USDC Options REST API Endpoints', () => { const API_KEY = undefined; const API_SECRET = undefined; - const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCOptionClient(API_KEY, API_SECRET, useLivenet); const symbol = 'BTC-30SEP22-400000-C'; it('getOrderBook()', async () => { From 0500ee2d99d5d0cacad76d0c6855c787a83373fe Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 18:46:40 +0100 Subject: [PATCH 07/74] readme cleanup --- README.md | 202 ++++-------------- src/index.ts | 2 +- ...ptions-client.ts => usdc-option-client.ts} | 0 3 files changed, 42 insertions(+), 162 deletions(-) rename src/{usdc-options-client.ts => usdc-option-client.ts} (100%) diff --git a/README.md b/README.md index 7474367..4e00e99 100644 --- a/README.md +++ b/README.md @@ -34,28 +34,8 @@ Check out my related projects: - [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples) ## Documentation -Most methods accept JS objects. These can be populated using parameters specified by Bybit's API documentation. -- [Bybit API Inverse Documentation](https://bybit-exchange.github.io/docs/inverse/#t-introduction). -- [Bybit API Inverse Futures Documentation](https://bybit-exchange.github.io/docs/inverse_futures/#t-introduction). -- [Bybit API Linear Documentation](https://bybit-exchange.github.io/docs/linear/#t-introduction) - -## REST Clients -Each REST API category has a dedicated REST client. Here are the REST clients and their API group: -| Class | Description | -|:-----------------------------------------------------: |:-----------------------------------------------------------------------------------------------------------: | -| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | -| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | -| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | -| [SpotClient](src/spot-client.ts) | [Spot Market APIs](https://bybit-exchange.github.io/docs/spot/#t-introduction) | -| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | -| [USDC Perpetual](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | -| [USDC Option](src/usdc-options-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | -| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | -| Spot v3 | Under Development | -| Derivatives V3 unified margin | Under Development | -| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | - -Examples for using each client can be found in the [examples](./examples) folder and the [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples) repository. +Most methods accept JS objects. These can be populated using parameters specified by Bybit's API documentation, or check the type definition in each class within this repository (see table below for convenient links to each class). +- [Bybit API Docs (choose API category from the tabs at the top)](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-introduction). ## Structure The connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bybit-api). This connector is fully compatible with both TypeScript and pure JavaScript projects. @@ -66,19 +46,45 @@ The connector is written in TypeScript. A pure JavaScript version can be built u - [examples](./examples) - some implementation examples & demonstrations. Contributions are welcome! --- +## REST API Clients +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 | +|:-----------------------------------------------------: |:-----------------------------------------------------------------------------------------------------------: | +| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | +| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | +| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | +| [SpotClient](src/spot-client.ts) | [Spot Market APIs](https://bybit-exchange.github.io/docs/spot/#t-introduction) | +| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | +| [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | +| [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | +| Copy Trading | Under Development | +| Spot v3 | Under Development | +| Derivatives V3 unified margin | Under Development | +| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | -## Usage -Create API credentials at Bybit +Examples for using each client can be found in: +- the [examples](./examples) folder. +- the [awesome-crypto-examples](https://github.com/tiagosiebler/awesome-crypto-examples) repository. + +If you're missing an example, you're welcome to request one. Priority will be given to [github sponsors](https://github.com/sponsors/tiagosiebler). + +### Usage +Create API credentials on Bybit's website: - [Livenet](https://bybit.com/app/user/api-management?affiliate_id=9410&language=en-US&group_id=0&group_type=1) - [Testnet](https://testnet.bybit.com/app/user/api-management) -## REST API Clients - -### REST Inverse -To use the inverse REST APIs, import the `InverseClient`: +All REST clients have can be used in a similar way. However, method names, parameters and responses may vary depending on the API category you're using! ```javascript -const { InverseClient } = require('bybit-api'); +const { + InverseClient, + LinearClient, + InverseFuturesClient, + SpotClient, + USDCOptionClient, + USDCPerpetualClient, + AccountAssetClient, +} = require('bybit-api'); const restClientOptions = { // override the max size of the request window (in ms) @@ -118,146 +124,23 @@ const client = new InverseClient( client.getApiKeyInfo() .then(result => { - console.log("apiKey result: ", result); + console.log("getApiKeyInfo result: ", result); }) .catch(err => { - console.error("apiKey error: ", err); + console.error("getApiKeyInfo error: ", err); }); client.getOrderBook({ symbol: 'BTCUSD' }) .then(result => { - console.log("getOrderBook inverse result: ", result); + console.log("getOrderBook result: ", result); }) .catch(err => { - console.error("getOrderBook inverse error: ", err); + console.error("getOrderBook error: ", err); }); ``` - -See [inverse-client.ts](./src/inverse-client.ts) for further information. - -### REST Inverse Futures -To use the inverse futures REST APIs, import the `InverseFuturesClient`: - -```javascript -const { InverseFuturesClient } = require('bybit-api'); - -const API_KEY = 'xxx'; -const PRIVATE_KEY = 'yyy'; -const useLivenet = false; - -const client = new InverseFuturesClient( - API_KEY, - PRIVATE_KEY, - - // optional, uses testnet by default. Set to 'true' to use livenet. - useLivenet, - - // restClientOptions, - // requestLibraryOptions -); - -client.getApiKeyInfo() - .then(result => { - console.log("apiKey result: ", result); - }) - .catch(err => { - console.error("apiKey error: ", err); - }); - -client.getOrderBook({ symbol: 'BTCUSDH21' }) - .then(result => { - console.log("getOrderBook inverse futures result: ", result); - }) - .catch(err => { - console.error("getOrderBook inverse futures error: ", err); - }); -``` - -See [inverse-futures-client.ts](./src/inverse-futures-client.ts) for further information. - -### REST Linear -To use the Linear (USDT) REST APIs, import the `LinearClient`: - -```javascript -const { LinearClient } = require('bybit-api'); - -const API_KEY = 'xxx'; -const PRIVATE_KEY = 'yyy'; -const useLivenet = false; - -const client = new LinearClient( - API_KEY, - PRIVATE_KEY, - - // optional, uses testnet by default. Set to 'true' to use livenet. - useLivenet, - - // restClientOptions, - // requestLibraryOptions -); - -client.getApiKeyInfo() - .then(result => { - console.log(result); - }) - .catch(err => { - console.error(err); - }); - -client.getOrderBook({ symbol: 'BTCUSDT' }) - .then(result => { - console.log("getOrderBook linear result: ", result); - }) - .catch(err => { - console.error("getOrderBook linear error: ", err); - }); -``` - -See [linear-client.ts](./src/linear-client.ts) for further information. - -### REST Spot -To use the Spot REST APIs, import the `SpotClient`: - -```javascript -const { SpotClient } = require('bybit-api'); - -const API_KEY = 'xxx'; -const PRIVATE_KEY = 'yyy'; -const useLivenet = false; - -const client = new SpotClient( - API_KEY, - PRIVATE_KEY, - - // optional, uses testnet by default. Set to 'true' to use livenet. - useLivenet, - - // restClientOptions, - // requestLibraryOptions -); - -client.getSymbols() - .then(result => { - console.log(result); - }) - .catch(err => { - console.error(err); - }); - -client.getBalances() - .then(result => { - console.log("getBalances result: ", result); - }) - .catch(err => { - console.error("getBalances error: ", err); - }); -``` - -See [spot-client.ts](./src/spot-client.ts) for further information. - ## WebSockets -Inverse, linear & spot WebSockets can be used via a shared `WebsocketClient`. However, make sure to make one instance of WebsocketClient per market type (spot vs inverse vs linear vs linearfutures): +All API groups can be used via a shared `WebsocketClient`. However, make sure to make one instance of the WebsocketClient per API group (spot vs inverse vs linear vs linearfutures etc): ```javascript const { WebsocketClient } = require('bybit-api'); @@ -337,11 +220,8 @@ ws.on('error', err => { }); ``` - See [websocket-client.ts](./src/websocket-client.ts) for further information. -Note: for linear websockets, pass `linear: true` in the constructor options when instancing the `WebsocketClient`. To connect to both linear and inverse websockets, make two instances of the WebsocketClient. - --- ## Customise Logging @@ -383,7 +263,7 @@ Or buy me a coffee using any of these: - ETH (ERC20): `0xd773d8e6a50758e1ada699bb6c4f98bb4abf82da` #### pixtron -The original library was started by @pixtron. If this library helps you to trade better on bybit, feel free to donate a coffee to @pixtron: +An early generation of this library was started by @pixtron. If this library helps you to trade better on bybit, feel free to donate a coffee to @pixtron: - BTC `1Fh1158pXXudfM6ZrPJJMR7Y5SgZUz4EdF` - ETH `0x21aEdeC53ab7593b77C9558942f0c9E78131e8d7` - LTC `LNdHSVtG6UWsriMYLJR3qLdfVNKwJ6GSLF` diff --git a/src/index.ts b/src/index.ts index 7e4c856..78627e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export * from './inverse-client'; export * from './inverse-futures-client'; export * from './linear-client'; export * from './spot-client'; -export * from './usdc-options-client'; +export * from './usdc-option-client'; export * from './usdc-perpetual-client'; export * from './websocket-client'; export * from './logger'; diff --git a/src/usdc-options-client.ts b/src/usdc-option-client.ts similarity index 100% rename from src/usdc-options-client.ts rename to src/usdc-option-client.ts From ec13879848095707ede0e161cbbc7105be11e92d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 9 Sep 2022 12:40:31 +0100 Subject: [PATCH 08/74] github CI test --- .github/workflows/integrationtest.yml | 36 +++++++++++++++++++++++++++ jest.config.js | 6 ++--- 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/integrationtest.yml diff --git a/.github/workflows/integrationtest.yml b/.github/workflows/integrationtest.yml new file mode 100644 index 0000000..369bfab --- /dev/null +++ b/.github/workflows/integrationtest.yml @@ -0,0 +1,36 @@ +name: "Build & Test" + +on: [push] + +# on: +# # pull_request: +# # branches: +# # - "master" +# push: +# branches: + +jobs: + build: + name: "Build, Test & Validate" + runs-on: ubuntu-latest + + steps: + - name: "Checkout source code" + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'npm' + + - name: Install + run: npm ci --ignore-scripts + + - name: Build + run: npm run build + + - name: Test + run: npm run test + env: + API_KEY_COM: ${{ secrets.API_KEY_COM }} + API_SECRET_COM: ${{ secrets.API_SECRET_COM }} diff --git a/jest.config.js b/jest.config.js index 32b968b..8f22e92 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,10 +6,10 @@ module.exports = { __PROD__: false }, testEnvironment: 'node', - preset: "ts-jest", + preset: 'ts-jest', verbose: true, // report individual test bail: false, // enable to stop test when an error occur, - detectOpenHandles: false, + detectOpenHandles: true, moduleDirectories: ['node_modules', 'src', 'test'], testMatch: ['**/test/**/*.test.ts?(x)'], testPathIgnorePatterns: ['node_modules/', 'dist/', '.json'], @@ -25,4 +25,4 @@ module.exports = { statements: -10 } } -}; \ No newline at end of file +}; From 81c5cf32a153e618133dedebd35a2b40a45cb7e9 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 9 Sep 2022 12:42:23 +0100 Subject: [PATCH 09/74] change job name --- .github/workflows/integrationtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrationtest.yml b/.github/workflows/integrationtest.yml index 369bfab..ce574c3 100644 --- a/.github/workflows/integrationtest.yml +++ b/.github/workflows/integrationtest.yml @@ -11,7 +11,7 @@ on: [push] jobs: build: - name: "Build, Test & Validate" + name: "Build & Test" runs-on: ubuntu-latest steps: From d27f79a5fa9d441b230750ffd1c07025b34e5aa2 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 9 Sep 2022 12:55:23 +0100 Subject: [PATCH 10/74] readme tweaks --- .github/workflows/npmpublish.yml | 5 +++++ README.md | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index 4bf5267..7bbb04e 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -51,6 +51,11 @@ jobs: env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} + - name: Create Github Release + if: steps.version-updated.outputs.has-updated + id: create_release + uses: ncipollo/release-action@v1 + #publish-gpr: #needs: build #runs-on: ubuntu-latest diff --git a/README.md b/README.md index 4e00e99..76e8dba 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ Most methods accept JS objects. These can be populated using parameters specifie - [Bybit API Docs (choose API category from the tabs at the top)](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-introduction). ## Structure -The connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bybit-api). This connector is fully compatible with both TypeScript and pure JavaScript projects. +The connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bybit-api). + +This connector is fully compatible with both TypeScript and pure JavaScript projects. The version on npm is the output from the `build` command and can be used in projects without TypeScript (although TypeScript is definitely recommended). - [src](./src) - the whole connector written in TypeScript - [lib](./lib) - the JavaScript version of the project (built from TypeScript). This should not be edited directly, as it will be overwritten with each release. From eac6c95669ee26a32e3de8c182267bf0655d63f0 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 9 Sep 2022 12:57:04 +0100 Subject: [PATCH 11/74] fix readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76e8dba..4c30716 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Each REST API group has a dedicated REST client. To avoid confusion, here are th | [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | | [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | | [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | -| [SpotClient](src/spot-client.ts) | [Spot Market APIs](https://bybit-exchange.github.io/docs/spot/#t-introduction) | +| [SpotClient](src/spot-client.ts) | [Spot Market (v1) APIs](https://bybit-exchange.github.io/docs/spot/v1/#t-introduction) | | [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | | [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | | [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | From a1c0887417d3b4d13482c98e8c1f90d40b237f07 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 9 Sep 2022 15:26:47 +0100 Subject: [PATCH 12/74] copy trading API --- .github/workflows/npmpublish.yml | 12 +- src/constants/enum.ts | 1 + src/copy-trading-client.ts | 157 +++++++++++++++++++++++++ src/index.ts | 3 +- src/types/request/copy-trading.ts | 53 +++++++++ src/types/request/index.ts | 1 + src/util/WsStore.ts | 6 +- src/util/index.ts | 1 + src/{ => util}/logger.ts | 0 src/websocket-client.ts | 2 +- test/copy-trading/private.read.test.ts | 36 ++++++ test/copy-trading/public.read.test.ts | 26 ++++ 12 files changed, 282 insertions(+), 16 deletions(-) create mode 100644 src/copy-trading-client.ts create mode 100644 src/types/request/copy-trading.ts rename src/{ => util}/logger.ts (100%) create mode 100644 test/copy-trading/private.read.test.ts create mode 100644 test/copy-trading/public.read.test.ts diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index 7bbb04e..813e3f5 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -9,16 +9,6 @@ on: - master jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 12 - #- run: npm ci - #- run: npm test - publish-npm: needs: build runs-on: ubuntu-latest @@ -37,7 +27,7 @@ jobs: - uses: actions/setup-node@v1 if: steps.version-updated.outputs.has-updated with: - node-version: 12 + node-version: 16 registry-url: https://registry.npmjs.org/ - run: npm ci --ignore-scripts diff --git a/src/constants/enum.ts b/src/constants/enum.ts index 18b23d9..3ef8f6b 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -16,6 +16,7 @@ export const API_ERROR_CODE = { SUCCESS: 0, /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, + INCORRECT_API_KEY_PERMISSIONS: 10005, ORDER_NOT_FOUND_OR_TOO_LATE: 20001, POSITION_STATUS_NOT_NORMAL: 30013, CANNOT_SET_TRADING_STOP_FOR_ZERO_POS: 30024, diff --git a/src/copy-trading-client.ts b/src/copy-trading-client.ts new file mode 100644 index 0000000..0ffe176 --- /dev/null +++ b/src/copy-trading-client.ts @@ -0,0 +1,157 @@ +import { + APIResponseWithTime, + CopyTradingCancelOrderRequest, + CopyTradingCloseOrderRequest, + CopyTradingOrderListRequest, + CopyTradingOrderRequest, + CopyTradingTradingStopRequest, + CopyTradingTransferRequest, + USDCAPIResponse, +} from './types'; +import { REST_CLIENT_TYPE_ENUM } from './util'; +import BaseRestClient from './util/BaseRestClient'; + +/** + * REST API client for USDC Perpetual APIs + */ +export class CopyTradingClient extends BaseRestClient { + getClientType() { + // Follows the same authentication mechanism as USDC APIs + return REST_CLIENT_TYPE_ENUM.usdc; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Market Data Endpoints + * + */ + + getSymbolList(): Promise> { + return this.get('/contract/v3/public/copytrading/symbol/list'); + } + + /** + * + * Account Data Endpoints + * + */ + + /** -> Order API */ + + /** Create order */ + submitOrder(params: CopyTradingOrderRequest): Promise> { + return this.postPrivate( + '/contract/v3/private/copytrading/order/create', + params + ); + } + + /** Set Trading Stop */ + setTradingStop( + params: CopyTradingTradingStopRequest + ): Promise> { + return this.postPrivate( + '/contract/v3/private/copytrading/order/trading-stop', + params + ); + } + + /** Query Order List */ + getActiveOrders( + params?: CopyTradingOrderListRequest + ): Promise> { + return this.getPrivate( + '/contract/v3/private/copytrading/order/list', + params + ); + } + + /** Cancel order */ + cancelOrder( + params: CopyTradingCancelOrderRequest + ): Promise> { + return this.postPrivate( + '/contract/v3/private/copytrading/order/cancel', + params + ); + } + + /** Close Order. This endpoint's rate_limit will decrease by 10 per request; ie, one request to this endpoint consumes 10 from the limit allowed per minute. */ + closeOrder( + params: CopyTradingCloseOrderRequest + ): Promise> { + return this.postPrivate('/contract/v3/private/copytrading/order/close', { + params, + }); + } + + /** -> Positions API */ + + /** Position List */ + getPositions(symbol?: string): Promise> { + return this.getPrivate('/contract/v3/private/copytrading/position/list', { + symbol, + }); + } + + /** Close Position */ + closePosition( + symbol: string, + positionIdx: string + ): Promise> { + return this.postPrivate('/contract/v3/private/copytrading/position/close', { + symbol, + positionIdx, + }); + } + + /** Only integers can be set to set the leverage */ + setLeverage( + symbol: string, + buyLeverage: string, + sellLeverage: string + ): Promise> { + return this.postPrivate( + '/contract/v3/private/copytrading/position/set-leverage', + { symbol, buyLeverage, sellLeverage } + ); + } + + /** + * + * Wallet Data Endpoints + * + */ + + /** Get Wallet Balance */ + getBalance(): Promise> { + return this.getPrivate('/contract/v3/private/copytrading/wallet/balance'); + } + + /** Transfer */ + transfer(params: CopyTradingTransferRequest): Promise> { + return this.postPrivate( + '/contract/v3/private/copytrading/wallet/transfer', + params + ); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } + + getAnnouncements(): Promise> { + return this.get('/v2/public/announcement'); + } +} diff --git a/src/index.ts b/src/index.ts index 78627e2..1336734 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './account-asset-client'; +export * from './copy-trading-client'; export * from './inverse-client'; export * from './inverse-futures-client'; export * from './linear-client'; @@ -6,7 +7,7 @@ export * from './spot-client'; export * from './usdc-option-client'; export * from './usdc-perpetual-client'; export * from './websocket-client'; -export * from './logger'; +export * from './util/logger'; export * from './types'; export * from './util/WsStore'; export * from './constants/enum'; diff --git a/src/types/request/copy-trading.ts b/src/types/request/copy-trading.ts new file mode 100644 index 0000000..d61200e --- /dev/null +++ b/src/types/request/copy-trading.ts @@ -0,0 +1,53 @@ +import { OrderSide } from '../shared'; +import { USDCOrderType } from './usdc-shared'; + +export interface CopyTradingOrderRequest { + side: OrderSide; + symbol: string; + orderType: USDCOrderType; + price: string; + qty: string; + takeProfit?: string; + stopLoss?: string; + tpTriggerBy?: string; + slTriggerBy?: string; + orderLinkId?: string; +} + +export interface CopyTradingTradingStopRequest { + symbol: string; + parentOrderId: string; + takeProfit?: string; + stopLoss?: string; + tpTriggerBy?: string; + slTriggerBy?: string; + parentOrderLinkId?: string; +} + +export interface CopyTradingOrderListRequest { + symbol?: string; + orderId?: string; + orderLinkId?: string; + copyTradeOrderType?: string; +} + +export interface CopyTradingCancelOrderRequest { + symbol: string; + orderId?: string; + orderLinkId?: string; +} + +export interface CopyTradingCloseOrderRequest { + symbol: string; + orderLinkId?: string; + parentOrderId?: string; + parentOrderLinkId?: string; +} + +export interface CopyTradingTransferRequest { + transferId: string; + coin: string; + amount: string; + fromAccountType: string; + toAccountType: string; +} diff --git a/src/types/request/index.ts b/src/types/request/index.ts index 2b797e3..e80dc2f 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,4 +1,5 @@ export * from './account-asset'; +export * from './copy-trading'; export * from './usdt-perp'; export * from './usdc-perp'; export * from './usdc-options'; diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index 995106c..dd7360e 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -1,8 +1,8 @@ -import { WsConnectionState } from '../websocket-client'; -import { DefaultLogger } from '../logger'; - import WebSocket from 'isomorphic-ws'; +import { WsConnectionState } from '../websocket-client'; +import { DefaultLogger } from './logger'; + type WsTopic = string; type WsTopicList = Set; diff --git a/src/util/index.ts b/src/util/index.ts index 90c60e3..e25bf42 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,3 +1,4 @@ export * from './BaseRestClient'; export * from './requestUtils'; export * from './WsStore'; +export * from './logger'; diff --git a/src/logger.ts b/src/util/logger.ts similarity index 100% rename from src/logger.ts rename to src/util/logger.ts diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 939058c..c8dd784 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -3,7 +3,7 @@ import WebSocket from 'isomorphic-ws'; import { InverseClient } from './inverse-client'; import { LinearClient } from './linear-client'; -import { DefaultLogger } from './logger'; +import { DefaultLogger } from './util/logger'; import { SpotClient } from './spot-client'; import { KlineInterval } from './types/shared'; import { signMessage } from './util/node-support'; diff --git a/test/copy-trading/private.read.test.ts b/test/copy-trading/private.read.test.ts new file mode 100644 index 0000000..f33c5e7 --- /dev/null +++ b/test/copy-trading/private.read.test.ts @@ -0,0 +1,36 @@ +import { API_ERROR_CODE, CopyTradingClient } from '../../src'; +import { successResponseObject } from '../response.util'; + +describe('Private Copy Trading REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new CopyTradingClient(API_KEY, API_SECRET, useLivenet); + + // Don't have copy trading properly enabled on the test account, so testing is very light + // (just make sure auth works and endpoint doesn't throw) + + it('getActiveOrders()', async () => { + expect(await api.getActiveOrders()).toMatchObject({ + retCode: API_ERROR_CODE.INCORRECT_API_KEY_PERMISSIONS, + }); + }); + + it('getPositions()', async () => { + expect(await api.getPositions()).toMatchObject({ + retCode: API_ERROR_CODE.INCORRECT_API_KEY_PERMISSIONS, + }); + }); + + it('getBalance()', async () => { + expect(await api.getBalance()).toMatchObject({ + retCode: API_ERROR_CODE.INCORRECT_API_KEY_PERMISSIONS, + }); + }); +}); diff --git a/test/copy-trading/public.read.test.ts b/test/copy-trading/public.read.test.ts new file mode 100644 index 0000000..89c24bb --- /dev/null +++ b/test/copy-trading/public.read.test.ts @@ -0,0 +1,26 @@ +import { CopyTradingClient } from '../../src'; +import { successResponseObject } from '../response.util'; + +describe('Public Copy Trading REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = undefined; + const API_SECRET = undefined; + + const api = new CopyTradingClient(API_KEY, API_SECRET, useLivenet); + + it('getSymbolList()', async () => { + expect(await api.getSymbolList()).toMatchObject({ + result: { + list: expect.any(Array), + }, + }); + }); + + it('getServerTime()', async () => { + expect(await api.getServerTime()).toMatchObject(successResponseObject()); + }); + + it('getAnnouncements()', async () => { + expect(await api.getAnnouncements()).toMatchObject(successResponseObject()); + }); +}); From 3a984594dc27e8bcd9e340e71de97f026eae5263 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 12:23:32 +0100 Subject: [PATCH 13/74] refactoring in new classes around consistency. Add spotv3 REST client --- README.md | 5 +- src/copy-trading-client.ts | 26 +-- src/spot-client-v3.ts | 271 +++++++++++++++++++++++ src/spot-client.ts | 2 + src/types/shared.ts | 2 +- src/usdc-option-client.ts | 62 +++--- src/usdc-perpetual-client.ts | 68 +++--- src/util/BaseRestClient.ts | 2 +- src/util/requestUtils.ts | 2 +- test/copy-trading/private.read.test.ts | 4 +- test/copy-trading/public.read.test.ts | 4 +- test/usdc/options/private.read.test.ts | 4 +- test/usdc/perpetual/private.read.test.ts | 4 +- test/usdc/perpetual/public.read.test.ts | 4 +- 14 files changed, 365 insertions(+), 95 deletions(-) create mode 100644 src/spot-client-v3.ts diff --git a/README.md b/README.md index 4c30716..4524667 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ [1]: https://www.npmjs.com/package/bybit-api Node.js connector for the Bybit APIs and WebSockets: +- Complete integration with all bybit APIs. - TypeScript support (with type declarations for most API requests & responses). -- Integration tests with real API calls validating any changes before they reach npm. +- Over 200 integration tests making real API calls, validating any changes before they reach npm. - Robust WebSocket integration with connection heartbeats & automatic reconnection. - Browser support (via webpack bundle - see "Browser Usage" below). @@ -77,6 +78,8 @@ Create API credentials on Bybit's website: All REST clients have can be used in a similar way. However, method names, parameters and responses may vary depending on the API category you're using! +Not sure which function to call or which parameters to use? Click the class name in the table above to look at all the function names (they are in the same order as the official API docs), and check the API docs for a list of endpoints/paramters/responses. + ```javascript const { InverseClient, diff --git a/src/copy-trading-client.ts b/src/copy-trading-client.ts index 0ffe176..0162f36 100644 --- a/src/copy-trading-client.ts +++ b/src/copy-trading-client.ts @@ -6,7 +6,7 @@ import { CopyTradingOrderRequest, CopyTradingTradingStopRequest, CopyTradingTransferRequest, - USDCAPIResponse, + APIResponseV3, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -17,7 +17,7 @@ import BaseRestClient from './util/BaseRestClient'; export class CopyTradingClient extends BaseRestClient { getClientType() { // Follows the same authentication mechanism as USDC APIs - return REST_CLIENT_TYPE_ENUM.usdc; + return REST_CLIENT_TYPE_ENUM.v3; } async fetchServerTime(): Promise { @@ -31,7 +31,7 @@ export class CopyTradingClient extends BaseRestClient { * */ - getSymbolList(): Promise> { + getSymbols(): Promise> { return this.get('/contract/v3/public/copytrading/symbol/list'); } @@ -44,7 +44,7 @@ export class CopyTradingClient extends BaseRestClient { /** -> Order API */ /** Create order */ - submitOrder(params: CopyTradingOrderRequest): Promise> { + submitOrder(params: CopyTradingOrderRequest): Promise> { return this.postPrivate( '/contract/v3/private/copytrading/order/create', params @@ -54,7 +54,7 @@ export class CopyTradingClient extends BaseRestClient { /** Set Trading Stop */ setTradingStop( params: CopyTradingTradingStopRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/contract/v3/private/copytrading/order/trading-stop', params @@ -64,7 +64,7 @@ export class CopyTradingClient extends BaseRestClient { /** Query Order List */ getActiveOrders( params?: CopyTradingOrderListRequest - ): Promise> { + ): Promise> { return this.getPrivate( '/contract/v3/private/copytrading/order/list', params @@ -74,7 +74,7 @@ export class CopyTradingClient extends BaseRestClient { /** Cancel order */ cancelOrder( params: CopyTradingCancelOrderRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/contract/v3/private/copytrading/order/cancel', params @@ -84,7 +84,7 @@ export class CopyTradingClient extends BaseRestClient { /** Close Order. This endpoint's rate_limit will decrease by 10 per request; ie, one request to this endpoint consumes 10 from the limit allowed per minute. */ closeOrder( params: CopyTradingCloseOrderRequest - ): Promise> { + ): Promise> { return this.postPrivate('/contract/v3/private/copytrading/order/close', { params, }); @@ -93,7 +93,7 @@ export class CopyTradingClient extends BaseRestClient { /** -> Positions API */ /** Position List */ - getPositions(symbol?: string): Promise> { + getPositions(symbol?: string): Promise> { return this.getPrivate('/contract/v3/private/copytrading/position/list', { symbol, }); @@ -103,7 +103,7 @@ export class CopyTradingClient extends BaseRestClient { closePosition( symbol: string, positionIdx: string - ): Promise> { + ): Promise> { return this.postPrivate('/contract/v3/private/copytrading/position/close', { symbol, positionIdx, @@ -115,7 +115,7 @@ export class CopyTradingClient extends BaseRestClient { symbol: string, buyLeverage: string, sellLeverage: string - ): Promise> { + ): Promise> { return this.postPrivate( '/contract/v3/private/copytrading/position/set-leverage', { symbol, buyLeverage, sellLeverage } @@ -129,12 +129,12 @@ export class CopyTradingClient extends BaseRestClient { */ /** Get Wallet Balance */ - getBalance(): Promise> { + getBalances(): Promise> { return this.getPrivate('/contract/v3/private/copytrading/wallet/balance'); } /** Transfer */ - transfer(params: CopyTradingTransferRequest): Promise> { + transfer(params: CopyTradingTransferRequest): Promise> { return this.postPrivate( '/contract/v3/private/copytrading/wallet/transfer', params diff --git a/src/spot-client-v3.ts b/src/spot-client-v3.ts new file mode 100644 index 0000000..514bb0e --- /dev/null +++ b/src/spot-client-v3.ts @@ -0,0 +1,271 @@ +import { + APIResponseWithTime, + APIResponseV3, + SpotOrderQueryById, + OrderSide, + OrderTypeSpot, + SpotBalances, +} from './types'; +import { REST_CLIENT_TYPE_ENUM } from './util'; +import BaseRestClient from './util/BaseRestClient'; + +/** + * REST API client for newer Spot V3 APIs. + */ +export class SpotV3Client extends BaseRestClient { + getClientType() { + // Follows the same authentication mechanism as other v3 APIs (e.g. USDC) + return REST_CLIENT_TYPE_ENUM.v3; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Market Data Endpoints + * + */ + + /** Get all symbols */ + getSymbols(): Promise> { + return this.get('/spot/v3/public/symbols'); + } + + /** Get orderbook for symbol */ + getOrderBook(symbol: string, limit?: number): Promise> { + return this.get('/spot/v3/public/quote/depth', { symbol, limit }); + } + + /** Get merged orderbook for symbol */ + getOrderBookMerged(params: unknown): Promise> { + return this.get('/spot/v3/public/quote/depth/merged', params); + } + + /** Get public trading records (raw trades) */ + getTrades(symbol: string, limit?: number): Promise> { + return this.get('/spot/v3/public/quote/trades', { symbol, limit }); + } + + /** Get candles/klines */ + getCandles(params: unknown): Promise> { + return this.get('/spot/v3/public/quote/kline', params); + } + + /** Get latest information for symbol (24hr ticker) */ + get24hrTicker(symbol?: string): Promise> { + return this.get('/spot/v3/public/quote/ticker/24hr', { symbol }); + } + + /** Get last traded price */ + getLastTradedPrice(symbol?: string): Promise> { + return this.get('/spot/v3/public/quote/ticker/price', { symbol }); + } + + /** Get best bid/ask price */ + getBestBidAskPrice(symbol?: string): Promise> { + return this.get('/spot/v3/public/quote/ticker/bookTicker', { symbol }); + } + + /** + * + * Account Data Endpoints + * + */ + + /** -> Order API */ + + /** Create order */ + submitOrder(params: unknown): Promise> { + return this.postPrivate('/spot/v3/private/order', params); + } + + /** Get active order state */ + getOrder(params: SpotOrderQueryById): Promise> { + return this.getPrivate('/spot/v3/private/order', params); + } + + /** Cancel order */ + cancelOrder(params: SpotOrderQueryById): Promise> { + return this.postPrivate('/spot/v3/private/cancel-order', params); + } + + /** Batch cancel orders */ + cancelOrderBatch(params: { + symbol: string; + side?: OrderSide; + orderTypes: OrderTypeSpot[]; + }): Promise> { + const orderTypes = params.orderTypes + ? params.orderTypes.join(',') + : undefined; + + return this.postPrivate('/spot/v3/private/cancel-orders', { + ...params, + orderTypes, + }); + } + + /** Batch cancel up to 100 orders by ID */ + cancelOrderBatchIDs(orderIds: string[]): Promise> { + const orderIdsCsv = orderIds.join(','); + return this.postPrivate('/spot/v3/private/cancel-orders-by-ids', { + orderIds: orderIdsCsv, + }); + } + + /** Get open orders */ + getOpenOrders( + symbol?: string, + orderId?: string, + limit?: number + ): Promise> { + return this.getPrivate('/spot/v3/private/open-orders', { + symbol, + orderId, + limit, + }); + } + + /** Get order history */ + getPastOrders( + symbol?: string, + orderId?: string, + limit?: number + ): Promise> { + return this.getPrivate('/spot/v3/private/history-orders', { + symbol, + orderId, + limit, + }); + } + + /** + * Get your trade history. + * If startTime is not specified, you can only query for records in the last 7 days. + * If you want to query for records older than 7 days, startTime is required. + */ + getMyTrades(params?: unknown): Promise> { + return this.getPrivate('/spot/v3/private/my-trades', params); + } + + /** + * + * Wallet Data Endpoints + * + */ + + /** Get Wallet Balance */ + getBalances(): Promise> { + return this.getPrivate('/spot/v3/private/account'); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } + + /** + * + * Leveraged Token Endpoints + * + */ + + /** Get all asset infos */ + getLeveragedTokenAssetInfos(ltCode?: string): Promise> { + return this.get('/spot/v3/public/infos', { ltCode }); + } + + /** Get leveraged token market info */ + getLeveragedTokenMarketInfo(ltCode: string): Promise> { + return this.getPrivate('/spot/v3/private/reference', { ltCode }); + } + + /** Purchase leveraged token */ + purchaseLeveragedToken( + ltCode: string, + ltAmount: string, + serialNo?: string + ): Promise> { + return this.postPrivate('/spot/v3/private/purchase', { + ltCode, + ltAmount, + serialNo, + }); + } + + /** Redeem leveraged token */ + redeemLeveragedToken( + ltCode: string, + ltAmount: string, + serialNo?: string + ): Promise> { + return this.postPrivate('/spot/v3/private/redeem', { + ltCode, + ltAmount, + serialNo, + }); + } + + /** Get leveraged token purchase/redemption history */ + getLeveragedTokenPRHistory(params: unknown): Promise> { + return this.getPrivate('/spot/v3/private/record', params); + } + + /** + * + * Cross Margin Trading Endpoints + * + */ + + /** Borrow margin loan */ + borrowCrossMarginLoan( + coin: string, + qty: string + ): Promise> { + return this.postPrivate('/spot/v3/private/cross-margin-loan', { + coin, + qty, + }); + } + + /** Repay margin loan */ + repayCrossMarginLoan(coin: string, qty: string): Promise> { + return this.postPrivate('/spot/v3/private/cross-margin-repay', { + coin, + qty, + }); + } + + /** Query borrowing info */ + getCrossMarginBorrowingInfo(params?: unknown): Promise> { + return this.getPrivate('/spot/v3/private/cross-margin-orders', params); + } + + /** Query account info */ + getCrossMarginAccountInfo(): Promise> { + return this.getPrivate('/spot/v3/private/cross-margin-account'); + } + + /** Query interest & quota */ + getCrossMarginInterestQuota(coin: string): Promise> { + return this.getPrivate('/spot/v3/private/cross-margin-loan-info', { coin }); + } + + /** Query repayment history */ + getCrossMarginRepaymentHistory( + params?: unknown + ): Promise> { + return this.getPrivate( + '/spot/v3/private/cross-margin-repay-history', + params + ); + } +} diff --git a/src/spot-client.ts b/src/spot-client.ts index 7753949..e87bb6a 100644 --- a/src/spot-client.ts +++ b/src/spot-client.ts @@ -13,6 +13,7 @@ import BaseRestClient from './util/BaseRestClient'; import { agentSource, REST_CLIENT_TYPE_ENUM } from './util/requestUtils'; /** + * @deprecated Use SpotV3Client instead, which leverages the newer v3 APIs * REST API client for Spot APIs (v1) */ export class SpotClient extends BaseRestClient { @@ -124,6 +125,7 @@ export class SpotClient extends BaseRestClient { const orderTypes = params.orderTypes ? params.orderTypes.join(',') : undefined; + return this.deletePrivate('/spot/order/batch-cancel', { ...params, orderTypes, diff --git a/src/types/shared.ts b/src/types/shared.ts index 7312a58..fdb7ced 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -25,7 +25,7 @@ export interface APIResponse { result: T; } -export interface USDCAPIResponse { +export interface APIResponseV3 { retCode: number; retMsg: 'OK' | string; result: T; diff --git a/src/usdc-option-client.ts b/src/usdc-option-client.ts index 252d4d2..acf7dc6 100644 --- a/src/usdc-option-client.ts +++ b/src/usdc-option-client.ts @@ -1,6 +1,6 @@ import { APIResponseWithTime, - USDCAPIResponse, + APIResponseV3, USDCOptionsActiveOrdersRealtimeRequest, USDCOptionsActiveOrdersRequest, USDCOptionsCancelAllOrdersRequest, @@ -27,7 +27,7 @@ import BaseRestClient from './util/BaseRestClient'; */ export class USDCOptionClient extends BaseRestClient { getClientType() { - return REST_CLIENT_TYPE_ENUM.usdc; + return REST_CLIENT_TYPE_ENUM.v3; } async fetchServerTime(): Promise { @@ -42,33 +42,33 @@ export class USDCOptionClient extends BaseRestClient { */ /** Query order book info. Each side has a depth of 25 orders. */ - getOrderBook(symbol: string): Promise> { + getOrderBook(symbol: string): Promise> { return this.get('/option/usdc/openapi/public/v1/order-book', { symbol }); } /** Fetch trading rules (such as min/max qty). Query for all if blank. */ getContractInfo( params?: USDCOptionsContractInfoRequest - ): Promise> { + ): Promise> { return this.get('/option/usdc/openapi/public/v1/symbols', params); } /** Get a symbol price/statistics ticker */ - getSymbolTicker(symbol: string): Promise> { + getSymbolTicker(symbol: string): Promise> { return this.get('/option/usdc/openapi/public/v1/tick', { symbol }); } /** Get delivery information */ getDeliveryPrice( params?: USDCOptionsDeliveryPriceRequest - ): Promise> { + ): Promise> { return this.get('/option/usdc/openapi/public/v1/delivery-price', params); } /** Returned records are Taker Buy in default. */ getLast500Trades( params: USDCOptionsRecentTradesRequest - ): Promise> { + ): Promise> { return this.get( '/option/usdc/openapi/public/v1/query-trade-latest', params @@ -84,7 +84,7 @@ export class USDCOptionClient extends BaseRestClient { */ getHistoricalVolatility( params?: USDCOptionsHistoricalVolatilityRequest - ): Promise> { + ): Promise> { return this.get( '/option/usdc/openapi/public/v1/query-historical-volatility', params @@ -104,7 +104,7 @@ export class USDCOptionClient extends BaseRestClient { * The request status can be queried in real-time. * The response parameters must be queried through a query or a WebSocket response. */ - submitOrder(params: USDCOptionsOrderRequest): Promise> { + submitOrder(params: USDCOptionsOrderRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/place-order', params @@ -116,7 +116,7 @@ export class USDCOptionClient extends BaseRestClient { */ batchSubmitOrders( orderRequest: USDCOptionsOrderRequest[] - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/batch-place-orders', { orderRequest } @@ -126,7 +126,7 @@ export class USDCOptionClient extends BaseRestClient { /** For Options, at least one of the three parameters — price, quantity or implied volatility — must be input. */ modifyOrder( params: USDCOptionsModifyOrderRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/replace-order', params @@ -136,7 +136,7 @@ export class USDCOptionClient extends BaseRestClient { /** Each request supports a max. of four orders. The reduceOnly parameter should be separate and unique for each order in the request. */ batchModifyOrders( replaceOrderRequest: USDCOptionsModifyOrderRequest[] - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/batch-replace-orders', { replaceOrderRequest } @@ -146,7 +146,7 @@ export class USDCOptionClient extends BaseRestClient { /** Cancel order */ cancelOrder( params: USDCOptionsCancelOrderRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/cancel-order', params @@ -156,7 +156,7 @@ export class USDCOptionClient extends BaseRestClient { /** Batch cancel orders */ batchCancelOrders( cancelRequest: USDCOptionsCancelOrderRequest[] - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/batch-cancel-orders', { cancelRequest } @@ -166,7 +166,7 @@ export class USDCOptionClient extends BaseRestClient { /** This is used to cancel all active orders. The real-time response indicates whether the request is successful, depending on retCode. */ cancelActiveOrders( params?: USDCOptionsCancelAllOrdersRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/cancel-all', params @@ -176,7 +176,7 @@ export class USDCOptionClient extends BaseRestClient { /** Query Unfilled/Partially Filled Orders(real-time), up to last 7 days of partially filled/unfilled orders */ getActiveRealtimeOrders( params?: USDCOptionsActiveOrdersRealtimeRequest - ): Promise> { + ): Promise> { return this.getPrivate( '/option/usdc/openapi/private/v1/trade/query-active-orders', params @@ -186,7 +186,7 @@ export class USDCOptionClient extends BaseRestClient { /** Query Unfilled/Partially Filled Orders */ getActiveOrders( params: USDCOptionsActiveOrdersRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-active-orders', params @@ -196,7 +196,7 @@ export class USDCOptionClient extends BaseRestClient { /** Query order history. The endpoint only supports up to 30 days of queried records */ getHistoricOrders( params: USDCOptionsHistoricOrdersRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-order-history', params @@ -206,7 +206,7 @@ export class USDCOptionClient extends BaseRestClient { /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ getOrderExecutionHistory( params: USDCOptionsOrderExecutionRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/execution-list', params @@ -218,7 +218,7 @@ export class USDCOptionClient extends BaseRestClient { /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ getTransactionLog( params: USDCTransactionLogRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-transaction-log', params @@ -226,14 +226,14 @@ export class USDCOptionClient extends BaseRestClient { } /** Wallet info for USDC account. */ - getBalance(): Promise> { + getBalances(): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-wallet-balance' ); } /** Asset Info */ - getAssetInfo(baseCoin?: string): Promise> { + getAssetInfo(baseCoin?: string): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-asset-info', { baseCoin } @@ -246,7 +246,7 @@ export class USDCOptionClient extends BaseRestClient { */ setMarginMode( newMarginMode: 'REGULAR_MARGIN' | 'PORTFOLIO_MARGIN' - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/private/asset/account/setMarginMode', { setMarginMode: newMarginMode } @@ -254,7 +254,7 @@ export class USDCOptionClient extends BaseRestClient { } /** Query margin mode for USDC account. */ - getMarginMode(): Promise> { + getMarginMode(): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-margin-info' ); @@ -263,7 +263,7 @@ export class USDCOptionClient extends BaseRestClient { /** -> Positions API */ /** Query my positions */ - getPositions(params: USDCPositionsRequest): Promise> { + getPositions(params: USDCPositionsRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-position', params @@ -273,7 +273,7 @@ export class USDCOptionClient extends BaseRestClient { /** Query Delivery History */ getDeliveryHistory( params: USDCOptionsDeliveryHistoryRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-delivery-list', params @@ -283,7 +283,7 @@ export class USDCOptionClient extends BaseRestClient { /** Query Positions Info Upon Expiry */ getPositionsInfoUponExpiry( params?: USDCOptionsPositionsInfoExpiryRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-position-exp-date', params @@ -293,9 +293,7 @@ export class USDCOptionClient extends BaseRestClient { /** -> Market Maker Protection */ /** modifyMMP */ - modifyMMP( - params: USDCOptionsModifyMMPRequest - ): Promise> { + modifyMMP(params: USDCOptionsModifyMMPRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/mmp-modify', params @@ -303,14 +301,14 @@ export class USDCOptionClient extends BaseRestClient { } /** resetMMP */ - resetMMP(currency: string): Promise> { + resetMMP(currency: string): Promise> { return this.postPrivate('/option/usdc/openapi/private/v1/mmp-reset', { currency, }); } /** queryMMPState */ - queryMMPState(baseCoin: string): Promise> { + queryMMPState(baseCoin: string): Promise> { return this.postPrivate('/option/usdc/openapi/private/v1/get-mmp-state', { baseCoin, }); diff --git a/src/usdc-perpetual-client.ts b/src/usdc-perpetual-client.ts index 9c22d89..807d498 100644 --- a/src/usdc-perpetual-client.ts +++ b/src/usdc-perpetual-client.ts @@ -2,7 +2,7 @@ import { APIResponseWithTime, SymbolLimitParam, SymbolPeriodLimitParam, - USDCAPIResponse, + APIResponseV3, USDCKlineRequest, USDCLast500TradesRequest, USDCOpenInterestRequest, @@ -25,7 +25,7 @@ import BaseRestClient from './util/BaseRestClient'; */ export class USDCPerpetualClient extends BaseRestClient { getClientType() { - return REST_CLIENT_TYPE_ENUM.usdc; + return REST_CLIENT_TYPE_ENUM.v3; } async fetchServerTime(): Promise { @@ -39,41 +39,41 @@ export class USDCPerpetualClient extends BaseRestClient { * */ - getOrderBook(symbol: string): Promise> { + getOrderBook(symbol: string): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/order-book', { symbol }); } /** Fetch trading rules (such as min/max qty). Query for all if blank. */ getContractInfo( params?: USDCSymbolDirectionLimit - ): Promise> { + ): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/symbols', params); } /** Get a symbol price/statistics ticker */ - getSymbolTicker(symbol: string): Promise> { + getSymbolTicker(symbol: string): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/tick', { symbol }); } - getKline(params: USDCKlineRequest): Promise> { + getCandles(params: USDCKlineRequest): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/kline/list', params); } - getMarkPrice(params: USDCKlineRequest): Promise> { + getMarkPrice(params: USDCKlineRequest): Promise> { return this.get( '/perpetual/usdc/openapi/public/v1/mark-price-kline', params ); } - getIndexPrice(params: USDCKlineRequest): Promise> { + getIndexPrice(params: USDCKlineRequest): Promise> { return this.get( '/perpetual/usdc/openapi/public/v1/index-price-kline', params ); } - getIndexPremium(params: USDCKlineRequest): Promise> { + getIndexPremium(params: USDCKlineRequest): Promise> { return this.get( '/perpetual/usdc/openapi/public/v1/premium-index-kline', params @@ -82,25 +82,25 @@ export class USDCPerpetualClient extends BaseRestClient { getOpenInterest( params: USDCOpenInterestRequest - ): Promise> { + ): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/open-interest', params); } getLargeOrders( params: SymbolLimitParam - ): Promise> { + ): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/big-deal', params); } getLongShortRatio( params: SymbolPeriodLimitParam - ): Promise> { + ): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/account-ratio', params); } getLast500Trades( params: USDCLast500TradesRequest - ): Promise> { + ): Promise> { return this.get( '/option/usdc/openapi/public/v1/query-trade-latest', params @@ -120,7 +120,7 @@ export class USDCPerpetualClient extends BaseRestClient { * The request status can be queried in real-time. * The response parameters must be queried through a query or a WebSocket response. */ - submitOrder(params: USDCPerpOrderRequest): Promise> { + submitOrder(params: USDCPerpOrderRequest): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/place-order', params @@ -128,9 +128,7 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Active order parameters (such as quantity, price) and stop order parameters cannot be modified in one request at the same time. Please request modification separately. */ - modifyOrder( - params: USDCPerpModifyOrderRequest - ): Promise> { + modifyOrder(params: USDCPerpModifyOrderRequest): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/replace-order', params @@ -138,9 +136,7 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Cancel order */ - cancelOrder( - params: USDCPerpCancelOrderRequest - ): Promise> { + cancelOrder(params: USDCPerpCancelOrderRequest): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/cancel-order', params @@ -151,7 +147,7 @@ export class USDCPerpetualClient extends BaseRestClient { cancelActiveOrders( symbol: string, orderFilter: USDCOrderFilter - ): Promise> { + ): Promise> { return this.postPrivate('/perpetual/usdc/openapi/private/v1/cancel-all', { symbol, orderFilter, @@ -161,7 +157,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** Query Unfilled/Partially Filled Orders */ getActiveOrders( params: USDCPerpActiveOrdersRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-active-orders', params @@ -171,7 +167,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** Query order history. The endpoint only supports up to 30 days of queried records */ getHistoricOrders( params: USDCPerpHistoricOrdersRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-order-history', params @@ -181,7 +177,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ getOrderExecutionHistory( params: USDCPerpActiveOrdersRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/execution-list', params @@ -193,7 +189,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ getTransactionLog( params: USDCTransactionLogRequest - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-transaction-log', params @@ -201,14 +197,14 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Wallet info for USDC account. */ - getBalance(): Promise> { + getBalances(): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-wallet-balance' ); } /** Asset Info */ - getAssetInfo(baseCoin?: string): Promise> { + getAssetInfo(baseCoin?: string): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-asset-info', { baseCoin } @@ -221,7 +217,7 @@ export class USDCPerpetualClient extends BaseRestClient { */ setMarginMode( newMarginMode: 'REGULAR_MARGIN' | 'PORTFOLIO_MARGIN' - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/private/asset/account/setMarginMode', { setMarginMode: newMarginMode } @@ -229,7 +225,7 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Query margin mode for USDC account. */ - getMarginMode(): Promise> { + getMarginMode(): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-margin-info' ); @@ -238,7 +234,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** -> Positions API */ /** Query my positions */ - getPositions(params: USDCPositionsRequest): Promise> { + getPositions(params: USDCPositionsRequest): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/query-position', params @@ -246,7 +242,7 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Only for REGULAR_MARGIN */ - setLeverage(symbol: string, leverage: string): Promise> { + setLeverage(symbol: string, leverage: string): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/position/leverage/save', { symbol, leverage } @@ -256,7 +252,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** Query Settlement History */ getSettlementHistory( params?: USDCSymbolDirectionLimitCursor - ): Promise> { + ): Promise> { return this.postPrivate( '/option/usdc/openapi/private/v1/session-settlement', params @@ -266,7 +262,7 @@ export class USDCPerpetualClient extends BaseRestClient { /** -> Risk Limit API */ /** Query risk limit */ - getRiskLimit(symbol: string): Promise> { + getRiskLimit(symbol: string): Promise> { return this.getPrivate( '/perpetual/usdc/openapi/public/v1/risk-limit/list', { @@ -276,7 +272,7 @@ export class USDCPerpetualClient extends BaseRestClient { } /** Set risk limit */ - setRiskLimit(symbol: string, riskId: number): Promise> { + setRiskLimit(symbol: string, riskId: number): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/position/set-risk-limit', { symbol, riskId } @@ -286,14 +282,14 @@ export class USDCPerpetualClient extends BaseRestClient { /** -> Funding API */ /** Funding settlement occurs every 8 hours at 00:00 UTC, 08:00 UTC and 16:00 UTC. The current interval's fund fee settlement is based on the previous interval's fund rate. For example, at 16:00, the settlement is based on the fund rate generated at 8:00. The fund rate generated at 16:00 will be used at 0:00 the next day. */ - getLastFundingRate(symbol: string): Promise> { + getLastFundingRate(symbol: string): Promise> { return this.get('/perpetual/usdc/openapi/public/v1/prev-funding-rate', { symbol, }); } /** Get predicted funding rate and my predicted funding fee */ - getPredictedFundingRate(symbol: string): Promise> { + getPredictedFundingRate(symbol: string): Promise> { return this.postPrivate( '/perpetual/usdc/openapi/private/v1/predicted-funding', { symbol } diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index eb4dee5..6b9ad75 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -124,7 +124,7 @@ export default abstract class BaseRestClient { } private isUSDCClient() { - return this.clientType === REST_CLIENT_TYPE_ENUM.usdc; + return this.clientType === REST_CLIENT_TYPE_ENUM.v3; } get(endpoint: string, params?: any) { diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index 9a79db6..d3d6eb7 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -84,7 +84,7 @@ export const REST_CLIENT_TYPE_ENUM = { inverseFutures: 'inverseFutures', linear: 'linear', spot: 'spot', - usdc: 'usdc', + v3: 'v3', } as const; export type RestClientType = diff --git a/test/copy-trading/private.read.test.ts b/test/copy-trading/private.read.test.ts index f33c5e7..44825c9 100644 --- a/test/copy-trading/private.read.test.ts +++ b/test/copy-trading/private.read.test.ts @@ -28,8 +28,8 @@ describe('Private Copy Trading REST API Endpoints', () => { }); }); - it('getBalance()', async () => { - expect(await api.getBalance()).toMatchObject({ + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject({ retCode: API_ERROR_CODE.INCORRECT_API_KEY_PERMISSIONS, }); }); diff --git a/test/copy-trading/public.read.test.ts b/test/copy-trading/public.read.test.ts index 89c24bb..8e5abc4 100644 --- a/test/copy-trading/public.read.test.ts +++ b/test/copy-trading/public.read.test.ts @@ -8,8 +8,8 @@ describe('Public Copy Trading REST API Endpoints', () => { const api = new CopyTradingClient(API_KEY, API_SECRET, useLivenet); - it('getSymbolList()', async () => { - expect(await api.getSymbolList()).toMatchObject({ + it('getSymbols()', async () => { + expect(await api.getSymbols()).toMatchObject({ result: { list: expect.any(Array), }, diff --git a/test/usdc/options/private.read.test.ts b/test/usdc/options/private.read.test.ts index c682a55..1535304 100644 --- a/test/usdc/options/private.read.test.ts +++ b/test/usdc/options/private.read.test.ts @@ -48,8 +48,8 @@ describe('Private Account Asset REST API Endpoints', () => { ); }); - it('getBalance()', async () => { - expect(await api.getBalance()).toMatchObject(successUSDCResponseObject()); + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject(successUSDCResponseObject()); }); it('getAssetInfo()', async () => { diff --git a/test/usdc/perpetual/private.read.test.ts b/test/usdc/perpetual/private.read.test.ts index 02f8a5c..23e5121 100644 --- a/test/usdc/perpetual/private.read.test.ts +++ b/test/usdc/perpetual/private.read.test.ts @@ -40,8 +40,8 @@ describe('Private Account Asset REST API Endpoints', () => { ); }); - it('getBalance()', async () => { - expect(await api.getBalance()).toMatchObject(successUSDCResponseObject()); + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject(successUSDCResponseObject()); }); it('getAssetInfo()', async () => { diff --git a/test/usdc/perpetual/public.read.test.ts b/test/usdc/perpetual/public.read.test.ts index 4f90a62..60ce93e 100644 --- a/test/usdc/perpetual/public.read.test.ts +++ b/test/usdc/perpetual/public.read.test.ts @@ -35,8 +35,8 @@ describe('Public USDC Options REST API Endpoints', () => { ); }); - it('getKline()', async () => { - expect(await api.getKline(candleRequest)).toMatchObject( + it('getCandles()', async () => { + expect(await api.getCandles(candleRequest)).toMatchObject( successUSDCResponseObject() ); }); From c287495052290d789d9e297bb41881b57dada25c Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 12:33:51 +0100 Subject: [PATCH 14/74] spot v3 request types --- src/spot-client-v3.ts | 48 +++++++++++++++++----- src/types/index.ts | 1 - src/types/request/index.ts | 1 + src/types/request/spot.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/types/spot.ts | 34 ---------------- 5 files changed, 121 insertions(+), 44 deletions(-) create mode 100644 src/types/request/spot.ts delete mode 100644 src/types/spot.ts diff --git a/src/spot-client-v3.ts b/src/spot-client-v3.ts index 514bb0e..bf75429 100644 --- a/src/spot-client-v3.ts +++ b/src/spot-client-v3.ts @@ -5,6 +5,12 @@ import { OrderSide, OrderTypeSpot, SpotBalances, + KlineInterval, + NewSpotOrderV3, + SpotMyTradesRequest, + SpotLeveragedTokenPRHistoryRequest, + SpotCrossMarginBorrowingInfoRequest, + SpotCrossMarginRepaymentHistoryRequest, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -40,8 +46,16 @@ export class SpotV3Client extends BaseRestClient { } /** Get merged orderbook for symbol */ - getOrderBookMerged(params: unknown): Promise> { - return this.get('/spot/v3/public/quote/depth/merged', params); + getOrderBookMerged( + symbol: string, + scale?: number, + limit?: number + ): Promise> { + return this.get('/spot/v3/public/quote/depth/merged', { + symbol, + scale, + limit, + }); } /** Get public trading records (raw trades) */ @@ -50,8 +64,20 @@ export class SpotV3Client extends BaseRestClient { } /** Get candles/klines */ - getCandles(params: unknown): Promise> { - return this.get('/spot/v3/public/quote/kline', params); + getCandles( + symbol: string, + interval: KlineInterval, + limit?: number, + startTime?: number, + endTime?: number + ): Promise> { + return this.get('/spot/v3/public/quote/kline', { + symbol, + interval, + limit, + startTime, + endTime, + }); } /** Get latest information for symbol (24hr ticker) */ @@ -78,7 +104,7 @@ export class SpotV3Client extends BaseRestClient { /** -> Order API */ /** Create order */ - submitOrder(params: unknown): Promise> { + submitOrder(params: NewSpotOrderV3): Promise> { return this.postPrivate('/spot/v3/private/order', params); } @@ -147,7 +173,7 @@ export class SpotV3Client extends BaseRestClient { * If startTime is not specified, you can only query for records in the last 7 days. * If you want to query for records older than 7 days, startTime is required. */ - getMyTrades(params?: unknown): Promise> { + getMyTrades(params?: SpotMyTradesRequest): Promise> { return this.getPrivate('/spot/v3/private/my-trades', params); } @@ -215,7 +241,9 @@ export class SpotV3Client extends BaseRestClient { } /** Get leveraged token purchase/redemption history */ - getLeveragedTokenPRHistory(params: unknown): Promise> { + getLeveragedTokenPRHistory( + params?: SpotLeveragedTokenPRHistoryRequest + ): Promise> { return this.getPrivate('/spot/v3/private/record', params); } @@ -245,7 +273,9 @@ export class SpotV3Client extends BaseRestClient { } /** Query borrowing info */ - getCrossMarginBorrowingInfo(params?: unknown): Promise> { + getCrossMarginBorrowingInfo( + params?: SpotCrossMarginBorrowingInfoRequest + ): Promise> { return this.getPrivate('/spot/v3/private/cross-margin-orders', params); } @@ -261,7 +291,7 @@ export class SpotV3Client extends BaseRestClient { /** Query repayment history */ getCrossMarginRepaymentHistory( - params?: unknown + params?: SpotCrossMarginRepaymentHistoryRequest ): Promise> { return this.getPrivate( '/spot/v3/private/cross-margin-repay-history', diff --git a/src/types/index.ts b/src/types/index.ts index 78f54db..145ac98 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,3 @@ export * from './response'; export * from './request'; export * from './shared'; -export * from './spot'; diff --git a/src/types/request/index.ts b/src/types/request/index.ts index e80dc2f..77aaef5 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,5 +1,6 @@ export * from './account-asset'; export * from './copy-trading'; +export * from './spot'; export * from './usdt-perp'; export * from './usdc-perp'; export * from './usdc-options'; diff --git a/src/types/request/spot.ts b/src/types/request/spot.ts new file mode 100644 index 0000000..fc589ea --- /dev/null +++ b/src/types/request/spot.ts @@ -0,0 +1,81 @@ +import { numberInString, OrderSide } from '../shared'; + +export type OrderTypeSpot = 'LIMIT' | 'MARKET' | 'LIMIT_MAKER'; +export type OrderTimeInForce = 'GTC' | 'FOK' | 'IOC'; + +export interface NewSpotOrder { + symbol: string; + qty: number; + side: OrderSide; + type: OrderTypeSpot; + timeInForce?: OrderTimeInForce; + price?: number; + orderLinkId?: string; +} + +export interface NewSpotOrderV3 { + symbol: string; + orderQty: string; + side: OrderSide; + orderType: OrderTypeSpot; + timeInForce?: OrderTimeInForce; + orderPrice?: string; + orderLinkId?: string; + orderCategory?: 0 | 1; + triggerPrice?: string; +} + +export interface SpotOrderQueryById { + orderId?: string; + orderLinkId?: string; +} + +export interface SpotSymbolInfo { + name: string; + alias: string; + baseCurrency: string; + quoteCurrency: string; + basePrecision: numberInString; + quotePrecision: numberInString; + minTradeQuantity: numberInString; + minTradeAmount: numberInString; + minPricePrecision: numberInString; + maxTradeQuantity: numberInString; + maxTradeAmount: numberInString; + category: numberInString; +} + +export interface SpotMyTradesRequest { + symbol?: string; + orderId?: string; + limit?: string; + startTime?: number; + endTime?: number; + fromTradeId?: string; + toTradeId?: string; +} + +export interface SpotLeveragedTokenPRHistoryRequest { + ltCode?: string; + orderId?: string; + startTime?: number; + endTime?: number; + limit?: number; + orderType?: 1 | 2; + serialNo?: string; +} + +export interface SpotCrossMarginBorrowingInfoRequest { + startTime?: number; + endTime?: number; + coin?: string; + status?: 0 | 1 | 2; + limit?: number; +} + +export interface SpotCrossMarginRepaymentHistoryRequest { + startTime?: number; + endTime?: number; + coin?: string; + limit?: number; +} diff --git a/src/types/spot.ts b/src/types/spot.ts deleted file mode 100644 index 24a581d..0000000 --- a/src/types/spot.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { numberInString, OrderSide } from './shared'; - -export type OrderTypeSpot = 'LIMIT' | 'MARKET' | 'LIMIT_MAKER'; -export type OrderTimeInForce = 'GTC' | 'FOK' | 'IOC'; - -export interface NewSpotOrder { - symbol: string; - qty: number; - side: OrderSide; - type: OrderTypeSpot; - timeInForce?: OrderTimeInForce; - price?: number; - orderLinkId?: string; -} - -export interface SpotOrderQueryById { - orderId?: string; - orderLinkId?: string; -} - -export interface SpotSymbolInfo { - name: string; - alias: string; - baseCurrency: string; - quoteCurrency: string; - basePrecision: numberInString; - quotePrecision: numberInString; - minTradeQuantity: numberInString; - minTradeAmount: numberInString; - minPricePrecision: numberInString; - maxTradeQuantity: numberInString; - maxTradeAmount: numberInString; - category: numberInString; -} From f7b251c26df4965ccc79589950c86580b4970bf2 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 12:39:08 +0100 Subject: [PATCH 15/74] public v3 spot tests --- src/index.ts | 1 + src/spot-client-v3.ts | 4 +- test/response.util.ts | 6 +-- test/spot/public.v3.test.ts | 74 +++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 test/spot/public.v3.test.ts diff --git a/src/index.ts b/src/index.ts index 1336734..44392ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export * from './inverse-client'; export * from './inverse-futures-client'; export * from './linear-client'; export * from './spot-client'; +export * from './spot-client-v3'; export * from './usdc-option-client'; export * from './usdc-perpetual-client'; export * from './websocket-client'; diff --git a/src/spot-client-v3.ts b/src/spot-client-v3.ts index bf75429..33719a9 100644 --- a/src/spot-client-v3.ts +++ b/src/spot-client-v3.ts @@ -18,7 +18,7 @@ import BaseRestClient from './util/BaseRestClient'; /** * REST API client for newer Spot V3 APIs. */ -export class SpotV3Client extends BaseRestClient { +export class SpotClientV3 extends BaseRestClient { getClientType() { // Follows the same authentication mechanism as other v3 APIs (e.g. USDC) return REST_CLIENT_TYPE_ENUM.v3; @@ -46,7 +46,7 @@ export class SpotV3Client extends BaseRestClient { } /** Get merged orderbook for symbol */ - getOrderBookMerged( + getMergedOrderBook( symbol: string, scale?: number, limit?: number diff --git a/test/response.util.ts b/test/response.util.ts index 18b3010..b9bc320 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -16,14 +16,14 @@ export function successResponseObject(successMsg: string | null = 'OK') { }; } -export function successUSDCResponseObject() { +export function successResponseObjectV3() { return { result: expect.any(Object), - ...successUSDCEmptyResponseObject(), + ...successEmptyResponseObjectV3(), }; } -export function successUSDCEmptyResponseObject() { +export function successEmptyResponseObjectV3() { return { retCode: API_ERROR_CODE.SUCCESS, retMsg: expect.stringMatching( diff --git a/test/spot/public.v3.test.ts b/test/spot/public.v3.test.ts new file mode 100644 index 0000000..13dbad4 --- /dev/null +++ b/test/spot/public.v3.test.ts @@ -0,0 +1,74 @@ +import { SpotClientV3 } from '../../src'; +import { + notAuthenticatedError, + successResponseList, + successResponseObject, + successResponseObjectV3, +} from '../response.util'; + +describe('Public Spot REST API Endpoints', () => { + const useLivenet = true; + const api = new SpotClientV3(undefined, undefined, useLivenet); + + const symbol = 'BTCUSDT'; + const interval = '15m'; + const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60; + const from = Number(timestampOneHourAgo.toFixed(0)); + + it('should throw for unauthenticated private calls', async () => { + expect(() => api.getOpenOrders()).rejects.toMatchObject( + notAuthenticatedError() + ); + expect(() => api.getBalances()).rejects.toMatchObject( + notAuthenticatedError() + ); + }); + + it('getSymbols()', async () => { + expect(await api.getSymbols()).toMatchObject(successResponseObjectV3()); + }); + + it('getOrderBook()', async () => { + expect(await api.getOrderBook(symbol)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getMergedOrderBook()', async () => { + expect(await api.getMergedOrderBook(symbol)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getTrades()', async () => { + expect(await api.getTrades(symbol)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getCandles()', async () => { + expect(await api.getCandles(symbol, interval)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('get24hrTicker()', async () => { + expect(await api.get24hrTicker()).toMatchObject(successResponseObjectV3()); + }); + + it('getLastTradedPrice()', async () => { + expect(await api.getLastTradedPrice()).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getBestBidAskPrice()', async () => { + expect(await api.getBestBidAskPrice()).toMatchObject( + successResponseObjectV3() + ); + }); + + it('fetchServertime() returns number', async () => { + expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number)); + }); +}); From d7496c402b584b83b68a3258c74941b8596ad162 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 12:48:57 +0100 Subject: [PATCH 16/74] spot private read tests v3 --- src/constants/enum.ts | 3 ++ test/response.util.ts | 25 +++++++-- test/spot/private.v3.read.test.ts | 89 +++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 test/spot/private.v3.read.test.ts diff --git a/src/constants/enum.ts b/src/constants/enum.ts index 3ef8f6b..43c357a 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -17,6 +17,9 @@ export const API_ERROR_CODE = { /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, INCORRECT_API_KEY_PERMISSIONS: 10005, + ORDER_NOT_FOUND_SPOT_V3: 12213, + ORDER_NOT_FOUND_LEVERAGED_TOKEN: 12407, + QUERY_ACCOUNT_INFO_ERROR: 12602, ORDER_NOT_FOUND_OR_TOO_LATE: 20001, POSITION_STATUS_NOT_NORMAL: 30013, CANNOT_SET_TRADING_STOP_FOR_ZERO_POS: 30024, diff --git a/test/response.util.ts b/test/response.util.ts index b9bc320..760e352 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -1,5 +1,7 @@ import { API_ERROR_CODE } from '../src'; +const SUCCESS_MSG_REGEX = /OK|SUCCESS|success|success\.|Request accepted|/gim; + export function successResponseList(successMsg: string | null = 'OK') { return { result: expect.any(Array), @@ -8,6 +10,15 @@ export function successResponseList(successMsg: string | null = 'OK') { }; } +export function successResponseListV3(successMsg: string | null = 'OK') { + return { + result: { + list: expect.any(Array), + }, + ...successEmptyResponseObjectV3(), + }; +} + export function successResponseObject(successMsg: string | null = 'OK') { return { result: expect.any(Object), @@ -26,9 +37,7 @@ export function successResponseObjectV3() { export function successEmptyResponseObjectV3() { return { retCode: API_ERROR_CODE.SUCCESS, - retMsg: expect.stringMatching( - /OK|SUCCESS|success|success\.|Request accepted|/gim - ), + retMsg: expect.stringMatching(SUCCESS_MSG_REGEX), }; } @@ -44,6 +53,16 @@ export function errorResponseObject( }; } +export function errorResponseObjectV3( + result: null | any = null, + retCode: number +) { + return { + result, + retCode: retCode, + }; +} + export function notAuthenticatedError() { return new Error('Private endpoints require api and private keys set'); } diff --git a/test/spot/private.v3.read.test.ts b/test/spot/private.v3.read.test.ts new file mode 100644 index 0000000..003b2dc --- /dev/null +++ b/test/spot/private.v3.read.test.ts @@ -0,0 +1,89 @@ +import { API_ERROR_CODE, SpotClientV3 } from '../../src'; +import { + errorResponseObject, + errorResponseObjectV3, + successEmptyResponseObjectV3, + successResponseListV3, + successResponseObjectV3, +} from '../response.util'; + +describe('Private Spot REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new SpotClientV3(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCUSDT'; + const interval = '15m'; + const ltCode = 'BTC3S'; + + it('getOrder()', async () => { + // No auth error == test pass + expect(await api.getOrder({ orderId: '123123' })).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_SPOT_V3, + }); + }); + + it('getOpenOrders()', async () => { + expect(await api.getOpenOrders()).toMatchObject(successResponseListV3()); + }); + + it('getPastOrders()', async () => { + expect(await api.getPastOrders()).toMatchObject(successResponseListV3()); + }); + + it('getMyTrades()', async () => { + expect(await api.getMyTrades()).toMatchObject(successResponseListV3()); + }); + + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject({ + result: { + balances: expect.any(Array), + }, + ...successEmptyResponseObjectV3(), + }); + }); + + it('getLeveragedTokenMarketInfo()', async () => { + expect(await api.getLeveragedTokenMarketInfo(ltCode)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getLeveragedTokenPRHistory()', async () => { + expect(await api.getLeveragedTokenPRHistory()).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_LEVERAGED_TOKEN, + }); + }); + + it('getCrossMarginBorrowingInfo()', async () => { + expect(await api.getCrossMarginBorrowingInfo()).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getCrossMarginAccountInfo()', async () => { + expect(await api.getCrossMarginAccountInfo()).toMatchObject({ + retCode: API_ERROR_CODE.QUERY_ACCOUNT_INFO_ERROR, + }); + }); + + it('getCrossMarginInterestQuota()', async () => { + expect(await api.getCrossMarginInterestQuota('USDT')).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getCrossMarginRepaymentHistory()', async () => { + expect(await api.getCrossMarginRepaymentHistory()).toMatchObject( + successResponseObjectV3() + ); + }); +}); From 1c9c6aa1e2b5d32c31694bd30c8e4365e9c94ac4 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 12:49:44 +0100 Subject: [PATCH 17/74] refactoring around test names spot --- ...e.read.test.ts => private.v1.read.test.ts} | 0 ...write.test.ts => private.v1.write.test.ts} | 0 test/spot/private.v3.write.test.ts | 54 +++++++++++++++++++ .../{public.test.ts => public.v1.test.ts} | 0 4 files changed, 54 insertions(+) rename test/spot/{private.read.test.ts => private.v1.read.test.ts} (100%) rename test/spot/{private.write.test.ts => private.v1.write.test.ts} (100%) create mode 100644 test/spot/private.v3.write.test.ts rename test/spot/{public.test.ts => public.v1.test.ts} (100%) diff --git a/test/spot/private.read.test.ts b/test/spot/private.v1.read.test.ts similarity index 100% rename from test/spot/private.read.test.ts rename to test/spot/private.v1.read.test.ts diff --git a/test/spot/private.write.test.ts b/test/spot/private.v1.write.test.ts similarity index 100% rename from test/spot/private.write.test.ts rename to test/spot/private.v1.write.test.ts diff --git a/test/spot/private.v3.write.test.ts b/test/spot/private.v3.write.test.ts new file mode 100644 index 0000000..4f3ef5e --- /dev/null +++ b/test/spot/private.v3.write.test.ts @@ -0,0 +1,54 @@ +import { API_ERROR_CODE, SpotClient } from '../../src'; +import { successResponseObject } from '../response.util'; + +describe('Private Inverse-Futures REST API POST Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new SpotClient(API_KEY, API_SECRET, useLivenet); + + // Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol! + const symbol = 'BTCUSDT'; + + // These tests are primarily check auth is working by expecting balance or order not found style errors + + it('submitOrder()', async () => { + expect( + await api.submitOrder({ + side: 'Buy', + symbol, + qty: 10000, + type: 'MARKET', + }) + ).toMatchObject({ + ret_code: API_ERROR_CODE.BALANCE_INSUFFICIENT_SPOT, + ret_msg: 'Balance insufficient ', + }); + }); + + it('cancelOrder()', async () => { + expect( + await api.cancelOrder({ + orderId: '1231231', + }) + ).toMatchObject({ + ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_SPOT, + ret_msg: 'Order does not exist.', + }); + }); + + it('cancelOrderBatch()', async () => { + expect( + await api.cancelOrderBatch({ + symbol, + orderTypes: ['LIMIT', 'LIMIT_MAKER'], + }) + ).toMatchObject(successResponseObject()); + }); +}); diff --git a/test/spot/public.test.ts b/test/spot/public.v1.test.ts similarity index 100% rename from test/spot/public.test.ts rename to test/spot/public.v1.test.ts From 66dcd09f18f59e2bc3b7fbf32d2733f95cc570a6 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 12:57:00 +0100 Subject: [PATCH 18/74] add test coverage for all spot v3 endpoints --- src/constants/enum.ts | 4 +++ test/spot/private.v1.read.test.ts | 3 -- test/spot/private.v3.write.test.ts | 47 +++++++++++++++++++++++------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/constants/enum.ts b/src/constants/enum.ts index 43c357a..cf7fd6f 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -17,9 +17,13 @@ export const API_ERROR_CODE = { /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, INCORRECT_API_KEY_PERMISSIONS: 10005, + BALANCE_INSUFFICIENT_SPOT_V3: 12131, ORDER_NOT_FOUND_SPOT_V3: 12213, ORDER_NOT_FOUND_LEVERAGED_TOKEN: 12407, + EXCEEDED_UPPER_LIMIT_LEVERAGED_TOKEN: 12409, QUERY_ACCOUNT_INFO_ERROR: 12602, + CROSS_MARGIN_USER_NOT_FOUND: 12607, + CROSS_MARGIN_REPAYMENT_NOT_REQUIRED: 12616, ORDER_NOT_FOUND_OR_TOO_LATE: 20001, POSITION_STATUS_NOT_NORMAL: 30013, CANNOT_SET_TRADING_STOP_FOR_ZERO_POS: 30024, diff --git a/test/spot/private.v1.read.test.ts b/test/spot/private.v1.read.test.ts index 6f61734..bbc6182 100644 --- a/test/spot/private.v1.read.test.ts +++ b/test/spot/private.v1.read.test.ts @@ -18,9 +18,6 @@ describe('Private Spot REST API Endpoints', () => { const api = new SpotClient(API_KEY, API_SECRET, useLivenet); - const symbol = 'BTCUSDT'; - const interval = '15m'; - it('getOrder()', async () => { // No auth error == test pass expect(await api.getOrder({ orderId: '123123' })).toMatchObject( diff --git a/test/spot/private.v3.write.test.ts b/test/spot/private.v3.write.test.ts index 4f3ef5e..b226292 100644 --- a/test/spot/private.v3.write.test.ts +++ b/test/spot/private.v3.write.test.ts @@ -1,5 +1,8 @@ -import { API_ERROR_CODE, SpotClient } from '../../src'; -import { successResponseObject } from '../response.util'; +import { API_ERROR_CODE, SpotClientV3 } from '../../src'; +import { + successResponseObject, + successResponseObjectV3, +} from '../response.util'; describe('Private Inverse-Futures REST API POST Endpoints', () => { const useLivenet = true; @@ -11,10 +14,10 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new SpotClient(API_KEY, API_SECRET, useLivenet); + const api = new SpotClientV3(API_KEY, API_SECRET, useLivenet); - // Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol! const symbol = 'BTCUSDT'; + const ltCode = 'BTC3S'; // These tests are primarily check auth is working by expecting balance or order not found style errors @@ -23,12 +26,11 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { await api.submitOrder({ side: 'Buy', symbol, - qty: 10000, - type: 'MARKET', + orderQty: '10000', + orderType: 'MARKET', }) ).toMatchObject({ - ret_code: API_ERROR_CODE.BALANCE_INSUFFICIENT_SPOT, - ret_msg: 'Balance insufficient ', + retCode: API_ERROR_CODE.BALANCE_INSUFFICIENT_SPOT_V3, }); }); @@ -38,8 +40,7 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { orderId: '1231231', }) ).toMatchObject({ - ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_SPOT, - ret_msg: 'Order does not exist.', + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_SPOT_V3, }); }); @@ -49,6 +50,30 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { symbol, orderTypes: ['LIMIT', 'LIMIT_MAKER'], }) - ).toMatchObject(successResponseObject()); + ).toMatchObject(successResponseObjectV3()); + }); + + it('purchaseLeveragedToken()', async () => { + expect(await api.purchaseLeveragedToken(ltCode, '1')).toMatchObject({ + retCode: API_ERROR_CODE.EXCEEDED_UPPER_LIMIT_LEVERAGED_TOKEN, + }); + }); + + it('redeemLeveragedToken()', async () => { + expect(await api.redeemLeveragedToken(ltCode, '1')).toMatchObject({ + retCode: 12426, // unknown error code, not listed in docs yet + }); + }); + + it('borrowCrossMarginLoan()', async () => { + expect(await api.borrowCrossMarginLoan('USDT', '1')).toMatchObject({ + retCode: API_ERROR_CODE.CROSS_MARGIN_USER_NOT_FOUND, + }); + }); + + it('repayCrossMarginLoan()', async () => { + expect(await api.repayCrossMarginLoan('USDT', '1')).toMatchObject({ + retCode: API_ERROR_CODE.CROSS_MARGIN_REPAYMENT_NOT_REQUIRED, + }); }); }); From 9d59d7074af50051ca549b8dc09b75826920303a Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 13:00:38 +0100 Subject: [PATCH 19/74] fix tests from refactoring --- test/response.util.ts | 2 +- test/usdc/options/private.read.test.ts | 29 ++++++++++------------- test/usdc/options/private.write.test.ts | 4 ++-- test/usdc/options/public.read.test.ts | 14 +++++------ test/usdc/perpetual/private.read.test.ts | 24 +++++++++---------- test/usdc/perpetual/private.write.test.ts | 8 +++---- test/usdc/perpetual/public.read.test.ts | 26 ++++++++++---------- 7 files changed, 50 insertions(+), 57 deletions(-) diff --git a/test/response.util.ts b/test/response.util.ts index 760e352..2804b66 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -10,7 +10,7 @@ export function successResponseList(successMsg: string | null = 'OK') { }; } -export function successResponseListV3(successMsg: string | null = 'OK') { +export function successResponseListV3() { return { result: { list: expect.any(Array), diff --git a/test/usdc/options/private.read.test.ts b/test/usdc/options/private.read.test.ts index 1535304..04f446e 100644 --- a/test/usdc/options/private.read.test.ts +++ b/test/usdc/options/private.read.test.ts @@ -1,8 +1,5 @@ import { USDCOptionClient } from '../../../src'; -import { - successResponseObject, - successUSDCResponseObject, -} from '../../response.util'; +import { successResponseObjectV3 } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { const useLivenet = true; @@ -20,63 +17,61 @@ describe('Private Account Asset REST API Endpoints', () => { it('getActiveRealtimeOrders()', async () => { expect(await api.getActiveRealtimeOrders()).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getActiveOrders()', async () => { expect(await api.getActiveOrders({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getHistoricOrders()', async () => { expect(await api.getHistoricOrders({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getOrderExecutionHistory()', async () => { expect(await api.getOrderExecutionHistory({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getTransactionLog()', async () => { expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getBalances()', async () => { - expect(await api.getBalances()).toMatchObject(successUSDCResponseObject()); + expect(await api.getBalances()).toMatchObject(successResponseObjectV3()); }); it('getAssetInfo()', async () => { - expect(await api.getAssetInfo()).toMatchObject(successUSDCResponseObject()); + expect(await api.getAssetInfo()).toMatchObject(successResponseObjectV3()); }); it('getMarginMode()', async () => { - expect(await api.getMarginMode()).toMatchObject( - successUSDCResponseObject() - ); + expect(await api.getMarginMode()).toMatchObject(successResponseObjectV3()); }); it('getPositions()', async () => { expect(await api.getPositions({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getDeliveryHistory()', async () => { expect(await api.getDeliveryHistory({ symbol })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getPositionsInfoUponExpiry()', async () => { expect(await api.getPositionsInfoUponExpiry()).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); }); diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts index 2949d0e..7a6f9c8 100644 --- a/test/usdc/options/private.write.test.ts +++ b/test/usdc/options/private.write.test.ts @@ -1,5 +1,5 @@ import { API_ERROR_CODE, USDCOptionClient } from '../../../src'; -import { successUSDCResponseObject } from '../../response.util'; +import { successResponseObjectV3 } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { const useLivenet = true; @@ -132,7 +132,7 @@ describe('Private Account Asset REST API Endpoints', () => { it('setMarginMode()', async () => { expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); diff --git a/test/usdc/options/public.read.test.ts b/test/usdc/options/public.read.test.ts index 97ff6bd..891082c 100644 --- a/test/usdc/options/public.read.test.ts +++ b/test/usdc/options/public.read.test.ts @@ -1,7 +1,7 @@ import { USDCOptionClient } from '../../../src'; import { successResponseObject, - successUSDCResponseObject, + successResponseObjectV3, } from '../../response.util'; describe('Public USDC Options REST API Endpoints', () => { @@ -14,37 +14,37 @@ describe('Public USDC Options REST API Endpoints', () => { it('getOrderBook()', async () => { expect(await api.getOrderBook(symbol)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getContractInfo()', async () => { expect(await api.getContractInfo()).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getSymbolTicker()', async () => { expect(await api.getSymbolTicker(symbol)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getDeliveryPrice()', async () => { expect(await api.getDeliveryPrice()).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getLast500Trades()', async () => { expect(await api.getLast500Trades({ category: 'OPTION' })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getHistoricalVolatility()', async () => { expect(await api.getHistoricalVolatility()).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); diff --git a/test/usdc/perpetual/private.read.test.ts b/test/usdc/perpetual/private.read.test.ts index 23e5121..ae86d70 100644 --- a/test/usdc/perpetual/private.read.test.ts +++ b/test/usdc/perpetual/private.read.test.ts @@ -1,5 +1,5 @@ import { USDCPerpetualClient } from '../../../src'; -import { successUSDCResponseObject } from '../../response.util'; +import { successResponseObjectV3 } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { const useLivenet = true; @@ -18,57 +18,55 @@ describe('Private Account Asset REST API Endpoints', () => { it('getActiveOrders()', async () => { expect(await api.getActiveOrders({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getHistoricOrders()', async () => { expect(await api.getHistoricOrders({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getOrderExecutionHistory()', async () => { expect(await api.getOrderExecutionHistory({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getTransactionLog()', async () => { expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getBalances()', async () => { - expect(await api.getBalances()).toMatchObject(successUSDCResponseObject()); + expect(await api.getBalances()).toMatchObject(successResponseObjectV3()); }); it('getAssetInfo()', async () => { - expect(await api.getAssetInfo()).toMatchObject(successUSDCResponseObject()); + expect(await api.getAssetInfo()).toMatchObject(successResponseObjectV3()); }); it('getMarginMode()', async () => { - expect(await api.getMarginMode()).toMatchObject( - successUSDCResponseObject() - ); + expect(await api.getMarginMode()).toMatchObject(successResponseObjectV3()); }); it('getPositions()', async () => { expect(await api.getPositions({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getSettlementHistory()', async () => { expect(await api.getSettlementHistory({ symbol })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getPredictedFundingRate()', async () => { expect(await api.getPredictedFundingRate(symbol)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); }); diff --git a/test/usdc/perpetual/private.write.test.ts b/test/usdc/perpetual/private.write.test.ts index ced7af4..97637a0 100644 --- a/test/usdc/perpetual/private.write.test.ts +++ b/test/usdc/perpetual/private.write.test.ts @@ -1,7 +1,7 @@ import { API_ERROR_CODE, USDCPerpetualClient } from '../../../src'; import { - successUSDCEmptyResponseObject, - successUSDCResponseObject, + successEmptyResponseObjectV3, + successResponseObjectV3, } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { @@ -62,13 +62,13 @@ describe('Private Account Asset REST API Endpoints', () => { it('cancelActiveOrders()', async () => { expect(await api.cancelActiveOrders(symbol, 'Order')).toMatchObject( - successUSDCEmptyResponseObject() + successEmptyResponseObjectV3() ); }); it('setMarginMode()', async () => { expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); diff --git a/test/usdc/perpetual/public.read.test.ts b/test/usdc/perpetual/public.read.test.ts index 60ce93e..68a902a 100644 --- a/test/usdc/perpetual/public.read.test.ts +++ b/test/usdc/perpetual/public.read.test.ts @@ -1,7 +1,7 @@ import { USDCKlineRequest, USDCPerpetualClient } from '../../../src'; import { successResponseObject, - successUSDCResponseObject, + successResponseObjectV3, } from '../../response.util'; describe('Public USDC Options REST API Endpoints', () => { @@ -19,73 +19,73 @@ describe('Public USDC Options REST API Endpoints', () => { it('getOrderBook()', async () => { expect(await api.getOrderBook(symbol)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getContractInfo()', async () => { expect(await api.getContractInfo()).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getSymbolTicker()', async () => { expect(await api.getSymbolTicker(symbol)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getCandles()', async () => { expect(await api.getCandles(candleRequest)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getMarkPrice()', async () => { expect(await api.getMarkPrice(candleRequest)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getIndexPrice()', async () => { expect(await api.getIndexPrice(candleRequest)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getIndexPremium()', async () => { expect(await api.getIndexPremium(candleRequest)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getOpenInterest()', async () => { expect(await api.getOpenInterest({ symbol, period: '1m' })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getLargeOrders()', async () => { expect(await api.getLargeOrders({ symbol })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getLongShortRatio()', async () => { expect(await api.getLongShortRatio({ symbol, period: '1m' })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getLast500Trades()', async () => { expect(await api.getLast500Trades({ category })).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); it('getLastFundingRate()', async () => { expect(await api.getLastFundingRate(symbol)).toMatchObject( - successUSDCResponseObject() + successResponseObjectV3() ); }); From 1fd73f4d0ed852a11e4a1e41a19cf7d7f46dd5bc Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 13:04:17 +0100 Subject: [PATCH 20/74] readme updates --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4524667..95086d4 100644 --- a/README.md +++ b/README.md @@ -51,19 +51,19 @@ This connector is fully compatible with both TypeScript and pure JavaScript proj --- ## REST API Clients 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 | -|:-----------------------------------------------------: |:-----------------------------------------------------------------------------------------------------------: | -| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | -| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | -| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | -| [SpotClient](src/spot-client.ts) | [Spot Market (v1) APIs](https://bybit-exchange.github.io/docs/spot/v1/#t-introduction) | -| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | -| [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | -| [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | -| Copy Trading | Under Development | -| Spot v3 | Under Development | -| Derivatives V3 unified margin | Under Development | -| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | +| Class | Description | +|:------------------------------------------------------------------: |:-----------------------------------------------------------------------------------------------------------: | +| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | +| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | +| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | +| [SpotClientV3](src/spot-client-v3.ts) | [Spot Market (v3) APIs](https://bybit-exchange.github.io/docs/spot/v3/#t-introduction) | +| [~SpotClient~](src/spot-client.ts) (deprecated, v3 client recommended)| [Spot Market (v1) APIs](https://bybit-exchange.github.io/docs/spot/v1/#t-introduction) | +| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | +| [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | +| [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | +| [CopyTradingClient](src/copy-trading-client.ts) | [Copy Trading APIs](https://bybit-exchange.github.io/docs/copy_trading/#t-introduction) | +| Derivatives V3 unified margin | Under Development | +| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | Examples for using each client can be found in: - the [examples](./examples) folder. @@ -86,8 +86,10 @@ const { LinearClient, InverseFuturesClient, SpotClient, + SpotClient3, USDCOptionClient, USDCPerpetualClient, + CopyTradingClient, AccountAssetClient, } = require('bybit-api'); From 0cbdc5351ca2c4c56563ee9884e365361ac8e437 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 18:57:48 +0100 Subject: [PATCH 21/74] add unified margin client with request types --- README.md | 26 +- src/types/request/index.ts | 1 + src/types/request/unified-margin.ts | 247 ++++++++++++++++++ src/types/request/usdc-perp.ts | 9 +- src/types/request/usdc-shared.ts | 3 + src/types/shared.ts | 15 ++ src/unified-margin-client.ts | 391 ++++++++++++++++++++++++++++ src/util/BaseRestClient.ts | 2 +- 8 files changed, 677 insertions(+), 17 deletions(-) create mode 100644 src/types/request/unified-margin.ts create mode 100644 src/unified-margin-client.ts diff --git a/README.md b/README.md index 95086d4..52a5852 100644 --- a/README.md +++ b/README.md @@ -51,19 +51,19 @@ This connector is fully compatible with both TypeScript and pure JavaScript proj --- ## REST API Clients 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 | -|:------------------------------------------------------------------: |:-----------------------------------------------------------------------------------------------------------: | -| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | -| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | -| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | -| [SpotClientV3](src/spot-client-v3.ts) | [Spot Market (v3) APIs](https://bybit-exchange.github.io/docs/spot/v3/#t-introduction) | -| [~SpotClient~](src/spot-client.ts) (deprecated, v3 client recommended)| [Spot Market (v1) APIs](https://bybit-exchange.github.io/docs/spot/v1/#t-introduction) | -| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | -| [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | -| [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | -| [CopyTradingClient](src/copy-trading-client.ts) | [Copy Trading APIs](https://bybit-exchange.github.io/docs/copy_trading/#t-introduction) | -| Derivatives V3 unified margin | Under Development | -| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | +| Class | Description | +|:------------------------------------------------------------------: |:----------------------------------------------------------------------------------------------------------------------------: | +| [InverseClient](src/inverse-client.ts) | [Inverse Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse/) | +| [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | +| [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | +| [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | +| [UnifiedMarginClient](src/unified-margin-client.ts) | [Derivatives (v3) unified margin APIs](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-introduction) | +| [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | +| [SpotClientV3](src/spot-client-v3.ts) | [Spot Market (v3) APIs](https://bybit-exchange.github.io/docs/spot/v3/#t-introduction) | +| [~SpotClient~](src/spot-client.ts) (deprecated, v3 client recommended)| [Spot Market (v1) APIs](https://bybit-exchange.github.io/docs/spot/v1/#t-introduction) | +| [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | +| [CopyTradingClient](src/copy-trading-client.ts) | [Copy Trading APIs](https://bybit-exchange.github.io/docs/copy_trading/#t-introduction) | +| [WebsocketClient](src/websocket-client.ts) | All WebSocket Events (Public & Private for all API categories) | Examples for using each client can be found in: - the [examples](./examples) folder. diff --git a/src/types/request/index.ts b/src/types/request/index.ts index 77aaef5..8ee5bfd 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -5,3 +5,4 @@ export * from './usdt-perp'; export * from './usdc-perp'; export * from './usdc-options'; export * from './usdc-shared'; +export * from './unified-margin'; diff --git a/src/types/request/unified-margin.ts b/src/types/request/unified-margin.ts new file mode 100644 index 0000000..29c8e74 --- /dev/null +++ b/src/types/request/unified-margin.ts @@ -0,0 +1,247 @@ +import { KlineIntervalV3, OrderSide } from '../shared'; +import { USDCOrderFilter, USDCTimeInForce } from './usdc-shared'; + +export type UMCategory = 'linear' | 'inverse' | 'option'; +export type UMOrderType = 'Limit' | 'Market'; +export type UMDirection = 'prev' | 'next'; + +export interface UMCandlesRequest { + category: UMCategory; + symbol: string; + interval: KlineIntervalV3; + start: number; + end: number; + limit?: number; +} + +export interface UMInstrumentInfoRequest { + category: UMCategory; + symbol?: string; + baseCoin?: string; + limit?: string; + cursor?: string; +} + +export interface UMFundingRateHistoryRequest { + category: UMCategory; + symbol: string; + startTime?: number; + endTime?: number; + limit?: number; +} + +export interface UMOptionDeliveryPriceRequest { + category: UMCategory; + symbol?: string; + baseCoin?: string; + direction?: UMDirection; + limit?: string; + cursor?: string; +} + +export interface UMPublicTradesRequest { + category: UMCategory; + symbol: string; + baseCoin?: string; + optionType?: 'Call' | 'Put'; + limit?: string; +} + +export interface UMOpenInterestRequest { + category: UMCategory; + symbol: string; + interval: '5min' | '15min' | '30min' | '1h' | '4h' | '1d'; + startTime?: number; + endTime?: number; + limit?: number; +} + +export interface UMOrderRequest { + category: UMCategory; + symbol: string; + side: OrderSide; + positionIdx?: '0'; + orderType: UMOrderType; + qty: string; + price?: string; + basePrice?: string; + triggerPrice?: string; + triggerBy?: string; + iv?: string; + timeInForce: USDCTimeInForce; + orderLinkId?: string; + takeProfit?: number; + stopLoss?: number; + tpTriggerBy?: string; + slTriggerBy?: string; + reduceOnly?: boolean; + closeOnTrigger?: boolean; + mmp?: boolean; +} + +export interface UMModifyOrderRequest { + category: UMCategory; + symbol: string; + orderId?: string; + orderLinkId?: string; + iv?: string; + triggerPrice?: string; + qty?: string; + price?: string; + takeProfit?: number; + stopLoss?: number; + tpTriggerBy?: string; + slTriggerBy?: string; + triggerBy?: string; +} + +export interface UMCancelOrderRequest { + category: UMCategory; + symbol: string; + orderId?: string; + orderLinkId?: string; + orderFilter?: USDCOrderFilter; +} + +export interface UMActiveOrdersRequest { + category: UMCategory; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + orderFilter?: USDCOrderFilter; + direction?: UMDirection; + limit?: number; + cursor?: string; +} + +export interface UMHistoricOrdersRequest { + category: UMCategory; + symbol?: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + orderStatus?: string; + orderFilter?: USDCOrderFilter; + direction?: UMDirection; + limit?: number; + cursor?: string; +} + +export interface UMBatchOrder { + symbol: string; + side: OrderSide; + positionIdx?: '0'; + orderType: UMOrderType; + qty: string; + price?: string; + iv?: string; + timeInForce: USDCTimeInForce; + orderLinkId?: string; + reduceOnly?: boolean; + closeOnTrigger?: boolean; + mmp?: boolean; +} + +export interface UMBatchOrderReplace { + symbol: string; + orderId?: string; + orderLinkId?: string; + iv?: string; + qty?: string; + price?: string; +} + +export interface UMBatchOrderCancel { + symbol: string; + orderId?: string; + orderLinkId?: string; +} + +export interface UMCancelAllOrdersRequest { + category: UMCategory; + baseCoin?: string; + settleCoin?: string; + symbol?: string; + orderFilter?: USDCOrderFilter; +} + +export interface UMPositionsRequest { + category: UMCategory; + symbol?: string; + baseCoin?: string; + direction?: UMDirection; + limit?: number; + cursor?: string; +} + +export interface UMSetTPSLRequest { + category: UMCategory; + symbol: string; + takeProfit?: string; + stopLoss?: string; + trailingStop?: string; + tpTriggerBy?: string; + slTriggerBy?: string; + activePrice?: string; + slSize?: string; + tpSize?: string; + positionIdx?: '0'; +} + +export interface UM7DayTradingHistoryRequest { + category: UMCategory; + symbol: string; + baseCoin?: string; + orderId?: string; + orderLinkId?: string; + startTime?: number; + endTime?: number; + direction?: UMDirection; + limit?: number; + cursor?: string; + execType?: string; +} + +export interface UMOptionsSettlementHistoryRequest { + category: UMCategory; + symbol: string; + expDate?: string; + direction?: UMDirection; + limit?: number; + cursor?: string; +} + +export interface UMPerpSettlementHistoryRequest { + category: UMCategory; + symbol: string; + direction?: UMDirection; + limit?: number; + cursor?: string; +} + +export interface UMTransactionLogRequest { + category: UMCategory; + currency: string; + baseCoin?: string; + type?: string; + startTime?: number; + endTime?: number; + direction?: UMDirection; + limit?: number; + cursor?: string; +} + +export interface UMExchangeCoinsRequest { + fromCoin?: string; + toCoin?: string; +} + +export interface UMBorrowHistoryRequest { + currency: string; + startTime?: number; + endTime?: number; + direction?: UMDirection; + limit?: number; + cursor?: string; +} diff --git a/src/types/request/usdc-perp.ts b/src/types/request/usdc-perp.ts index 6f77789..553109a 100644 --- a/src/types/request/usdc-perp.ts +++ b/src/types/request/usdc-perp.ts @@ -1,5 +1,10 @@ import { OrderSide } from '../shared'; -import { USDCAPICategory, USDCOrderType, USDCTimeInForce } from './usdc-shared'; +import { + USDCAPICategory, + USDCOrderFilter, + USDCOrderType, + USDCTimeInForce, +} from './usdc-shared'; export interface USDCOpenInterestRequest { symbol: string; @@ -27,8 +32,6 @@ export interface USDCSymbolDirectionLimitCursor { cursor?: string; } -export type USDCOrderFilter = 'Order' | 'StopOrder'; - export interface USDCPerpOrderRequest { symbol: string; orderType: USDCOrderType; diff --git a/src/types/request/usdc-shared.ts b/src/types/request/usdc-shared.ts index f8faf59..4c388c2 100644 --- a/src/types/request/usdc-shared.ts +++ b/src/types/request/usdc-shared.ts @@ -1,12 +1,15 @@ export type USDCAPICategory = 'PERPETUAL' | 'OPTION'; export type USDCOrderType = 'Limit' | 'Market'; + export type USDCTimeInForce = | 'GoodTillCancel' | 'ImmediateOrCancel' | 'FillOrKill' | 'PostOnly'; +export type USDCOrderFilter = 'Order' | 'StopOrder'; + export interface USDCKlineRequest { symbol: string; period: string; diff --git a/src/types/shared.ts b/src/types/shared.ts index fdb7ced..88f465a 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -17,6 +17,21 @@ export type KlineInterval = | '1w' | '1M'; +export type KlineIntervalV3 = + | '1' + | '3' + | '5' + | '15' + | '30' + | '60' + | '120' + | '240' + | '360' + | '720' + | 'D' + | 'W' + | 'M'; + export interface APIResponse { ret_code: number; ret_msg: 'OK' | string; diff --git a/src/unified-margin-client.ts b/src/unified-margin-client.ts new file mode 100644 index 0000000..9c5b045 --- /dev/null +++ b/src/unified-margin-client.ts @@ -0,0 +1,391 @@ +import { + APIResponseWithTime, + APIResponseV3, + UMCategory, + UMCandlesRequest, + UMInstrumentInfoRequest, + UMFundingRateHistoryRequest, + UMOptionDeliveryPriceRequest, + UMPublicTradesRequest, + UMOpenInterestRequest, + UMOrderRequest, + UMModifyOrderRequest, + UMCancelOrderRequest, + UMActiveOrdersRequest, + UMHistoricOrdersRequest, + UMBatchOrder, + UMBatchOrderReplace, + UMBatchOrderCancel, + UMCancelAllOrdersRequest, + UMPositionsRequest, + UMSetTPSLRequest, + UM7DayTradingHistoryRequest, + UMOptionsSettlementHistoryRequest, + UMPerpSettlementHistoryRequest, + UMTransactionLogRequest, + InternalTransferRequest, + UMExchangeCoinsRequest, + UMBorrowHistoryRequest, +} from './types'; +import { REST_CLIENT_TYPE_ENUM } from './util'; +import BaseRestClient from './util/BaseRestClient'; + +/** + * REST API client for Derivatives V3 unified margin APIs + */ +export class UnifiedMarginClient extends BaseRestClient { + getClientType() { + return REST_CLIENT_TYPE_ENUM.v3; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Market Data Endpoints + * + */ + + /** Query order book info. Each side has a depth of 25 orders. */ + getOrderBook( + symbol: string, + category?: string, + limit?: number + ): Promise> { + return this.get('/derivatives/v3/public/order-book/L2', { + category, + symbol, + limit, + }); + } + + /** Get candles/klines */ + getCandles(params: UMCandlesRequest): Promise> { + return this.get('/derivatives/v3/public/kline', params); + } + + /** Get a symbol price/statistics ticker */ + getSymbolTicker( + category: UMCategory, + symbol?: string + ): Promise> { + return this.get('/derivatives/v3/public/tickers', { category, symbol }); + } + + /** Get trading rules per symbol/contract, incl price/amount/value/leverage filters */ + getInstrumentInfo( + params: UMInstrumentInfoRequest + ): Promise> { + return this.get('/derivatives/v3/public/instruments-info', params); + } + + /** Query mark price kline (like getCandles() but for mark price). */ + getMarkPriceCandles(params: UMCandlesRequest): Promise> { + return this.get('/derivatives/v3/public/mark-price-kline', params); + } + + /** Query Index Price Kline */ + getIndexPriceCandles(params: UMCandlesRequest): Promise> { + return this.get('/derivatives/v3/public/index-price-kline', params); + } + + /** + * The funding rate is generated every 8 hours at 00:00 UTC, 08:00 UTC and 16:00 UTC. + * For example, if a request is sent at 12:00 UTC, the funding rate generated earlier that day at 08:00 UTC will be sent. + */ + getFundingRateHistory( + params: UMFundingRateHistoryRequest + ): Promise> { + return this.get( + '/derivatives/v3/public/funding/history-funding-rate', + params + ); + } + + /** Get Risk Limit */ + getRiskLimit( + category: UMCategory, + symbol: string + ): Promise> { + return this.get('/derivatives/v3/public/risk-limit/list', { + category, + symbol, + }); + } + + /** Get option delivery price */ + getOptionDeliveryPrice( + params: UMOptionDeliveryPriceRequest + ): Promise> { + return this.get('/derivatives/v3/public/delivery-price', params); + } + + /** Get recent trades */ + getTrades(params: UMPublicTradesRequest): Promise> { + return this.get('/derivatives/v3/public/recent-trade', params); + } + + /** + * Gets the total amount of unsettled contracts. + * In other words, the total number of contracts held in open positions. + */ + getOpenInterest(params: UMOpenInterestRequest): Promise> { + return this.get('/derivatives/v3/public/open-interest', params); + } + + /** + * + * Unified Margin Account Endpoints + * + */ + + /** -> Order API */ + + /** Place an order */ + submitOrder(params: UMOrderRequest): Promise> { + return this.postPrivate('/unified/v3/private/order/create', params); + } + + /** Active order parameters (such as quantity, price) and stop order parameters cannot be modified in one request at the same time. Please request modification separately. */ + modifyOrder(params: UMModifyOrderRequest): Promise> { + return this.postPrivate('/unified/v3/private/order/replace', params); + } + + /** Cancel order */ + cancelOrder(params: UMCancelOrderRequest): Promise> { + return this.postPrivate('/unified/v3/private/order/cancel', params); + } + + /** Query Open Orders */ + getActiveOrders(params: UMActiveOrdersRequest): Promise> { + return this.getPrivate('/unified/v3/private/order/unfilled-orders', params); + } + + /** 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> { + return this.getPrivate('/unified/v3/private/order/list', params); + } + + /** + * This API provides the batch order mode under the unified margin account. + * Max 10 per request + */ + batchSubmitOrders( + category: UMCategory, + orders: UMBatchOrder[] + ): Promise> { + return this.postPrivate('/unified/v3/private/order/create-batch', { + category, + request: orders, + }); + } + + /** + * This interface can modify the open order information in batches. + * Currently, it is not supported to modify the conditional order information. + * Please note that only unfilled or partial filled orders can be modified. + * If both futures and options orders are in one request, only the orders matching the category will be operated according to the category type + */ + batchReplaceOrders( + category: UMCategory, + orders: UMBatchOrderReplace[] + ): Promise> { + return this.postPrivate('/unified/v3/private/order/replace-batch', { + category, + request: orders, + }); + } + + /** + * This API provides batch cancellation under the unified margin account. + * Order cancellation of futures and options cannot be canceled in one request at the same time. + * If both futures and options orders are in one request, only the orders matching the category will be operated according to the category type. + */ + batchCancelOrders( + category: UMCategory, + orders: UMBatchOrderCancel[] + ): Promise> { + return this.postPrivate('/unified/v3/private/order/cancel-batch', { + category, + request: orders, + }); + } + + /** + * This API provides the cancellation of all open orders under the unified margin account. + * Order cancellation of futures and options cannot be canceled in one request at the same time. + * If both futures and options orders are in one request, only the orders matching the category will be operated according to the category type. + */ + cancelAllOrders( + params: UMCancelAllOrdersRequest + ): Promise> { + return this.postPrivate('/unified/v3/private/order/cancel-all', params); + } + + /** -> Positions API */ + + /** + * Query my positions real-time. Accessing personal list of positions. + * Users can access their position holding information through this interface, such as the number of position holdings and wallet balance. + */ + getPositions(params: UMPositionsRequest): Promise> { + return this.postPrivate('/unified/v3/private/position/list', params); + } + + /** Leverage setting. */ + setLeverage( + category: UMCategory, + symbol: string, + buyLeverage: number, + sellLeverage: number + ): Promise> { + return this.postPrivate('/unified/v3/private/position/set-leverage', { + category, + symbol, + buyLeverage, + sellLeverage, + }); + } + + /** + * Switching the TP/SL mode to the cross margin mode or selected positions. + * When you set the TP/SL mode on the selected positions, the quantity of take-profit or stop-loss orders can be smaller than the position size. Please use Trading-Stop endpoint. + */ + setTPSLMode( + category: UMCategory, + symbol: string, + tpSlMode: 1 | 0 + ): Promise> { + return this.postPrivate('/unified/v3/private/position/tpsl/switch-mode', { + category, + symbol, + tpSlMode, + }); + } + + /** Set risk limit */ + setRiskLimit( + category: UMCategory, + symbol: string, + riskId: number, + positionIdx: number + ): Promise> { + return this.postPrivate('/unified/v3/private/position/set-risk-limit', { + category, + symbol, + riskId, + positionIdx, + }); + } + + /** + * Set position TP/SL and trailing stop. + * Pass the following parameters, then the system will create conditional orders. + * If the position is closed, the system will cancel these orders, and adjust the position size. + */ + setTPSL(params: UMSetTPSLRequest): Promise> { + return this.postPrivate( + '/unified/v3/private/position/trading-stop', + params + ); + } + + /** + * Access the user's filled history, ranked by time in descending order. + * There might be multiple filled histories for an order. + */ + get7DayTradingHistory( + params: UM7DayTradingHistoryRequest + ): Promise> { + return this.getPrivate('/unified/v3/private/execution/list', params); + } + + /** Query the settlement history, ranked by time in descending order. */ + getOptionsSettlementHistory( + params: UMOptionsSettlementHistoryRequest + ): Promise> { + return this.getPrivate('/unified/v3/private/delivery-record', params); + } + + /** Query session settlement records, only for USDC perpetual */ + getUSDCPerpetualSettlementHistory( + params: UMPerpSettlementHistoryRequest + ): Promise> { + return this.getPrivate('/unified/v3/private/settlement-record', params); + } + + /** -> Account API */ + + /** Query wallet balance */ + getBalances(coin?: string): Promise> { + return this.getPrivate('/unified/v3/private/account/wallet/balance', { + coin, + }); + } + + /** Upgrade to unified margin account */ + upgradeToUnifiedMargin(): Promise> { + return this.postPrivate( + '/unified/v3/private/account/upgrade-unified-account' + ); + } + + /** Query trading history */ + getTransactionLog( + params: UMTransactionLogRequest + ): Promise> { + return this.getPrivate( + '/unified/v3/private/account/transaction-log', + params + ); + } + + /** Fund transfer between accounts (v2) */ + transferFunds(params: InternalTransferRequest): Promise> { + return this.postPrivate('/asset/v1/private/transfer', params); + } + + /** Exchange Coins */ + exchangeCoins(params?: UMExchangeCoinsRequest): Promise> { + return this.getPrivate( + '/asset/v2/private/exchange/exchange-order-all', + params + ); + } + + /** Get Borrow History */ + getBorrowHistory( + params?: UMBorrowHistoryRequest + ): Promise> { + return this.getPrivate( + '/unified/v3/private/account/borrow-history', + params + ); + } + + /** Get Borrow Rate */ + getBorrowRate(currency?: string): Promise> { + return this.getPrivate('/unified/v3/private/account/borrow-rate', { + currency, + }); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } + + getAnnouncements(): Promise> { + return this.get('/v2/public/announcement'); + } +} diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 6b9ad75..b5a7a13 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -270,7 +270,7 @@ export default abstract class BaseRestClient { params?: any, isPublicApi?: boolean ): Promise { - // Sanity check to make sure it's only ever signed by + // Sanity check to make sure it's only ever prefixed by one forward slash const requestUrl = [this.baseUrl, endpoint].join( endpoint.startsWith('/') ? '' : '/' ); From 8e60d5dfdf52201c1b13730524ee421526f22b11 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 19:07:58 +0100 Subject: [PATCH 22/74] test coverage for public unified margin endpoints --- src/index.ts | 1 + src/types/request/usdc-options.ts | 1 - src/unified-margin-client.ts | 2 +- test/unified-margin/private.read.test.ts | 72 +++++++++++++++ test/unified-margin/private.write.test.ts | 86 +++++++++++++++++ test/unified-margin/public.read.test.ts | 108 ++++++++++++++++++++++ 6 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 test/unified-margin/private.read.test.ts create mode 100644 test/unified-margin/private.write.test.ts create mode 100644 test/unified-margin/public.read.test.ts diff --git a/src/index.ts b/src/index.ts index 44392ca..1325998 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export * from './spot-client'; export * from './spot-client-v3'; export * from './usdc-option-client'; export * from './usdc-perpetual-client'; +export * from './unified-margin-client'; export * from './websocket-client'; export * from './util/logger'; export * from './types'; diff --git a/src/types/request/usdc-options.ts b/src/types/request/usdc-options.ts index 0ba1971..125f766 100644 --- a/src/types/request/usdc-options.ts +++ b/src/types/request/usdc-options.ts @@ -1,5 +1,4 @@ import { OrderSide } from '../shared'; -import { USDCOrderFilter } from './usdc-perp'; import { USDCAPICategory, USDCOrderType, USDCTimeInForce } from './usdc-shared'; export interface USDCOptionsContractInfoRequest { diff --git a/src/unified-margin-client.ts b/src/unified-margin-client.ts index 9c5b045..3491833 100644 --- a/src/unified-margin-client.ts +++ b/src/unified-margin-client.ts @@ -52,7 +52,7 @@ export class UnifiedMarginClient extends BaseRestClient { /** Query order book info. Each side has a depth of 25 orders. */ getOrderBook( symbol: string, - category?: string, + category: string, limit?: number ): Promise> { return this.get('/derivatives/v3/public/order-book/L2', { diff --git a/test/unified-margin/private.read.test.ts b/test/unified-margin/private.read.test.ts new file mode 100644 index 0000000..ae86d70 --- /dev/null +++ b/test/unified-margin/private.read.test.ts @@ -0,0 +1,72 @@ +import { USDCPerpetualClient } from '../../../src'; +import { successResponseObjectV3 } from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + const category = 'PERPETUAL'; + + it('getActiveOrders()', async () => { + expect(await api.getActiveOrders({ category })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getHistoricOrders()', async () => { + expect(await api.getHistoricOrders({ category })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getOrderExecutionHistory()', async () => { + expect(await api.getOrderExecutionHistory({ category })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getTransactionLog()', async () => { + expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject(successResponseObjectV3()); + }); + + it('getAssetInfo()', async () => { + expect(await api.getAssetInfo()).toMatchObject(successResponseObjectV3()); + }); + + it('getMarginMode()', async () => { + expect(await api.getMarginMode()).toMatchObject(successResponseObjectV3()); + }); + + it('getPositions()', async () => { + expect(await api.getPositions({ category })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getSettlementHistory()', async () => { + expect(await api.getSettlementHistory({ symbol })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getPredictedFundingRate()', async () => { + expect(await api.getPredictedFundingRate(symbol)).toMatchObject( + successResponseObjectV3() + ); + }); +}); diff --git a/test/unified-margin/private.write.test.ts b/test/unified-margin/private.write.test.ts new file mode 100644 index 0000000..97637a0 --- /dev/null +++ b/test/unified-margin/private.write.test.ts @@ -0,0 +1,86 @@ +import { API_ERROR_CODE, USDCPerpetualClient } from '../../../src'; +import { + successEmptyResponseObjectV3, + successResponseObjectV3, +} from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + + it('submitOrder()', async () => { + expect( + await api.submitOrder({ + symbol, + side: 'Sell', + orderType: 'Limit', + orderFilter: 'Order', + orderQty: '1', + orderPrice: '20000', + orderLinkId: Date.now().toString(), + timeInForce: 'GoodTillCancel', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST, + }); + }); + + it('modifyOrder()', async () => { + expect( + await api.modifyOrder({ + symbol, + orderId: 'somethingFake', + orderPrice: '20000', + orderFilter: 'Order', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + }); + }); + + it('cancelOrder()', async () => { + expect( + await api.cancelOrder({ + symbol, + orderId: 'somethingFake1', + orderFilter: 'Order', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + }); + }); + + it('cancelActiveOrders()', async () => { + expect(await api.cancelActiveOrders(symbol, 'Order')).toMatchObject( + successEmptyResponseObjectV3() + ); + }); + + it('setMarginMode()', async () => { + expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( + successResponseObjectV3() + ); + }); + + it('setLeverage()', async () => { + expect(await api.setLeverage(symbol, '10')).toMatchObject({ + retCode: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED, + }); + }); + + it('setRiskLimit()', async () => { + expect(await api.setRiskLimit(symbol, 1)).toMatchObject({ + retCode: API_ERROR_CODE.RISK_LIMIT_NOT_EXISTS, + }); + }); +}); diff --git a/test/unified-margin/public.read.test.ts b/test/unified-margin/public.read.test.ts new file mode 100644 index 0000000..26a6ab3 --- /dev/null +++ b/test/unified-margin/public.read.test.ts @@ -0,0 +1,108 @@ +import { + USDCKlineRequest, + UnifiedMarginClient, + UMCandlesRequest, +} from '../../src'; +import { + successResponseObject, + successResponseObjectV3, +} from '../response.util'; + +describe('Public USDC Options REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = undefined; + const API_SECRET = undefined; + + const api = new UnifiedMarginClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCUSDT'; + const category = 'linear'; + const start = Number((Date.now() / 1000).toFixed(0)); + const end = start + 1000 * 60 * 60 * 24; + const interval = '1'; + + const candleRequest: UMCandlesRequest = { + category, + symbol, + interval, + start, + end, + }; + + it('getOrderBook()', async () => { + expect(await api.getOrderBook(symbol, category)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getCandles()', async () => { + expect(await api.getCandles(candleRequest)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getSymbolTicker()', async () => { + expect(await api.getSymbolTicker(category)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getInstrumentInfo()', async () => { + expect(await api.getInstrumentInfo({ category })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getMarkPrice()', async () => { + expect(await api.getMarkPriceCandles(candleRequest)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getIndexPrice()', async () => { + expect(await api.getIndexPriceCandles(candleRequest)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getLastFundingRate()', async () => { + expect( + await api.getFundingRateHistory({ + category, + symbol, + }) + ).toMatchObject(successResponseObjectV3()); + }); + + it('getRiskLimit()', async () => { + expect(await api.getRiskLimit(category, symbol)).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getOptionDeliveryPrice()', async () => { + expect(await api.getOptionDeliveryPrice({ category })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getTrades()', async () => { + expect(await api.getTrades({ category, symbol })).toMatchObject( + successResponseObjectV3() + ); + }); + + it('getOpenInterest()', async () => { + expect( + await api.getOpenInterest({ symbol, category, interval: '5min' }) + ).toMatchObject(successResponseObjectV3()); + }); + + it('getServerTime()', async () => { + expect(await api.getServerTime()).toMatchObject(successResponseObject()); + }); + + it('getAnnouncements()', async () => { + expect(await api.getAnnouncements()).toMatchObject(successResponseObject()); + }); +}); From 8d85bcb1826999edc4df0e3021ad3b04d7c86c50 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 19:35:17 +0100 Subject: [PATCH 23/74] private unified read tests --- src/constants/enum.ts | 2 + src/types/request/unified-margin.ts | 4 +- src/unified-margin-client.ts | 6 +- test/unified-margin/private.read.test.ts | 106 +++++++++++++---------- test/unified-margin/public.read.test.ts | 6 +- 5 files changed, 71 insertions(+), 53 deletions(-) diff --git a/src/constants/enum.ts b/src/constants/enum.ts index cf7fd6f..18de262 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -17,6 +17,8 @@ export const API_ERROR_CODE = { /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, INCORRECT_API_KEY_PERMISSIONS: 10005, + /** Account not unified margin, update required */ + ACCOUNT_NOT_UNIFIED: 10020, BALANCE_INSUFFICIENT_SPOT_V3: 12131, ORDER_NOT_FOUND_SPOT_V3: 12213, ORDER_NOT_FOUND_LEVERAGED_TOKEN: 12407, diff --git a/src/types/request/unified-margin.ts b/src/types/request/unified-margin.ts index 29c8e74..85eb3b3 100644 --- a/src/types/request/unified-margin.ts +++ b/src/types/request/unified-margin.ts @@ -205,7 +205,7 @@ export interface UM7DayTradingHistoryRequest { export interface UMOptionsSettlementHistoryRequest { category: UMCategory; - symbol: string; + symbol?: string; expDate?: string; direction?: UMDirection; limit?: number; @@ -214,7 +214,7 @@ export interface UMOptionsSettlementHistoryRequest { export interface UMPerpSettlementHistoryRequest { category: UMCategory; - symbol: string; + symbol?: string; direction?: UMDirection; limit?: number; cursor?: string; diff --git a/src/unified-margin-client.ts b/src/unified-margin-client.ts index 3491833..c52be5b 100644 --- a/src/unified-margin-client.ts +++ b/src/unified-margin-client.ts @@ -234,7 +234,7 @@ export class UnifiedMarginClient extends BaseRestClient { * Users can access their position holding information through this interface, such as the number of position holdings and wallet balance. */ getPositions(params: UMPositionsRequest): Promise> { - return this.postPrivate('/unified/v3/private/position/list', params); + return this.getPrivate('/unified/v3/private/position/list', params); } /** Leverage setting. */ @@ -351,7 +351,9 @@ export class UnifiedMarginClient extends BaseRestClient { } /** Exchange Coins */ - exchangeCoins(params?: UMExchangeCoinsRequest): Promise> { + getCoinExchangeHistory( + params?: UMExchangeCoinsRequest + ): Promise> { return this.getPrivate( '/asset/v2/private/exchange/exchange-order-all', params diff --git a/test/unified-margin/private.read.test.ts b/test/unified-margin/private.read.test.ts index ae86d70..3620ed6 100644 --- a/test/unified-margin/private.read.test.ts +++ b/test/unified-margin/private.read.test.ts @@ -1,5 +1,5 @@ -import { USDCPerpetualClient } from '../../../src'; -import { successResponseObjectV3 } from '../../response.util'; +import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; +import { successResponseObjectV3 } from '../response.util'; describe('Private Account Asset REST API Endpoints', () => { const useLivenet = true; @@ -11,62 +11,80 @@ describe('Private Account Asset REST API Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + const api = new UnifiedMarginClient(API_KEY, API_SECRET, useLivenet); - const symbol = 'BTCPERP'; - const category = 'PERPETUAL'; + const symbol = 'BTCUSDT'; + const category = 'linear'; it('getActiveOrders()', async () => { - expect(await api.getActiveOrders({ category })).toMatchObject( - successResponseObjectV3() - ); + expect(await api.getActiveOrders({ category })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); }); it('getHistoricOrders()', async () => { - expect(await api.getHistoricOrders({ category })).toMatchObject( - successResponseObjectV3() - ); - }); - - it('getOrderExecutionHistory()', async () => { - expect(await api.getOrderExecutionHistory({ category })).toMatchObject( - successResponseObjectV3() - ); - }); - - it('getTransactionLog()', async () => { - expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( - successResponseObjectV3() - ); - }); - - it('getBalances()', async () => { - expect(await api.getBalances()).toMatchObject(successResponseObjectV3()); - }); - - it('getAssetInfo()', async () => { - expect(await api.getAssetInfo()).toMatchObject(successResponseObjectV3()); - }); - - it('getMarginMode()', async () => { - expect(await api.getMarginMode()).toMatchObject(successResponseObjectV3()); + expect(await api.getHistoricOrders({ category })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); }); it('getPositions()', async () => { - expect(await api.getPositions({ category })).toMatchObject( + expect(await api.getPositions({ category })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('get7DayTradingHistory()', async () => { + expect(await api.get7DayTradingHistory({ category, symbol })).toMatchObject( + { + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + } + ); + }); + + it('getOptionsSettlementHistory()', async () => { + expect(await api.getOptionsSettlementHistory({ category })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('getUSDCPerpetualSettlementHistory()', async () => { + expect( + await api.getUSDCPerpetualSettlementHistory({ category }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('getBalances()', async () => { + expect(await api.getBalances()).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('getTransactionLog()', async () => { + expect( + await api.getTransactionLog({ category, currency: 'USDT' }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('getCoinExchangeHistory()', async () => { + expect(await api.getCoinExchangeHistory()).toMatchObject( successResponseObjectV3() ); }); - it('getSettlementHistory()', async () => { - expect(await api.getSettlementHistory({ symbol })).toMatchObject( - successResponseObjectV3() - ); + it('getBorrowHistory()', async () => { + expect(await api.getBorrowHistory()).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); }); - it('getPredictedFundingRate()', async () => { - expect(await api.getPredictedFundingRate(symbol)).toMatchObject( - successResponseObjectV3() - ); + it('getBorrowRate()', async () => { + expect(await api.getBorrowRate()).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); }); }); diff --git a/test/unified-margin/public.read.test.ts b/test/unified-margin/public.read.test.ts index 26a6ab3..a1ee661 100644 --- a/test/unified-margin/public.read.test.ts +++ b/test/unified-margin/public.read.test.ts @@ -1,8 +1,4 @@ -import { - USDCKlineRequest, - UnifiedMarginClient, - UMCandlesRequest, -} from '../../src'; +import { UnifiedMarginClient, UMCandlesRequest } from '../../src'; import { successResponseObject, successResponseObjectV3, From d2b41848ec05945c47197f1bfa3fbc932e381a88 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 19:51:00 +0100 Subject: [PATCH 24/74] add unified margin private write tests --- src/constants/enum.ts | 1 + src/unified-margin-client.ts | 5 +- test/unified-margin/private.read.test.ts | 2 +- test/unified-margin/private.write.test.ts | 157 ++++++++++++++++++---- test/unified-margin/public.read.test.ts | 2 +- 5 files changed, 136 insertions(+), 31 deletions(-) diff --git a/src/constants/enum.ts b/src/constants/enum.ts index 18de262..769ad4c 100644 --- a/src/constants/enum.ts +++ b/src/constants/enum.ts @@ -16,6 +16,7 @@ export const API_ERROR_CODE = { SUCCESS: 0, /** This could mean bad request, incorrect value types or even incorrect/missing values */ PARAMS_MISSING_OR_WRONG: 10001, + INVALID_API_KEY_OR_PERMISSIONS: 10003, INCORRECT_API_KEY_PERMISSIONS: 10005, /** Account not unified margin, update required */ ACCOUNT_NOT_UNIFIED: 10020, diff --git a/src/unified-margin-client.ts b/src/unified-margin-client.ts index c52be5b..6ce1201 100644 --- a/src/unified-margin-client.ts +++ b/src/unified-margin-client.ts @@ -328,7 +328,10 @@ export class UnifiedMarginClient extends BaseRestClient { }); } - /** Upgrade to unified margin account */ + /** + * Upgrade to unified margin account. + * WARNING: This is currently not reversable! + */ upgradeToUnifiedMargin(): Promise> { return this.postPrivate( '/unified/v3/private/account/upgrade-unified-account' diff --git a/test/unified-margin/private.read.test.ts b/test/unified-margin/private.read.test.ts index 3620ed6..1348ffb 100644 --- a/test/unified-margin/private.read.test.ts +++ b/test/unified-margin/private.read.test.ts @@ -1,7 +1,7 @@ import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; import { successResponseObjectV3 } from '../response.util'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private Unified Margin REST API Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/unified-margin/private.write.test.ts b/test/unified-margin/private.write.test.ts index 97637a0..39a8112 100644 --- a/test/unified-margin/private.write.test.ts +++ b/test/unified-margin/private.write.test.ts @@ -1,10 +1,6 @@ -import { API_ERROR_CODE, USDCPerpetualClient } from '../../../src'; -import { - successEmptyResponseObjectV3, - successResponseObjectV3, -} from '../../response.util'; +import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private Unified Margin REST API Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -14,24 +10,30 @@ describe('Private Account Asset REST API Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + const api = new UnifiedMarginClient(API_KEY, API_SECRET, useLivenet); - const symbol = 'BTCPERP'; + const symbol = 'BTCUSDT'; + const category = 'linear'; + + /** + * While it may seem silly, these tests simply validate the request is processed at all. + * Something very wrong would be a sign error or complaints about the endpoint/request method/server error. + */ it('submitOrder()', async () => { expect( await api.submitOrder({ symbol, + category, side: 'Sell', orderType: 'Limit', - orderFilter: 'Order', - orderQty: '1', - orderPrice: '20000', + qty: '1', + price: '20000', orderLinkId: Date.now().toString(), timeInForce: 'GoodTillCancel', }) ).toMatchObject({ - retCode: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST, + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, }); }); @@ -39,12 +41,12 @@ describe('Private Account Asset REST API Endpoints', () => { expect( await api.modifyOrder({ symbol, + category, orderId: 'somethingFake', - orderPrice: '20000', - orderFilter: 'Order', + price: '20000', }) ).toMatchObject({ - retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, }); }); @@ -52,35 +54,134 @@ describe('Private Account Asset REST API Endpoints', () => { expect( await api.cancelOrder({ symbol, + category, orderId: 'somethingFake1', orderFilter: 'Order', }) ).toMatchObject({ - retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, }); }); - it('cancelActiveOrders()', async () => { - expect(await api.cancelActiveOrders(symbol, 'Order')).toMatchObject( - successEmptyResponseObjectV3() - ); + it('batchSubmitOrders()', async () => { + expect( + await api.batchSubmitOrders(category, [ + { + symbol, + side: 'Buy', + orderType: 'Limit', + qty: '1', + price: '10000', + timeInForce: 'FillOrKill', + }, + { + symbol, + side: 'Buy', + orderType: 'Limit', + qty: '1', + price: '10001', + timeInForce: 'FillOrKill', + }, + { + symbol, + side: 'Buy', + orderType: 'Limit', + qty: '1', + price: '10002', + timeInForce: 'FillOrKill', + }, + ]) + ).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); }); - it('setMarginMode()', async () => { - expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( - successResponseObjectV3() - ); + it('batchReplaceOrders()', async () => { + expect( + await api.batchReplaceOrders(category, [ + { + symbol, + orderLinkId: 'somethingFake1', + qty: '4', + }, + { + symbol, + orderLinkId: 'somethingFake2', + qty: '5', + }, + { + symbol, + orderLinkId: 'somethingFake3', + qty: '6', + }, + ]) + ).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('batchCancelOrders()', async () => { + expect( + await api.batchCancelOrders(category, [ + { + symbol, + orderLinkId: 'somethingFake1', + }, + { + symbol, + orderLinkId: 'somethingFake2', + }, + { + symbol, + orderLinkId: 'somethingFake3', + }, + ]) + ).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('cancelAllOrders()', async () => { + expect(await api.cancelAllOrders({ category })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); }); it('setLeverage()', async () => { - expect(await api.setLeverage(symbol, '10')).toMatchObject({ - retCode: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED, + expect(await api.setLeverage(category, symbol, 5, 5)).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('setTPSLMode()', async () => { + expect(await api.setTPSLMode(category, symbol, 1)).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, }); }); it('setRiskLimit()', async () => { - expect(await api.setRiskLimit(symbol, 1)).toMatchObject({ - retCode: API_ERROR_CODE.RISK_LIMIT_NOT_EXISTS, + expect(await api.setRiskLimit(category, symbol, 1, 0)).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('setTPSL()', async () => { + expect(await api.setTPSL({ category, symbol })).toMatchObject({ + retCode: API_ERROR_CODE.ACCOUNT_NOT_UNIFIED, + }); + }); + + it('transferFunds()', async () => { + expect( + await api.transferFunds({ + amount: '1', + coin: 'USDT', + from_account_type: 'SPOT', + to_account_type: 'CONTRACT', + transfer_id: 'testtransfer', + }) + ).toMatchObject({ + ret_code: API_ERROR_CODE.INVALID_API_KEY_OR_PERMISSIONS, }); }); }); diff --git a/test/unified-margin/public.read.test.ts b/test/unified-margin/public.read.test.ts index a1ee661..9f7843d 100644 --- a/test/unified-margin/public.read.test.ts +++ b/test/unified-margin/public.read.test.ts @@ -4,7 +4,7 @@ import { successResponseObjectV3, } from '../response.util'; -describe('Public USDC Options REST API Endpoints', () => { +describe('Public Unified Margin REST API Endpoints', () => { const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; From fe49cf9b12f1e5daa0b41b1a5a98b3dfd26ebb4c Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 19:59:38 +0100 Subject: [PATCH 25/74] fix test names & cleaning --- test/account-asset/private.read.test.ts | 2 +- test/copy-trading/private.read.test.ts | 3 +-- test/inverse-futures/private.read.test.ts | 2 +- test/inverse-futures/public.test.ts | 2 +- test/inverse/private.read.test.ts | 2 +- test/inverse/private.write.test.ts | 2 +- test/linear/private.read.test.ts | 2 +- test/linear/private.write.test.ts | 2 +- test/spot/private.v1.read.test.ts | 9 ++------- test/spot/private.v1.write.test.ts | 2 +- test/spot/private.v3.read.test.ts | 2 +- test/spot/private.v3.write.test.ts | 7 ++----- test/spot/public.v3.test.ts | 2 -- test/unified-margin/private.read.test.ts | 2 +- test/unified-margin/private.write.test.ts | 2 +- test/usdc/options/private.read.test.ts | 2 +- test/usdc/options/private.write.test.ts | 2 +- test/usdc/perpetual/private.read.test.ts | 2 +- test/usdc/perpetual/private.write.test.ts | 2 +- test/usdc/perpetual/public.read.test.ts | 2 +- 20 files changed, 21 insertions(+), 32 deletions(-) diff --git a/test/account-asset/private.read.test.ts b/test/account-asset/private.read.test.ts index c060274..36f1458 100644 --- a/test/account-asset/private.read.test.ts +++ b/test/account-asset/private.read.test.ts @@ -1,7 +1,7 @@ import { AccountAssetClient } from '../../src/'; import { successResponseObject } from '../response.util'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private Account Asset REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/copy-trading/private.read.test.ts b/test/copy-trading/private.read.test.ts index 44825c9..d0637ea 100644 --- a/test/copy-trading/private.read.test.ts +++ b/test/copy-trading/private.read.test.ts @@ -1,7 +1,6 @@ import { API_ERROR_CODE, CopyTradingClient } from '../../src'; -import { successResponseObject } from '../response.util'; -describe('Private Copy Trading REST API Endpoints', () => { +describe('Private Copy Trading REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/inverse-futures/private.read.test.ts b/test/inverse-futures/private.read.test.ts index 6e24cf5..821d82b 100644 --- a/test/inverse-futures/private.read.test.ts +++ b/test/inverse-futures/private.read.test.ts @@ -1,7 +1,7 @@ import { InverseFuturesClient } from '../../src/inverse-futures-client'; import { successResponseList, successResponseObject } from '../response.util'; -describe('Public Inverse-Futures REST API GET Endpoints', () => { +describe('Private Inverse-Futures REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/inverse-futures/public.test.ts b/test/inverse-futures/public.test.ts index 24ba232..fd34263 100644 --- a/test/inverse-futures/public.test.ts +++ b/test/inverse-futures/public.test.ts @@ -5,7 +5,7 @@ import { successResponseObject, } from '../response.util'; -describe('Public Inverse Futures REST API Endpoints', () => { +describe('Public Inverse-Futures REST API Endpoints', () => { const useLivenet = true; const api = new InverseFuturesClient(undefined, undefined, useLivenet); diff --git a/test/inverse/private.read.test.ts b/test/inverse/private.read.test.ts index 4a3f076..d0b9ee9 100644 --- a/test/inverse/private.read.test.ts +++ b/test/inverse/private.read.test.ts @@ -1,7 +1,7 @@ import { InverseClient } from '../../src/'; import { successResponseList, successResponseObject } from '../response.util'; -describe('Private Inverse REST API Endpoints', () => { +describe('Private Inverse REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/inverse/private.write.test.ts b/test/inverse/private.write.test.ts index 52a2b19..f667045 100644 --- a/test/inverse/private.write.test.ts +++ b/test/inverse/private.write.test.ts @@ -2,7 +2,7 @@ import { API_ERROR_CODE } from '../../src'; import { InverseClient } from '../../src/inverse-client'; import { successResponseObject } from '../response.util'; -describe('Private Inverse REST API Endpoints', () => { +describe('Private Inverse REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/linear/private.read.test.ts b/test/linear/private.read.test.ts index 5b5179b..9eef850 100644 --- a/test/linear/private.read.test.ts +++ b/test/linear/private.read.test.ts @@ -1,7 +1,7 @@ import { LinearClient } from '../../src/linear-client'; import { successResponseList, successResponseObject } from '../response.util'; -describe('Public Linear REST API GET Endpoints', () => { +describe('Private Linear REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/linear/private.write.test.ts b/test/linear/private.write.test.ts index 9747029..dd920f1 100644 --- a/test/linear/private.write.test.ts +++ b/test/linear/private.write.test.ts @@ -1,7 +1,7 @@ import { API_ERROR_CODE, LinearClient } from '../../src'; import { successResponseObject } from '../response.util'; -describe('Private Inverse-Futures REST API POST Endpoints', () => { +describe('Private Linear REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/spot/private.v1.read.test.ts b/test/spot/private.v1.read.test.ts index bbc6182..518e853 100644 --- a/test/spot/private.v1.read.test.ts +++ b/test/spot/private.v1.read.test.ts @@ -1,12 +1,7 @@ import { SpotClient } from '../../src'; -import { - errorResponseObject, - notAuthenticatedError, - successResponseList, - successResponseObject, -} from '../response.util'; +import { errorResponseObject, successResponseList } from '../response.util'; -describe('Private Spot REST API Endpoints', () => { +describe('Private Spot REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/spot/private.v1.write.test.ts b/test/spot/private.v1.write.test.ts index 4f3ef5e..4b678de 100644 --- a/test/spot/private.v1.write.test.ts +++ b/test/spot/private.v1.write.test.ts @@ -1,7 +1,7 @@ import { API_ERROR_CODE, SpotClient } from '../../src'; import { successResponseObject } from '../response.util'; -describe('Private Inverse-Futures REST API POST Endpoints', () => { +describe('Private Spot REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/spot/private.v3.read.test.ts b/test/spot/private.v3.read.test.ts index 003b2dc..b1d0680 100644 --- a/test/spot/private.v3.read.test.ts +++ b/test/spot/private.v3.read.test.ts @@ -7,7 +7,7 @@ import { successResponseObjectV3, } from '../response.util'; -describe('Private Spot REST API Endpoints', () => { +describe('Private Spot REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/spot/private.v3.write.test.ts b/test/spot/private.v3.write.test.ts index b226292..95ad6e7 100644 --- a/test/spot/private.v3.write.test.ts +++ b/test/spot/private.v3.write.test.ts @@ -1,10 +1,7 @@ import { API_ERROR_CODE, SpotClientV3 } from '../../src'; -import { - successResponseObject, - successResponseObjectV3, -} from '../response.util'; +import { successResponseObjectV3 } from '../response.util'; -describe('Private Inverse-Futures REST API POST Endpoints', () => { +describe('Private Spot REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/spot/public.v3.test.ts b/test/spot/public.v3.test.ts index 13dbad4..e18f260 100644 --- a/test/spot/public.v3.test.ts +++ b/test/spot/public.v3.test.ts @@ -1,8 +1,6 @@ import { SpotClientV3 } from '../../src'; import { notAuthenticatedError, - successResponseList, - successResponseObject, successResponseObjectV3, } from '../response.util'; diff --git a/test/unified-margin/private.read.test.ts b/test/unified-margin/private.read.test.ts index 1348ffb..c5093b9 100644 --- a/test/unified-margin/private.read.test.ts +++ b/test/unified-margin/private.read.test.ts @@ -1,7 +1,7 @@ import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; import { successResponseObjectV3 } from '../response.util'; -describe('Private Unified Margin REST API Endpoints', () => { +describe('Private Unified Margin REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/unified-margin/private.write.test.ts b/test/unified-margin/private.write.test.ts index 39a8112..3e8d297 100644 --- a/test/unified-margin/private.write.test.ts +++ b/test/unified-margin/private.write.test.ts @@ -1,6 +1,6 @@ import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; -describe('Private Unified Margin REST API Endpoints', () => { +describe('Private Unified Margin REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/usdc/options/private.read.test.ts b/test/usdc/options/private.read.test.ts index 04f446e..cd31815 100644 --- a/test/usdc/options/private.read.test.ts +++ b/test/usdc/options/private.read.test.ts @@ -1,7 +1,7 @@ import { USDCOptionClient } from '../../../src'; import { successResponseObjectV3 } from '../../response.util'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private USDC Options REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts index 7a6f9c8..5bd48a7 100644 --- a/test/usdc/options/private.write.test.ts +++ b/test/usdc/options/private.write.test.ts @@ -1,7 +1,7 @@ import { API_ERROR_CODE, USDCOptionClient } from '../../../src'; import { successResponseObjectV3 } from '../../response.util'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private USDC Options REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/usdc/perpetual/private.read.test.ts b/test/usdc/perpetual/private.read.test.ts index ae86d70..876aeac 100644 --- a/test/usdc/perpetual/private.read.test.ts +++ b/test/usdc/perpetual/private.read.test.ts @@ -1,7 +1,7 @@ import { USDCPerpetualClient } from '../../../src'; import { successResponseObjectV3 } from '../../response.util'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private USDC Perp REST API GET Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/usdc/perpetual/private.write.test.ts b/test/usdc/perpetual/private.write.test.ts index 97637a0..9401a37 100644 --- a/test/usdc/perpetual/private.write.test.ts +++ b/test/usdc/perpetual/private.write.test.ts @@ -4,7 +4,7 @@ import { successResponseObjectV3, } from '../../response.util'; -describe('Private Account Asset REST API Endpoints', () => { +describe('Private USDC Perp REST API POST Endpoints', () => { const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/usdc/perpetual/public.read.test.ts b/test/usdc/perpetual/public.read.test.ts index 68a902a..161c580 100644 --- a/test/usdc/perpetual/public.read.test.ts +++ b/test/usdc/perpetual/public.read.test.ts @@ -4,7 +4,7 @@ import { successResponseObjectV3, } from '../../response.util'; -describe('Public USDC Options REST API Endpoints', () => { +describe('Public USDC Perp REST API Endpoints', () => { const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; From 04fd7989dc1bc7473c57aa3ec30360adba4ddf81 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 10 Sep 2022 20:00:25 +0100 Subject: [PATCH 26/74] cleaning --- test/spot/private.v3.read.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/spot/private.v3.read.test.ts b/test/spot/private.v3.read.test.ts index b1d0680..949bf0f 100644 --- a/test/spot/private.v3.read.test.ts +++ b/test/spot/private.v3.read.test.ts @@ -1,7 +1,5 @@ import { API_ERROR_CODE, SpotClientV3 } from '../../src'; import { - errorResponseObject, - errorResponseObjectV3, successEmptyResponseObjectV3, successResponseListV3, successResponseObjectV3, From d1ed7971adb1bbb549027c2fef0a584d01b01fd2 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Wed, 14 Sep 2022 23:12:44 +0100 Subject: [PATCH 27/74] v2.4.0-beta.1. remove circleci, cleaning in ws client --- .circleci/config.yml | 31 ---- package.json | 2 +- src/types/index.ts | 1 + src/types/shared.ts | 11 ++ src/types/websockets.ts | 107 ++++++++++++ src/util/WsStore.ts | 36 +++- src/util/index.ts | 1 + src/util/websocket-util.ts | 40 +++++ src/websocket-client.ts | 331 ++++++++++++------------------------- 9 files changed, 296 insertions(+), 264 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 src/types/websockets.ts create mode 100644 src/util/websocket-util.ts diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index b0a8e2f..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: 2.1 - -jobs: - test: - docker: - - image: cimg/node:15.1 - steps: - - checkout - - restore_cache: - # See the configuration reference documentation for more details on using restore_cache and save_cache steps - # https://circleci.com/docs/2.0/configuration-reference/?section=reference#save_cache - keys: - - node-deps-v1-{{ .Branch }}-{{checksum "package-lock.json"}} - - run: - name: install packages - command: npm ci --ignore-scripts - - save_cache: - key: node-deps-v1-{{ .Branch }}-{{checksum "package-lock.json"}} - paths: - - ~/.npm - - run: - name: Run Build - command: npm run build - - run: - name: Run Tests - command: npm run test - -workflows: - integrationtests: - jobs: - - test diff --git a/package.json b/package.json index d150478..0d051f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "2.3.2", + "version": "2.4.0-beta.1", "description": "Node.js connector 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/index.ts b/src/types/index.ts index 145ac98..82b2b91 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ export * from './response'; export * from './request'; export * from './shared'; +export * from './websockets'; diff --git a/src/types/shared.ts b/src/types/shared.ts index 88f465a..2f8dd65 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -1,3 +1,14 @@ +import { InverseClient } from '../inverse-client'; +import { LinearClient } from '../linear-client'; +import { SpotClient } from '../spot-client'; +import { SpotClientV3 } from '../spot-client-v3'; + +export type RESTClient = + | InverseClient + | LinearClient + | SpotClient + | SpotClientV3; + export type numberInString = string; export type OrderSide = 'Buy' | 'Sell'; diff --git a/src/types/websockets.ts b/src/types/websockets.ts new file mode 100644 index 0000000..963375f --- /dev/null +++ b/src/types/websockets.ts @@ -0,0 +1,107 @@ +import { RestClientOptions } from '../util'; + +export type APIMarket = 'inverse' | 'linear' | 'spot' | 'v3'; + +// Same as inverse futures +export type WsPublicInverseTopic = + | 'orderBookL2_25' + | 'orderBookL2_200' + | 'trade' + | 'insurance' + | 'instrument_info' + | 'klineV2'; + +export type WsPublicUSDTPerpTopic = + | 'orderBookL2_25' + | 'orderBookL2_200' + | 'trade' + | 'insurance' + | 'instrument_info' + | 'kline'; + +export type WsPublicSpotV1Topic = + | 'trade' + | 'realtimes' + | 'kline' + | 'depth' + | 'mergedDepth' + | 'diffDepth'; + +export type WsPublicSpotV2Topic = + | 'depth' + | 'kline' + | 'trade' + | 'bookTicker' + | 'realtimes'; + +export type WsPublicTopics = + | WsPublicInverseTopic + | WsPublicUSDTPerpTopic + | WsPublicSpotV1Topic + | WsPublicSpotV2Topic + | string; + +// Same as inverse futures +export type WsPrivateInverseTopic = + | 'position' + | 'execution' + | 'order' + | 'stop_order'; + +export type WsPrivateUSDTPerpTopic = + | 'position' + | 'execution' + | 'order' + | 'stop_order' + | 'wallet'; + +export type WsPrivateSpotTopic = + | 'outboundAccountInfo' + | 'executionReport' + | 'ticketInfo'; + +export type WsPrivateTopic = + | WsPrivateInverseTopic + | WsPrivateUSDTPerpTopic + | WsPrivateSpotTopic + | string; + +export type WsTopic = WsPublicTopics | WsPrivateTopic; + +/** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */ +export type WsKey = + | 'inverse' + | 'linearPrivate' + | 'linearPublic' + | 'spotPrivate' + | 'spotPublic'; + +export interface WSClientConfigurableOptions { + key?: string; + secret?: string; + livenet?: boolean; + + /** + * The API group this client should connect to. + * + * For the V3 APIs use `v3` as the market (spot/unified margin/usdc/account asset/copy trading) + */ + market: APIMarket; + + pongTimeout?: number; + pingInterval?: number; + reconnectTimeout?: number; + restOptions?: RestClientOptions; + requestOptions?: any; + wsUrl?: string; + /** If true, fetch server time before trying to authenticate (disabled by default) */ + fetchTimeOffsetBeforeAuth?: boolean; +} + +export interface WebsocketClientOptions extends WSClientConfigurableOptions { + livenet: boolean; + market: APIMarket; + pongTimeout: number; + pingInterval: number; + reconnectTimeout: number; +} diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index dd7360e..f27cb56 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -1,16 +1,35 @@ import WebSocket from 'isomorphic-ws'; -import { WsConnectionState } from '../websocket-client'; import { DefaultLogger } from './logger'; +export enum WsConnectionStateEnum { + INITIAL = 0, + CONNECTING = 1, + CONNECTED = 2, + CLOSING = 3, + RECONNECTING = 4, +} +/** A "topic" is always a string */ type WsTopic = string; + +/** + * A "Set" is used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to) + * TODO: do any WS topics allow parameters? If so, we need a way to track those (see FTX implementation) + */ type WsTopicList = Set; interface WsStoredState { + /** The currently active websocket connection */ ws?: WebSocket; - connectionState?: WsConnectionState; + /** The current lifecycle state of the connection (enum) */ + connectionState?: WsConnectionStateEnum; + /** A timer that will send an upstream heartbeat (ping) when it expires */ activePingTimer?: ReturnType | undefined; + /** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ activePongTimer?: ReturnType | undefined; + /** + * All the topics we are expected to be subscribed to (and we automatically resubscribe to if the connection drops) + */ subscribedTopics: WsTopicList; } @@ -23,6 +42,9 @@ export default class WsStore { this.wsState = {}; } + /** Get WS stored state for key, optionally create if missing */ + get(key: string, createIfMissing?: true): WsStoredState; + get(key: string, createIfMissing?: false): WsStoredState | undefined; get(key: string, createIfMissing?: boolean): WsStoredState | undefined { if (this.wsState[key]) { return this.wsState[key]; @@ -46,7 +68,7 @@ export default class WsStore { } this.wsState[key] = { subscribedTopics: new Set(), - connectionState: WsConnectionState.READY_STATE_INITIAL, + connectionState: WsConnectionStateEnum.INITIAL, }; return this.get(key); } @@ -94,22 +116,22 @@ export default class WsStore { ); } - getConnectionState(key: string): WsConnectionState { + getConnectionState(key: string): WsConnectionStateEnum { return this.get(key, true)!.connectionState!; } - setConnectionState(key: string, state: WsConnectionState) { + setConnectionState(key: string, state: WsConnectionStateEnum) { this.get(key, true)!.connectionState = state; } - isConnectionState(key: string, state: WsConnectionState): boolean { + isConnectionState(key: string, state: WsConnectionStateEnum): boolean { return this.getConnectionState(key) === state; } /* subscribed topics */ getTopics(key: string): WsTopicList { - return this.get(key, true)!.subscribedTopics; + return this.get(key, true).subscribedTopics; } getTopicsByKey(): Record { diff --git a/src/util/index.ts b/src/util/index.ts index e25bf42..000f888 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -2,3 +2,4 @@ export * from './BaseRestClient'; export * from './requestUtils'; export * from './WsStore'; export * from './logger'; +export * from './websocket-util'; diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts new file mode 100644 index 0000000..1724dff --- /dev/null +++ b/src/util/websocket-util.ts @@ -0,0 +1,40 @@ +import { WsKey } from '../types'; + +export const wsKeyInverse = 'inverse'; +export const wsKeyLinearPrivate = 'linearPrivate'; +export const wsKeyLinearPublic = 'linearPublic'; +export const wsKeySpotPrivate = 'spotPrivate'; +export const wsKeySpotPublic = 'spotPublic'; + +export function getLinearWsKeyForTopic(topic: string): WsKey { + const privateLinearTopics = [ + 'position', + 'execution', + 'order', + 'stop_order', + 'wallet', + ]; + if (privateLinearTopics.includes(topic)) { + return wsKeyLinearPrivate; + } + + return wsKeyLinearPublic; +} + +export function getSpotWsKeyForTopic(topic: string): WsKey { + const privateLinearTopics = [ + 'position', + 'execution', + 'order', + 'stop_order', + 'outboundAccountInfo', + 'executionReport', + 'ticketInfo', + ]; + + if (privateLinearTopics.includes(topic)) { + return wsKeySpotPrivate; + } + + return wsKeySpotPublic; +} diff --git a/src/websocket-client.ts b/src/websocket-client.ts index c8dd784..6fc0bdd 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -3,17 +3,35 @@ import WebSocket from 'isomorphic-ws'; import { InverseClient } from './inverse-client'; import { LinearClient } from './linear-client'; -import { DefaultLogger } from './util/logger'; +import { SpotClientV3 } from './spot-client-v3'; import { SpotClient } from './spot-client'; -import { KlineInterval } from './types/shared'; + +import { DefaultLogger } from './util/logger'; +import { + APIMarket, + KlineInterval, + RESTClient, + WebsocketClientOptions, + WSClientConfigurableOptions, + WsKey, + WsTopic, +} from './types'; + import { signMessage } from './util/node-support'; + +import WsStore from './util/WsStore'; import { serializeParams, isWsPong, - RestClientOptions, -} from './util/requestUtils'; - -import WsStore from './util/WsStore'; + getLinearWsKeyForTopic, + getSpotWsKeyForTopic, + wsKeyInverse, + wsKeyLinearPrivate, + wsKeyLinearPublic, + wsKeySpotPrivate, + wsKeySpotPublic, + WsConnectionStateEnum, +} from './util'; const inverseEndpoints = { livenet: 'wss://stream.bybit.com/realtime', @@ -48,169 +66,6 @@ const spotEndpoints = { const loggerCategory = { category: 'bybit-ws' }; -const READY_STATE_INITIAL = 0; -const READY_STATE_CONNECTING = 1; -const READY_STATE_CONNECTED = 2; -const READY_STATE_CLOSING = 3; -const READY_STATE_RECONNECTING = 4; - -export enum WsConnectionState { - READY_STATE_INITIAL, - READY_STATE_CONNECTING, - READY_STATE_CONNECTED, - READY_STATE_CLOSING, - READY_STATE_RECONNECTING, -} - -export type APIMarket = 'inverse' | 'linear' | 'spot'; - -// Same as inverse futures -export type WsPublicInverseTopic = - | 'orderBookL2_25' - | 'orderBookL2_200' - | 'trade' - | 'insurance' - | 'instrument_info' - | 'klineV2'; - -export type WsPublicUSDTPerpTopic = - | 'orderBookL2_25' - | 'orderBookL2_200' - | 'trade' - | 'insurance' - | 'instrument_info' - | 'kline'; - -export type WsPublicSpotV1Topic = - | 'trade' - | 'realtimes' - | 'kline' - | 'depth' - | 'mergedDepth' - | 'diffDepth'; - -export type WsPublicSpotV2Topic = - | 'depth' - | 'kline' - | 'trade' - | 'bookTicker' - | 'realtimes'; - -export type WsPublicTopics = - | WsPublicInverseTopic - | WsPublicUSDTPerpTopic - | WsPublicSpotV1Topic - | WsPublicSpotV2Topic - | string; - -// Same as inverse futures -export type WsPrivateInverseTopic = - | 'position' - | 'execution' - | 'order' - | 'stop_order'; - -export type WsPrivateUSDTPerpTopic = - | 'position' - | 'execution' - | 'order' - | 'stop_order' - | 'wallet'; - -export type WsPrivateSpotTopic = - | 'outboundAccountInfo' - | 'executionReport' - | 'ticketInfo'; - -export type WsPrivateTopic = - | WsPrivateInverseTopic - | WsPrivateUSDTPerpTopic - | WsPrivateSpotTopic - | string; - -export type WsTopic = WsPublicTopics | WsPrivateTopic; - -export interface WSClientConfigurableOptions { - key?: string; - secret?: string; - livenet?: boolean; - - // defaults to inverse. - /** - * @deprecated Use the property { market: 'linear' } instead - */ - linear?: boolean; - - market?: APIMarket; - - pongTimeout?: number; - pingInterval?: number; - reconnectTimeout?: number; - restOptions?: RestClientOptions; - requestOptions?: any; - wsUrl?: string; - /** If true, fetch server time before trying to authenticate (disabled by default) */ - fetchTimeOffsetBeforeAuth?: boolean; -} - -export interface WebsocketClientOptions extends WSClientConfigurableOptions { - livenet: boolean; - /** - * @deprecated Use the property { market: 'linear' } instead - */ - linear?: boolean; - market?: APIMarket; - pongTimeout: number; - pingInterval: number; - reconnectTimeout: number; -} - -export const wsKeyInverse = 'inverse'; -export const wsKeyLinearPrivate = 'linearPrivate'; -export const wsKeyLinearPublic = 'linearPublic'; -export const wsKeySpotPrivate = 'spotPrivate'; -export const wsKeySpotPublic = 'spotPublic'; - -// This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) -export type WsKey = - | 'inverse' - | 'linearPrivate' - | 'linearPublic' - | 'spotPrivate' - | 'spotPublic'; - -const getLinearWsKeyForTopic = (topic: string): WsKey => { - const privateLinearTopics = [ - 'position', - 'execution', - 'order', - 'stop_order', - 'wallet', - ]; - if (privateLinearTopics.includes(topic)) { - return wsKeyLinearPrivate; - } - - return wsKeyLinearPublic; -}; -const getSpotWsKeyForTopic = (topic: string): WsKey => { - const privateLinearTopics = [ - 'position', - 'execution', - 'order', - 'stop_order', - 'outboundAccountInfo', - 'executionReport', - 'ticketInfo', - ]; - - if (privateLinearTopics.includes(topic)) { - return wsKeySpotPrivate; - } - - return wsKeySpotPublic; -}; - export declare interface WebsocketClient { on( event: 'open' | 'reconnected', @@ -223,16 +78,10 @@ export declare interface WebsocketClient { on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this; } -function resolveMarket(options: WSClientConfigurableOptions): APIMarket { - if (options.linear) { - return 'linear'; - } - return 'inverse'; -} - export class WebsocketClient extends EventEmitter { private logger: typeof DefaultLogger; - private restClient: InverseClient | LinearClient | SpotClient; + /** Purely used */ + private restClient: RESTClient; private options: WebsocketClientOptions; private wsStore: WsStore; @@ -254,11 +103,15 @@ export class WebsocketClient extends EventEmitter { ...options, }; - if (!this.options.market) { - this.options.market = resolveMarket(this.options); - } - - if (this.isLinear()) { + if (this.isV3()) { + this.restClient = new SpotClientV3( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + } else if (this.isLinear()) { this.restClient = new LinearClient( undefined, undefined, @@ -299,7 +152,12 @@ export class WebsocketClient extends EventEmitter { } public isInverse(): boolean { - return !this.isLinear() && !this.isSpot(); + return this.options.market === 'inverse'; + } + + /** USDC, spot v3, unified margin, account asset */ + public isV3(): boolean { + return this.options.market === 'v3'; } /** @@ -314,14 +172,22 @@ export class WebsocketClient extends EventEmitter { // attempt to send subscription topic per websocket this.wsStore.getKeys().forEach((wsKey: WsKey) => { // if connected, send subscription request - if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { return this.requestSubscribeTopics(wsKey, topics); } // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect if ( - !this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING) && - !this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING) + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.CONNECTING + ) && + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.RECONNECTING + ) ) { return this.connect(wsKey); } @@ -339,7 +205,9 @@ export class WebsocketClient extends EventEmitter { this.wsStore.getKeys().forEach((wsKey: WsKey) => { // unsubscribe request only necessary if active connection exists - if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { this.requestUnsubscribeTopics(wsKey, topics); } }); @@ -347,14 +215,14 @@ export class WebsocketClient extends EventEmitter { public close(wsKey: WsKey) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); - this.setWsState(wsKey, READY_STATE_CLOSING); + this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); this.clearTimers(wsKey); this.getWs(wsKey)?.close(); } /** - * Request connection of all dependent websockets, instead of waiting for automatic connection by library + * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ public connectAll(): Promise[] | undefined { if (this.isInverse()) { @@ -411,7 +279,9 @@ export class WebsocketClient extends EventEmitter { return this.wsStore.getWs(wsKey); } - if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING) + ) { this.logger.error( 'Refused to connect to ws, connection attempt already active', { ...loggerCategory, wsKey } @@ -421,9 +291,9 @@ export class WebsocketClient extends EventEmitter { if ( !this.wsStore.getConnectionState(wsKey) || - this.wsStore.isConnectionState(wsKey, READY_STATE_INITIAL) + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.INITIAL) ) { - this.setWsState(wsKey, READY_STATE_CONNECTING); + this.setWsState(wsKey, WsConnectionStateEnum.CONNECTING); } const authParams = await this.getAuthParams(wsKey); @@ -481,19 +351,23 @@ export class WebsocketClient extends EventEmitter { ? await this.restClient.fetchTimeOffset() : 0; - const params: any = { - api_key: this.options.key, - expires: Date.now() + timeOffset + 5000, - }; + const signatureExpires = Date.now() + timeOffset + 5000; - params.signature = await signMessage( - 'GET/realtime' + params.expires, + const signature = await signMessage( + 'GET/realtime' + signatureExpires, secret ); - return '?' + serializeParams(params); + + const authParams = { + api_key: this.options.key, + expires: signatureExpires, + signature, + }; + + return '?' + serializeParams(authParams); } else if (!key || !secret) { this.logger.warning( - 'Connot authenticate websocket, either api or private keys missing.', + 'Cannot authenticate websocket, either api or private keys missing.', { ...loggerCategory, wsKey } ); } else { @@ -508,8 +382,11 @@ export class WebsocketClient extends EventEmitter { private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) { this.clearTimers(wsKey); - if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CONNECTING) { - this.setWsState(wsKey, READY_STATE_RECONNECTING); + if ( + this.wsStore.getConnectionState(wsKey) !== + WsConnectionStateEnum.CONNECTING + ) { + this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); } setTimeout(() => { @@ -635,7 +512,9 @@ export class WebsocketClient extends EventEmitter { } private onWsOpen(event, wsKey: WsKey) { - if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING) + ) { this.logger.info('Websocket connected', { ...loggerCategory, wsKey, @@ -645,13 +524,13 @@ export class WebsocketClient extends EventEmitter { }); this.emit('open', { wsKey, event }); } else if ( - this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING) + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING) ) { this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey }); this.emit('reconnected', { wsKey, event }); } - this.setWsState(wsKey, READY_STATE_CONNECTED); + this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED); // TODO: persistence not working yet for spot topics if (wsKey !== 'spotPublic' && wsKey !== 'spotPrivate') { @@ -670,18 +549,20 @@ export class WebsocketClient extends EventEmitter { this.clearPongTimer(wsKey); const msg = JSON.parse((event && event.data) || event); - if ('success' in msg || msg?.pong) { - this.onWsMessageResponse(msg, wsKey); - } else if (msg.topic) { - this.onWsMessageUpdate(msg); - } else { - this.logger.warning('Got unhandled ws message', { - ...loggerCategory, - message: msg, - event, - wsKey, - }); + if (msg['success'] || msg?.pong) { + return this.onWsMessageResponse(msg, wsKey); } + + if (msg.topic) { + return this.emit('update', msg); + } + + this.logger.warning('Got unhandled ws message', { + ...loggerCategory, + message: msg, + event, + wsKey, + }); } catch (e) { this.logger.error('Failed to parse ws event message', { ...loggerCategory, @@ -694,7 +575,9 @@ export class WebsocketClient extends EventEmitter { private onWsError(error: any, wsKey: WsKey) { this.parseWsError('Websocket error', error, wsKey); - if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) { + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { this.emit('error', error); } } @@ -705,11 +588,13 @@ export class WebsocketClient extends EventEmitter { wsKey, }); - if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) { + if ( + this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING + ) { this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); this.emit('reconnect', { wsKey }); } else { - this.setWsState(wsKey, READY_STATE_INITIAL); + this.setWsState(wsKey, WsConnectionStateEnum.INITIAL); this.emit('close', { wsKey }); } } @@ -722,15 +607,11 @@ export class WebsocketClient extends EventEmitter { } } - private onWsMessageUpdate(message: any) { - this.emit('update', message); - } - private getWs(wsKey: string) { return this.wsStore.getWs(wsKey); } - private setWsState(wsKey: WsKey, state: WsConnectionState) { + private setWsState(wsKey: WsKey, state: WsConnectionStateEnum) { this.wsStore.setConnectionState(wsKey, state); } @@ -740,7 +621,7 @@ export class WebsocketClient extends EventEmitter { } const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; - // TODO: reptitive + // TODO: repetitive if (this.isLinear() || wsKey.startsWith('linear')) { if (wsKey === wsKeyLinearPublic) { return linearEndpoints.public[networkKey]; From 0e05a8d0ef0da80dbc36b80643636383d3a86af8 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Wed, 14 Sep 2022 23:55:24 +0100 Subject: [PATCH 28/74] tidying ws client --- src/types/websockets.ts | 2 +- src/util/WsStore.ts | 1 + src/util/node-support.ts | 1 + src/util/websocket-util.ts | 10 ++ src/websocket-client.ts | 326 ++++++++++++++++++++++--------------- 5 files changed, 205 insertions(+), 135 deletions(-) diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 963375f..a2a1cdf 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,6 +1,6 @@ import { RestClientOptions } from '../util'; -export type APIMarket = 'inverse' | 'linear' | 'spot' | 'v3'; +export type APIMarket = 'inverse' | 'linear' | 'spot'; //| 'v3'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index f27cb56..7c421ce 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -8,6 +8,7 @@ export enum WsConnectionStateEnum { CONNECTED = 2, CLOSING = 3, RECONNECTING = 4, + // ERROR = 5, } /** A "topic" is always a string */ type WsTopic = string; diff --git a/src/util/node-support.ts b/src/util/node-support.ts index bc3b159..bbbf8bd 100644 --- a/src/util/node-support.ts +++ b/src/util/node-support.ts @@ -1,5 +1,6 @@ import { createHmac } from 'crypto'; +/** This is async because the browser version uses a promise (browser-support) */ export async function signMessage( message: string, secret: string diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 1724dff..0b639af 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -6,6 +6,16 @@ export const wsKeyLinearPublic = 'linearPublic'; export const wsKeySpotPrivate = 'spotPrivate'; export const wsKeySpotPublic = 'spotPublic'; +export const WS_KEY_MAP = { + inverse: wsKeyInverse, + linearPrivate: wsKeyLinearPrivate, + linearPublic: wsKeyLinearPublic, + spotPrivate: wsKeySpotPrivate, + spotPublic: wsKeySpotPublic, +}; + +export const PUBLIC_WS_KEYS = [WS_KEY_MAP.linearPublic, WS_KEY_MAP.spotPublic]; + export function getLinearWsKeyForTopic(topic: string): WsKey { const privateLinearTopics = [ 'position', diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 6fc0bdd..759a4a3 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -31,6 +31,7 @@ import { wsKeySpotPrivate, wsKeySpotPublic, WsConnectionStateEnum, + PUBLIC_WS_KEYS, } from './util'; const inverseEndpoints = { @@ -38,7 +39,35 @@ const inverseEndpoints = { testnet: 'wss://stream-testnet.bybit.com/realtime', }; -const linearEndpoints = { +interface NetworkMapV3 { + livenet: string; + livenet2?: string; + testnet: string; + testnet2?: string; +} + +type NetworkType = 'public' | 'private'; + +function neverGuard(x: never, msg: string): Error { + return new Error(`Unhandled value exception "x", ${msg}`); +} + +const WS_BASE_URL_MAP: Record> = { + linear: { + private: { + livenet: 'wss://stream.bybit.com/realtime_private', + livenet2: 'wss://stream.bytick.com/realtime_private', + testnet: 'wss://stream-testnet.bybit.com/realtime_private', + }, + public: { + livenet: 'wss://stream.bybit.com/realtime_public', + livenet2: 'wss://stream.bytick.com/realtime_public', + testnet: 'wss://stream-testnet.bybit.com/realtime_public', + }, + }, +}; + +const linearEndpoints: Record = { private: { livenet: 'wss://stream.bybit.com/realtime_private', livenet2: 'wss://stream.bytick.com/realtime_private', @@ -51,7 +80,7 @@ const linearEndpoints = { }, }; -const spotEndpoints = { +const spotEndpoints: Record = { private: { livenet: 'wss://stream.bybit.com/spot/ws', testnet: 'wss://stream-testnet.bybit.com/spot/ws', @@ -80,8 +109,7 @@ export declare interface WebsocketClient { export class WebsocketClient extends EventEmitter { private logger: typeof DefaultLogger; - /** Purely used */ - private restClient: RESTClient; + private restClient?: RESTClient; private options: WebsocketClientOptions; private wsStore: WsStore; @@ -103,39 +131,64 @@ export class WebsocketClient extends EventEmitter { ...options, }; - if (this.isV3()) { - this.restClient = new SpotClientV3( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); - } else if (this.isLinear()) { - this.restClient = new LinearClient( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); - } else if (this.isSpot()) { - this.restClient = new SpotClient( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); - this.connectPublic(); - } else { - this.restClient = new InverseClient( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); + if (this.options.fetchTimeOffsetBeforeAuth) { + this.prepareRESTClient(); + } + } + + /** + * Only used if we fetch exchange time before attempting auth. + * Disabled by default. + * I've removed this for ftx and it's working great, tempted to remove this here + */ + prepareRESTClient(): void { + switch (this.options.market) { + case 'inverse': { + this.restClient = new InverseClient( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + break; + } + case 'linear': { + this.restClient = new LinearClient( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + break; + } + case 'spot': { + this.restClient = new SpotClient( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } + // if (this.isV3()) { + // this.restClient = new SpotClientV3( + // undefined, + // undefined, + // this.isLivenet(), + // this.options.restOptions, + // this.options.requestOptions + // ); + // } + default: { + throw neverGuard( + this.options.market, + `prepareRESTClient(): Unhandled market` + ); + } } } @@ -156,9 +209,9 @@ export class WebsocketClient extends EventEmitter { } /** USDC, spot v3, unified margin, account asset */ - public isV3(): boolean { - return this.options.market === 'v3'; - } + // public isV3(): boolean { + // return this.options.market === 'v3'; + // } /** * Add topic/topics to WS subscription list @@ -224,48 +277,63 @@ export class WebsocketClient extends EventEmitter { /** * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ - public connectAll(): Promise[] | undefined { - if (this.isInverse()) { - return [this.connect(wsKeyInverse)]; - } - - if (this.isLinear()) { - return [ - this.connect(wsKeyLinearPublic), - this.connect(wsKeyLinearPrivate), - ]; - } - - if (this.isSpot()) { - return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; + public connectAll(): Promise[] { + switch (this.options.market) { + case 'inverse': { + return [this.connect(wsKeyInverse)]; + } + case 'linear': { + return [ + this.connect(wsKeyLinearPublic), + this.connect(wsKeyLinearPrivate), + ]; + } + case 'spot': { + return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; + } + default: { + throw neverGuard(this.options.market, `connectAll(): Unhandled market`); + } } } - public connectPublic(): Promise | undefined { - if (this.isInverse()) { - return this.connect(wsKeyInverse); - } - - if (this.isLinear()) { - return this.connect(wsKeyLinearPublic); - } - - if (this.isSpot()) { - return this.connect(wsKeySpotPublic); + public connectPublic(): Promise { + switch (this.options.market) { + case 'inverse': { + return this.connect(wsKeyInverse); + } + case 'linear': { + return this.connect(wsKeyLinearPublic); + } + case 'spot': { + return this.connect(wsKeySpotPublic); + } + default: { + throw neverGuard( + this.options.market, + `connectPublic(): Unhandled market` + ); + } } } public connectPrivate(): Promise | undefined { - if (this.isInverse()) { - return this.connect(wsKeyInverse); - } - - if (this.isLinear()) { - return this.connect(wsKeyLinearPrivate); - } - - if (this.isSpot()) { - return this.connect(wsKeySpotPrivate); + switch (this.options.market) { + case 'inverse': { + return this.connect(wsKeyInverse); + } + case 'linear': { + return this.connect(wsKeyLinearPrivate); + } + case 'spot': { + return this.connect(wsKeySpotPrivate); + } + default: { + throw neverGuard( + this.options.market, + `connectPrivate(): Unhandled market` + ); + } } } @@ -336,48 +404,45 @@ export class WebsocketClient extends EventEmitter { private async getAuthParams(wsKey: WsKey): Promise { const { key, secret } = this.options; - if ( - key && - secret && - wsKey !== wsKeyLinearPublic && - wsKey !== wsKeySpotPublic - ) { - this.logger.debug("Getting auth'd request params", { - ...loggerCategory, - wsKey, - }); - - const timeOffset = this.options.fetchTimeOffsetBeforeAuth - ? await this.restClient.fetchTimeOffset() - : 0; - - const signatureExpires = Date.now() + timeOffset + 5000; - - const signature = await signMessage( - 'GET/realtime' + signatureExpires, - secret - ); - - const authParams = { - api_key: this.options.key, - expires: signatureExpires, - signature, - }; - - return '?' + serializeParams(authParams); - } else if (!key || !secret) { - this.logger.warning( - 'Cannot authenticate websocket, either api or private keys missing.', - { ...loggerCategory, wsKey } - ); - } else { + if (PUBLIC_WS_KEYS.includes(wsKey)) { this.logger.debug('Starting public only websocket client.', { ...loggerCategory, wsKey, }); + return ''; } - return ''; + if (!key || !secret) { + this.logger.warning( + 'Cannot authenticate websocket, either api or private keys missing.', + { ...loggerCategory, wsKey } + ); + return ''; + } + + this.logger.debug("Getting auth'd request params", { + ...loggerCategory, + wsKey, + }); + + const timeOffset = this.options.fetchTimeOffsetBeforeAuth + ? (await this.restClient?.fetchTimeOffset()) || 0 + : 0; + + const signatureExpiresAt = Date.now() + timeOffset + 5000; + + const signature = await signMessage( + 'GET/realtime' + signatureExpiresAt, + secret + ); + + const authParams = { + api_key: this.options.key, + expires: signatureExpiresAt, + signature, + }; + + return '?' + serializeParams(authParams); } private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) { @@ -621,38 +686,31 @@ export class WebsocketClient extends EventEmitter { } const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; - // TODO: repetitive - if (this.isLinear() || wsKey.startsWith('linear')) { - if (wsKey === wsKeyLinearPublic) { + + switch (wsKey) { + case wsKeyLinearPublic: { return linearEndpoints.public[networkKey]; } - - if (wsKey === wsKeyLinearPrivate) { + case wsKeyLinearPrivate: { return linearEndpoints.private[networkKey]; } - - this.logger.error('Unhandled linear wsKey: ', { - ...loggerCategory, - wsKey, - }); - return linearEndpoints[networkKey]; - } - - if (this.isSpot() || wsKey.startsWith('spot')) { - if (wsKey === wsKeySpotPublic) { + case wsKeySpotPublic: { return spotEndpoints.public[networkKey]; } - - if (wsKey === wsKeySpotPrivate) { + case wsKeySpotPrivate: { return spotEndpoints.private[networkKey]; } - - this.logger.error('Unhandled spot wsKey: ', { ...loggerCategory, wsKey }); - return spotEndpoints[networkKey]; + case wsKeyInverse: { + return inverseEndpoints[networkKey]; + } + default: { + this.logger.error('getWsUrl(): Unhandled wsKey: ', { + ...loggerCategory, + wsKey, + }); + throw neverGuard(wsKey, `getWsUrl(): Unhandled wsKey`); + } } - - // fallback to inverse - return inverseEndpoints[networkKey]; } private getWsKeyForTopic(topic: string) { From 3f5039ef8bf265fbe50abf138dbb60ff5d6d03ad Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 15 Sep 2022 12:20:39 +0100 Subject: [PATCH 29/74] cleaning around websocket client --- examples/ws-private.ts | 4 +- examples/ws-public.ts | 34 +++++---- src/index.ts | 2 +- src/types/websockets.ts | 9 +-- src/util/WsStore.ts | 37 ++++----- src/util/websocket-util.ts | 87 ++++++++++++++++------ src/websocket-client.ts | 149 +++++++++++++------------------------ 7 files changed, 164 insertions(+), 158 deletions(-) diff --git a/examples/ws-private.ts b/examples/ws-private.ts index 51ce8d7..59623f3 100644 --- a/examples/ws-private.ts +++ b/examples/ws-private.ts @@ -1,5 +1,5 @@ import { DefaultLogger } from '../src'; -import { WebsocketClient, wsKeySpotPublic } from '../src/websocket-client'; +import { WebsocketClient } from '../src/websocket-client'; // or // import { DefaultLogger, WebsocketClient } from 'bybit-api'; @@ -33,6 +33,8 @@ import { WebsocketClient, wsKeySpotPublic } from '../src/websocket-client'; logger ); + // wsClient.subscribePublicSpotOrderbook('test', 'full'); + wsClient.on('update', (data) => { console.log('raw message received ', JSON.stringify(data, null, 2)); }); diff --git a/examples/ws-public.ts b/examples/ws-public.ts index 130c87c..2aa1cd7 100644 --- a/examples/ws-public.ts +++ b/examples/ws-public.ts @@ -1,8 +1,7 @@ -import { DefaultLogger } from '../src'; -import { WebsocketClient, wsKeySpotPublic } from '../src/websocket-client'; +import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; // or -// import { DefaultLogger, WebsocketClient } from 'bybit-api'; +// import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from 'bybit-api'; (async () => { const logger = { @@ -10,13 +9,16 @@ import { WebsocketClient, wsKeySpotPublic } from '../src/websocket-client'; // silly: () => {}, }; - const wsClient = new WebsocketClient({ - // key: key, - // secret: secret, - // market: 'inverse', - // market: 'linear', - market: 'spot', - }, logger); + const wsClient = new WebsocketClient( + { + // key: key, + // secret: secret, + market: 'linear', + // market: 'inverse', + // market: 'spot', + }, + logger + ); wsClient.on('update', (data) => { console.log('raw message received ', JSON.stringify(data, null, 2)); @@ -25,7 +27,7 @@ import { WebsocketClient, wsKeySpotPublic } from '../src/websocket-client'; wsClient.on('open', (data) => { console.log('connection opened open:', data.wsKey); - if (data.wsKey === wsKeySpotPublic) { + if (data.wsKey === WS_KEY_MAP.spotPublic) { // Spot public. // wsClient.subscribePublicSpotTrades('BTCUSDT'); // wsClient.subscribePublicSpotTradingPair('BTCUSDT'); @@ -40,16 +42,20 @@ import { WebsocketClient, wsKeySpotPublic } from '../src/websocket-client'; console.log('ws automatically reconnecting.... ', wsKey); }); wsClient.on('reconnected', (data) => { - console.log('ws has reconnected ', data?.wsKey ); + console.log('ws has reconnected ', data?.wsKey); }); // Inverse // wsClient.subscribe('trade'); // Linear - // wsClient.subscribe('trade.BTCUSDT'); + wsClient.subscribe('trade.BTCUSDT'); + + setTimeout(() => { + console.log('unsubscribing'); + wsClient.unsubscribe('trade.BTCUSDT'); + }, 5 * 1000); // For spot, request public connection first then send required topics on 'open' // wsClient.connectPublic(); - })(); diff --git a/src/index.ts b/src/index.ts index 1325998..4f8c4c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,6 @@ export * from './usdc-perpetual-client'; export * from './unified-margin-client'; export * from './websocket-client'; export * from './util/logger'; +export * from './util'; export * from './types'; -export * from './util/WsStore'; export * from './constants/enum'; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index a2a1cdf..856d537 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,4 +1,4 @@ -import { RestClientOptions } from '../util'; +import { RestClientOptions, WS_KEY_MAP } from '../util'; export type APIMarket = 'inverse' | 'linear' | 'spot'; //| 'v3'; @@ -69,12 +69,7 @@ export type WsPrivateTopic = export type WsTopic = WsPublicTopics | WsPrivateTopic; /** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */ -export type WsKey = - | 'inverse' - | 'linearPrivate' - | 'linearPublic' - | 'spotPrivate' - | 'spotPublic'; +export type WsKey = typeof WS_KEY_MAP[keyof typeof WS_KEY_MAP]; export interface WSClientConfigurableOptions { key?: string; diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index 7c421ce..9539079 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -1,4 +1,5 @@ import WebSocket from 'isomorphic-ws'; +import { WsKey } from '../types'; import { DefaultLogger } from './logger'; @@ -44,9 +45,9 @@ export default class WsStore { } /** Get WS stored state for key, optionally create if missing */ - get(key: string, createIfMissing?: true): WsStoredState; - get(key: string, createIfMissing?: false): WsStoredState | undefined; - get(key: string, createIfMissing?: boolean): WsStoredState | undefined { + get(key: WsKey, createIfMissing?: true): WsStoredState; + get(key: WsKey, createIfMissing?: false): WsStoredState | undefined; + get(key: WsKey, createIfMissing?: boolean): WsStoredState | undefined { if (this.wsState[key]) { return this.wsState[key]; } @@ -56,11 +57,11 @@ export default class WsStore { } } - getKeys(): string[] { - return Object.keys(this.wsState); + getKeys(): WsKey[] { + return Object.keys(this.wsState) as WsKey[]; } - create(key: string): WsStoredState | undefined { + create(key: WsKey): WsStoredState | undefined { if (this.hasExistingActiveConnection(key)) { this.logger.warning( 'WsStore setConnection() overwriting existing open connection: ', @@ -74,7 +75,7 @@ export default class WsStore { return this.get(key); } - delete(key: string) { + delete(key: WsKey) { if (this.hasExistingActiveConnection(key)) { const ws = this.getWs(key); this.logger.warning( @@ -88,15 +89,15 @@ export default class WsStore { /* connection websocket */ - hasExistingActiveConnection(key: string) { + hasExistingActiveConnection(key: WsKey) { return this.get(key) && this.isWsOpen(key); } - getWs(key: string): WebSocket | undefined { + getWs(key: WsKey): WebSocket | undefined { return this.get(key)?.ws; } - setWs(key: string, wsConnection: WebSocket): WebSocket { + setWs(key: WsKey, wsConnection: WebSocket): WebSocket { if (this.isWsOpen(key)) { this.logger.warning( 'WsStore setConnection() overwriting existing open connection: ', @@ -109,7 +110,7 @@ export default class WsStore { /* connection state */ - isWsOpen(key: string): boolean { + isWsOpen(key: WsKey): boolean { const existingConnection = this.getWs(key); return ( !!existingConnection && @@ -117,37 +118,37 @@ export default class WsStore { ); } - getConnectionState(key: string): WsConnectionStateEnum { + getConnectionState(key: WsKey): WsConnectionStateEnum { return this.get(key, true)!.connectionState!; } - setConnectionState(key: string, state: WsConnectionStateEnum) { + setConnectionState(key: WsKey, state: WsConnectionStateEnum) { this.get(key, true)!.connectionState = state; } - isConnectionState(key: string, state: WsConnectionStateEnum): boolean { + isConnectionState(key: WsKey, state: WsConnectionStateEnum): boolean { return this.getConnectionState(key) === state; } /* subscribed topics */ - getTopics(key: string): WsTopicList { + getTopics(key: WsKey): WsTopicList { return this.get(key, true).subscribedTopics; } getTopicsByKey(): Record { const result = {}; for (const refKey in this.wsState) { - result[refKey] = this.getTopics(refKey); + result[refKey] = this.getTopics(refKey as WsKey); } return result; } - addTopic(key: string, topic: WsTopic) { + addTopic(key: WsKey, topic: WsTopic) { return this.getTopics(key).add(topic); } - deleteTopic(key: string, topic: WsTopic) { + deleteTopic(key: WsKey, topic: WsTopic) { return this.getTopics(key).delete(topic); } } diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 0b639af..5dafc45 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -1,38 +1,84 @@ import { WsKey } from '../types'; -export const wsKeyInverse = 'inverse'; -export const wsKeyLinearPrivate = 'linearPrivate'; -export const wsKeyLinearPublic = 'linearPublic'; -export const wsKeySpotPrivate = 'spotPrivate'; -export const wsKeySpotPublic = 'spotPublic'; +interface NetworkMapV3 { + livenet: string; + livenet2?: string; + testnet: string; + testnet2?: string; +} -export const WS_KEY_MAP = { - inverse: wsKeyInverse, - linearPrivate: wsKeyLinearPrivate, - linearPublic: wsKeyLinearPublic, - spotPrivate: wsKeySpotPrivate, - spotPublic: wsKeySpotPublic, +type PublicPrivateNetwork = 'public' | 'private'; + +export const WS_BASE_URL_MAP: Record< + string, + Record +> = { + inverse: { + private: { + livenet: 'wss://stream.bybit.com/realtime', + testnet: 'wss://stream-testnet.bybit.com/realtime', + }, + public: { + livenet: 'wss://stream.bybit.com/realtime', + testnet: 'wss://stream-testnet.bybit.com/realtime', + }, + }, + linear: { + private: { + livenet: 'wss://stream.bybit.com/realtime_private', + livenet2: 'wss://stream.bytick.com/realtime_private', + testnet: 'wss://stream-testnet.bybit.com/realtime_private', + }, + public: { + livenet: 'wss://stream.bybit.com/realtime_public', + livenet2: 'wss://stream.bytick.com/realtime_public', + testnet: 'wss://stream-testnet.bybit.com/realtime_public', + }, + }, + spot: { + private: { + livenet: 'wss://stream.bybit.com/spot/ws', + testnet: 'wss://stream-testnet.bybit.com/spot/ws', + }, + public: { + livenet: 'wss://stream.bybit.com/spot/quote/ws/v1', + livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2', + testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1', + testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2', + }, + }, }; -export const PUBLIC_WS_KEYS = [WS_KEY_MAP.linearPublic, WS_KEY_MAP.spotPublic]; +export const WS_KEY_MAP = { + inverse: 'inverse', + linearPrivate: 'linearPrivate', + linearPublic: 'linearPublic', + spotPrivate: 'spotPrivate', + spotPublic: 'spotPublic', +} as const; + +export const PUBLIC_WS_KEYS = [ + WS_KEY_MAP.linearPublic, + WS_KEY_MAP.spotPublic, +] as string[]; export function getLinearWsKeyForTopic(topic: string): WsKey { - const privateLinearTopics = [ + const privateTopics = [ 'position', 'execution', 'order', 'stop_order', 'wallet', ]; - if (privateLinearTopics.includes(topic)) { - return wsKeyLinearPrivate; + if (privateTopics.includes(topic)) { + return WS_KEY_MAP.linearPrivate; } - return wsKeyLinearPublic; + return WS_KEY_MAP.linearPublic; } export function getSpotWsKeyForTopic(topic: string): WsKey { - const privateLinearTopics = [ + const privateTopics = [ 'position', 'execution', 'order', @@ -42,9 +88,8 @@ export function getSpotWsKeyForTopic(topic: string): WsKey { 'ticketInfo', ]; - if (privateLinearTopics.includes(topic)) { - return wsKeySpotPrivate; + if (privateTopics.includes(topic)) { + return WS_KEY_MAP.spotPrivate; } - - return wsKeySpotPublic; + return WS_KEY_MAP.spotPublic; } diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 759a4a3..2b966c9 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -6,7 +6,9 @@ import { LinearClient } from './linear-client'; import { SpotClientV3 } from './spot-client-v3'; import { SpotClient } from './spot-client'; -import { DefaultLogger } from './util/logger'; +import { signMessage } from './util/node-support'; +import WsStore from './util/WsStore'; + import { APIMarket, KlineInterval, @@ -17,82 +19,22 @@ import { WsTopic, } from './types'; -import { signMessage } from './util/node-support'; - -import WsStore from './util/WsStore'; import { serializeParams, isWsPong, getLinearWsKeyForTopic, getSpotWsKeyForTopic, - wsKeyInverse, - wsKeyLinearPrivate, - wsKeyLinearPublic, - wsKeySpotPrivate, - wsKeySpotPublic, WsConnectionStateEnum, PUBLIC_WS_KEYS, + WS_KEY_MAP, + DefaultLogger, + WS_BASE_URL_MAP, } from './util'; -const inverseEndpoints = { - livenet: 'wss://stream.bybit.com/realtime', - testnet: 'wss://stream-testnet.bybit.com/realtime', -}; - -interface NetworkMapV3 { - livenet: string; - livenet2?: string; - testnet: string; - testnet2?: string; -} - -type NetworkType = 'public' | 'private'; - function neverGuard(x: never, msg: string): Error { return new Error(`Unhandled value exception "x", ${msg}`); } -const WS_BASE_URL_MAP: Record> = { - linear: { - private: { - livenet: 'wss://stream.bybit.com/realtime_private', - livenet2: 'wss://stream.bytick.com/realtime_private', - testnet: 'wss://stream-testnet.bybit.com/realtime_private', - }, - public: { - livenet: 'wss://stream.bybit.com/realtime_public', - livenet2: 'wss://stream.bytick.com/realtime_public', - testnet: 'wss://stream-testnet.bybit.com/realtime_public', - }, - }, -}; - -const linearEndpoints: Record = { - private: { - livenet: 'wss://stream.bybit.com/realtime_private', - livenet2: 'wss://stream.bytick.com/realtime_private', - testnet: 'wss://stream-testnet.bybit.com/realtime_private', - }, - public: { - livenet: 'wss://stream.bybit.com/realtime_public', - livenet2: 'wss://stream.bytick.com/realtime_public', - testnet: 'wss://stream-testnet.bybit.com/realtime_public', - }, -}; - -const spotEndpoints: Record = { - private: { - livenet: 'wss://stream.bybit.com/spot/ws', - testnet: 'wss://stream-testnet.bybit.com/spot/ws', - }, - public: { - livenet: 'wss://stream.bybit.com/spot/quote/ws/v1', - livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2', - testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1', - testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2', - }, -}; - const loggerCategory = { category: 'bybit-ws' }; export declare interface WebsocketClient { @@ -280,16 +222,19 @@ export class WebsocketClient extends EventEmitter { public connectAll(): Promise[] { switch (this.options.market) { case 'inverse': { - return [this.connect(wsKeyInverse)]; + return [this.connect(WS_KEY_MAP.inverse)]; } case 'linear': { return [ - this.connect(wsKeyLinearPublic), - this.connect(wsKeyLinearPrivate), + this.connect(WS_KEY_MAP.linearPublic), + this.connect(WS_KEY_MAP.linearPrivate), ]; } case 'spot': { - return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; + return [ + this.connect(WS_KEY_MAP.spotPublic), + this.connect(WS_KEY_MAP.spotPrivate), + ]; } default: { throw neverGuard(this.options.market, `connectAll(): Unhandled market`); @@ -300,13 +245,13 @@ export class WebsocketClient extends EventEmitter { public connectPublic(): Promise { switch (this.options.market) { case 'inverse': { - return this.connect(wsKeyInverse); + return this.connect(WS_KEY_MAP.inverse); } case 'linear': { - return this.connect(wsKeyLinearPublic); + return this.connect(WS_KEY_MAP.linearPublic); } case 'spot': { - return this.connect(wsKeySpotPublic); + return this.connect(WS_KEY_MAP.spotPublic); } default: { throw neverGuard( @@ -320,13 +265,13 @@ export class WebsocketClient extends EventEmitter { public connectPrivate(): Promise | undefined { switch (this.options.market) { case 'inverse': { - return this.connect(wsKeyInverse); + return this.connect(WS_KEY_MAP.inverse); } case 'linear': { - return this.connect(wsKeyLinearPrivate); + return this.connect(WS_KEY_MAP.linearPrivate); } case 'spot': { - return this.connect(wsKeySpotPrivate); + return this.connect(WS_KEY_MAP.spotPrivate); } default: { throw neverGuard( @@ -672,7 +617,7 @@ export class WebsocketClient extends EventEmitter { } } - private getWs(wsKey: string) { + private getWs(wsKey: WsKey) { return this.wsStore.getWs(wsKey); } @@ -688,20 +633,21 @@ export class WebsocketClient extends EventEmitter { const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; switch (wsKey) { - case wsKeyLinearPublic: { - return linearEndpoints.public[networkKey]; + case WS_KEY_MAP.linearPublic: { + return WS_BASE_URL_MAP.linear.public[networkKey]; } - case wsKeyLinearPrivate: { - return linearEndpoints.private[networkKey]; + case WS_KEY_MAP.linearPrivate: { + return WS_BASE_URL_MAP.linear.private[networkKey]; } - case wsKeySpotPublic: { - return spotEndpoints.public[networkKey]; + case WS_KEY_MAP.spotPublic: { + return WS_BASE_URL_MAP.spot.public[networkKey]; } - case wsKeySpotPrivate: { - return spotEndpoints.private[networkKey]; + case WS_KEY_MAP.spotPrivate: { + return WS_BASE_URL_MAP.linear.private[networkKey]; } - case wsKeyInverse: { - return inverseEndpoints[networkKey]; + case WS_KEY_MAP.inverse: { + // private and public are on the same WS connection + return WS_BASE_URL_MAP.inverse.public[networkKey]; } default: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { @@ -713,14 +659,24 @@ export class WebsocketClient extends EventEmitter { } } - private getWsKeyForTopic(topic: string) { - if (this.isInverse()) { - return wsKeyInverse; + private getWsKeyForTopic(topic: string): WsKey { + switch (this.options.market) { + case 'inverse': { + return WS_KEY_MAP.inverse; + } + case 'linear': { + return getLinearWsKeyForTopic(topic); + } + case 'spot': { + return getSpotWsKeyForTopic(topic); + } + default: { + throw neverGuard( + this.options.market, + `connectPublic(): Unhandled market` + ); + } } - if (this.isLinear()) { - return getLinearWsKeyForTopic(topic); - } - return getSpotWsKeyForTopic(topic); } private wrongMarketError(market: APIMarket) { @@ -736,7 +692,7 @@ export class WebsocketClient extends EventEmitter { } return this.tryWsSend( - wsKeySpotPublic, + WS_KEY_MAP.spotPublic, JSON.stringify({ topic: 'trade', event: 'sub', @@ -754,7 +710,7 @@ export class WebsocketClient extends EventEmitter { } return this.tryWsSend( - wsKeySpotPublic, + WS_KEY_MAP.spotPublic, JSON.stringify({ symbol, topic: 'realtimes', @@ -776,7 +732,7 @@ export class WebsocketClient extends EventEmitter { } return this.tryWsSend( - wsKeySpotPublic, + WS_KEY_MAP.spotPublic, JSON.stringify({ symbol, topic: 'kline_' + candleSize, @@ -791,6 +747,7 @@ export class WebsocketClient extends EventEmitter { //ws.send('{"symbol":"BTCUSDT","topic":"depth","event":"sub","params":{"binary":false}}'); //ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}'); //ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}'); + public subscribePublicSpotOrderbook( symbol: string, depth: 'full' | 'merge' | 'delta', @@ -831,6 +788,6 @@ export class WebsocketClient extends EventEmitter { if (dumpScale) { msg.params.dumpScale = dumpScale; } - return this.tryWsSend(wsKeySpotPublic, JSON.stringify(msg)); + return this.tryWsSend(WS_KEY_MAP.spotPublic, JSON.stringify(msg)); } } From 1b422a1bebc6448caedd2ac44e9b28ffd9abd601 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 15 Sep 2022 14:11:17 +0100 Subject: [PATCH 30/74] add basic ws connectivity tests --- src/types/websockets.ts | 4 +- src/websocket-client.ts | 81 ++++++++++++++++++++------------ test/inverse/private.ws.test.ts | 75 ++++++++++++++++++++++++++++++ test/inverse/public.ws.test.ts | 75 ++++++++++++++++++++++++++++++ test/linear/private.ws.test.ts | 73 +++++++++++++++++++++++++++++ test/linear/public.ws.test.ts | 76 ++++++++++++++++++++++++++++++ test/ws.util.ts | 82 +++++++++++++++++++++++++++++++++ 7 files changed, 435 insertions(+), 31 deletions(-) create mode 100644 test/inverse/private.ws.test.ts create mode 100644 test/inverse/public.ws.test.ts create mode 100644 test/linear/private.ws.test.ts create mode 100644 test/linear/public.ws.test.ts create mode 100644 test/ws.util.ts diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 856d537..4b713db 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -74,7 +74,7 @@ export type WsKey = typeof WS_KEY_MAP[keyof typeof WS_KEY_MAP]; export interface WSClientConfigurableOptions { key?: string; secret?: string; - livenet?: boolean; + testnet?: boolean; /** * The API group this client should connect to. @@ -94,7 +94,7 @@ export interface WSClientConfigurableOptions { } export interface WebsocketClientOptions extends WSClientConfigurableOptions { - livenet: boolean; + testnet?: boolean; market: APIMarket; pongTimeout: number; pingInterval: number; diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 2b966c9..1e81c01 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -37,16 +37,36 @@ function neverGuard(x: never, msg: string): Error { const loggerCategory = { category: 'bybit-ws' }; +export type WsClientEvent = + | 'open' + | 'update' + | 'close' + | 'error' + | 'reconnect' + | 'reconnected' + | 'response'; + +interface WebsocketClientEvents { + open: (evt: { wsKey: WsKey; event: any }) => void; + reconnect: (evt: { wsKey: WsKey; event: any }) => void; + reconnected: (evt: { wsKey: WsKey; event: any }) => void; + close: (evt: { wsKey: WsKey; event: any }) => void; + response: (response: any) => void; + update: (response: any) => void; + error: (response: any) => void; +} + +// Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837 export declare interface WebsocketClient { - on( - event: 'open' | 'reconnected', - listener: ({ wsKey: WsKey, event: any }) => void + on( + event: U, + listener: WebsocketClientEvents[U] ): this; - on( - event: 'response' | 'update' | 'error', - listener: (response: any) => void - ): this; - on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this; + + emit( + event: U, + ...args: Parameters + ): boolean; } export class WebsocketClient extends EventEmitter { @@ -65,7 +85,7 @@ export class WebsocketClient extends EventEmitter { this.wsStore = new WsStore(this.logger); this.options = { - livenet: false, + testnet: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500, @@ -89,7 +109,7 @@ export class WebsocketClient extends EventEmitter { this.restClient = new InverseClient( undefined, undefined, - this.isLivenet(), + !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -99,7 +119,7 @@ export class WebsocketClient extends EventEmitter { this.restClient = new LinearClient( undefined, undefined, - this.isLivenet(), + !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -109,7 +129,7 @@ export class WebsocketClient extends EventEmitter { this.restClient = new SpotClient( undefined, undefined, - this.isLivenet(), + !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -134,8 +154,8 @@ export class WebsocketClient extends EventEmitter { } } - public isLivenet(): boolean { - return this.options.livenet === true; + public isTestnet(): boolean { + return this.options.testnet === true; } public isLinear(): boolean { @@ -216,6 +236,13 @@ export class WebsocketClient extends EventEmitter { this.getWs(wsKey)?.close(); } + public closeAll() { + const keys = this.wsStore.getKeys(); + keys.forEach((key) => { + this.close(key); + }); + } + /** * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ @@ -528,9 +555,8 @@ export class WebsocketClient extends EventEmitter { this.logger.info('Websocket connected', { ...loggerCategory, wsKey, - livenet: this.isLivenet(), - linear: this.isLinear(), - spot: this.isSpot(), + testnet: this.isTestnet(), + market: this.options.market, }); this.emit('open', { wsKey, event }); } else if ( @@ -560,7 +586,12 @@ export class WebsocketClient extends EventEmitter { const msg = JSON.parse((event && event.data) || event); if (msg['success'] || msg?.pong) { - return this.onWsMessageResponse(msg, wsKey); + if (isWsPong(msg)) { + this.logger.silly('Received pong', { ...loggerCategory, wsKey }); + } else { + this.emit('response', msg); + } + return; } if (msg.topic) { @@ -602,18 +633,10 @@ export class WebsocketClient extends EventEmitter { this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING ) { this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); - this.emit('reconnect', { wsKey }); + this.emit('reconnect', { wsKey, event }); } else { this.setWsState(wsKey, WsConnectionStateEnum.INITIAL); - this.emit('close', { wsKey }); - } - } - - private onWsMessageResponse(response: any, wsKey: WsKey) { - if (isWsPong(response)) { - this.logger.silly('Received pong', { ...loggerCategory, wsKey }); - } else { - this.emit('response', response); + this.emit('close', { wsKey, event }); } } @@ -630,7 +653,7 @@ export class WebsocketClient extends EventEmitter { return this.options.wsUrl; } - const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; + const networkKey = this.isTestnet() ? 'testnet' : 'livenet'; switch (wsKey) { case WS_KEY_MAP.linearPublic: { diff --git a/test/inverse/private.ws.test.ts b/test/inverse/private.ws.test.ts new file mode 100644 index 0000000..cb73e13 --- /dev/null +++ b/test/inverse/private.ws.test.ts @@ -0,0 +1,75 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + promiseSleep, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Private Inverse Perps Websocket Client', () => { + let wsClient: WebsocketClient; + + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'inverse', + key: API_KEY, + secret: API_SECRET, + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPrivate(); + }); + + afterAll(() => { + // await promiseSleep(2000); + wsClient.closeAll(); + }); + + it('should open a ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.inverse, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to private wallet events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'wallet'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + + // No easy way to trigger a private event (other than executing trades) + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: wsTopic, + // data: expect.any(Array), + // }); + + wsClient.subscribe(wsTopic); + + await Promise.all([wsResponsePromise]); + }); +}); diff --git a/test/inverse/public.ws.test.ts b/test/inverse/public.ws.test.ts new file mode 100644 index 0000000..cb75af7 --- /dev/null +++ b/test/inverse/public.ws.test.ts @@ -0,0 +1,75 @@ +import { + LinearClient, + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + promiseSleep, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Inverse Perps Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'inverse', + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPublic(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.inverse, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public orderBookL2_25 events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'orderBookL2_25.BTCUSD'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + expect(wsUpdatePromise).resolves.toMatchObject({ + topic: wsTopic, + data: expect.any(Array), + }); + + wsClient.subscribe(wsTopic); + + try { + await wsResponsePromise; + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + } + + try { + await wsUpdatePromise; + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + } + }); +}); diff --git a/test/linear/private.ws.test.ts b/test/linear/private.ws.test.ts new file mode 100644 index 0000000..a87457e --- /dev/null +++ b/test/linear/private.ws.test.ts @@ -0,0 +1,73 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + promiseSleep, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Private Linear Websocket Client', () => { + let wsClient: WebsocketClient; + + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'linear', + key: API_KEY, + secret: API_SECRET, + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPrivate(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.linearPrivate, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to private wallet events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'wallet'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + + // No easy way to trigger a private event (other than executing trades) + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: wsTopic, + // data: expect.any(Array), + // }); + + wsClient.subscribe(wsTopic); + + await Promise.all([wsResponsePromise]); + }); +}); diff --git a/test/linear/public.ws.test.ts b/test/linear/public.ws.test.ts new file mode 100644 index 0000000..23e9ecf --- /dev/null +++ b/test/linear/public.ws.test.ts @@ -0,0 +1,76 @@ +import { + LinearClient, + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Linear Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'linear', + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPublic(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.linearPublic, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public orderBookL2_25 events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'orderBookL2_25.BTCUSDT'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + expect(wsUpdatePromise).resolves.toMatchObject({ + topic: wsTopic, + data: { + order_book: expect.any(Array), + }, + }); + + wsClient.subscribe(wsTopic); + + try { + await wsResponsePromise; + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + } + + try { + await wsUpdatePromise; + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + } + }); +}); diff --git a/test/ws.util.ts b/test/ws.util.ts new file mode 100644 index 0000000..3eb19eb --- /dev/null +++ b/test/ws.util.ts @@ -0,0 +1,82 @@ +import { WebsocketClient, WsClientEvent } from '../src'; + +export const silentLogger = { + silly: () => {}, + debug: () => {}, + notice: () => {}, + info: () => {}, + warning: () => {}, + error: () => {}, +}; + +export const WS_OPEN_EVENT_PARTIAL = { + type: 'open', +}; + +/** Resolves a promise if an event is seen before a timeout (defaults to 2.5 seconds) */ +export function waitForSocketEvent( + wsClient: WebsocketClient, + event: WsClientEvent, + timeoutMs: number = 4.5 * 1000 +) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject( + `Failed to receive "${event}" event before timeout. Check that these are correct: topic, api keys (if private), signature process (if private)` + ); + }, timeoutMs); + + let resolvedOnce = false; + + wsClient.on(event, (event) => { + clearTimeout(timeout); + resolve(event); + resolvedOnce = true; + }); + + wsClient.on('error', (event) => { + clearTimeout(timeout); + if (!resolvedOnce) { + reject(event); + } + }); + + // if (event !== 'close') { + // wsClient.on('close', (event) => { + // clearTimeout(timeout); + + // if (!resolvedOnce) { + // reject(event); + // } + // }); + // } + }); +} + +export function logAllEvents(wsClient: WebsocketClient) { + wsClient.on('update', (data) => { + console.log('wsUpdate: ', JSON.stringify(data, null, 2)); + }); + + wsClient.on('open', (data) => { + console.log('wsOpen: ', data.wsKey); + }); + wsClient.on('response', (data) => { + console.log('wsResponse ', JSON.stringify(data, null, 2)); + }); + wsClient.on('reconnect', ({ wsKey }) => { + console.log('wsReconnecting ', wsKey); + }); + wsClient.on('reconnected', (data) => { + console.log('wsReconnected ', data?.wsKey); + }); + wsClient.on('close', (data) => { + // console.log('wsClose: ', data); + }); +} + +export function promiseSleep(ms: number) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} From 27bd81593c2597a2c8ad58161a98c17165c5a459 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 15 Sep 2022 16:47:39 +0100 Subject: [PATCH 31/74] improve private ws tests --- test/inverse/private.ws.test.ts | 112 +++++++++++++++++++------------ test/linear/private.ws.test.ts | 113 +++++++++++++++++++++----------- test/linear/public.ws.test.ts | 24 +++++++ test/ws.util.ts | 2 +- 4 files changed, 167 insertions(+), 84 deletions(-) diff --git a/test/inverse/private.ws.test.ts b/test/inverse/private.ws.test.ts index cb73e13..7a4c325 100644 --- a/test/inverse/private.ws.test.ts +++ b/test/inverse/private.ws.test.ts @@ -4,72 +4,98 @@ import { WS_KEY_MAP, } from '../../src'; import { - logAllEvents, - promiseSleep, silentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; describe('Private Inverse Perps Websocket Client', () => { - let wsClient: WebsocketClient; - const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; - it('should have api credentials to test with', () => { - expect(API_KEY).toStrictEqual(expect.any(String)); - expect(API_SECRET).toStrictEqual(expect.any(String)); - }); - const wsClientOptions: WSClientConfigurableOptions = { market: 'inverse', key: API_KEY, secret: API_SECRET, }; - beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); - wsClient.connectPrivate(); + describe('with invalid credentials', () => { + it('should fail to open a connection if keys/signature are incorrect', async () => { + const badClient = new WebsocketClient( + { + ...wsClientOptions, + key: 'bad', + secret: 'bad', + }, + silentLogger + ); + + const wsOpenPromise = waitForSocketEvent(badClient, 'open', 2500); + + badClient.connectPrivate(); + + expect(wsOpenPromise).rejects.toMatch('Failed to receive'); + + try { + await Promise.all([wsOpenPromise]); + } catch (e) { + // console.error() + } + badClient.closeAll(); + }); }); - afterAll(() => { - // await promiseSleep(2000); - wsClient.closeAll(); - }); + describe('with valid API credentails', () => { + let wsClient: WebsocketClient; - it('should open a ws connection', async () => { - const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.inverse, + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); }); - await Promise.all([wsOpenPromise]); - }); - - it('should subscribe to private wallet events', async () => { - const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); - // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); - - const wsTopic = 'wallet'; - expect(wsResponsePromise).resolves.toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, - success: true, + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPrivate(); }); - // No easy way to trigger a private event (other than executing trades) - // expect(wsUpdatePromise).resolves.toMatchObject({ - // topic: wsTopic, - // data: expect.any(Array), - // }); + afterAll(() => { + // await promiseSleep(2000); + wsClient.closeAll(); + }); - wsClient.subscribe(wsTopic); + it('should open a ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - await Promise.all([wsResponsePromise]); + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.inverse, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to private wallet events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'wallet'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + + // No easy way to trigger a private event (other than executing trades) + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: wsTopic, + // data: expect.any(Array), + // }); + + wsClient.subscribe(wsTopic); + + await Promise.all([wsResponsePromise]); + }); }); }); diff --git a/test/linear/private.ws.test.ts b/test/linear/private.ws.test.ts index a87457e..fa197ff 100644 --- a/test/linear/private.ws.test.ts +++ b/test/linear/private.ws.test.ts @@ -4,70 +4,103 @@ import { WS_KEY_MAP, } from '../../src'; import { - promiseSleep, silentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; describe('Private Linear Websocket Client', () => { - let wsClient: WebsocketClient; - const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; - it('should have api credentials to test with', () => { - expect(API_KEY).toStrictEqual(expect.any(String)); - expect(API_SECRET).toStrictEqual(expect.any(String)); - }); - const wsClientOptions: WSClientConfigurableOptions = { market: 'linear', key: API_KEY, secret: API_SECRET, }; - beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); - wsClient.connectPrivate(); + describe('with invalid credentials', () => { + it('should fail to open a connection if keys/signature are incorrect', async () => { + const badClient = new WebsocketClient( + { + ...wsClientOptions, + key: 'bad', + secret: 'bad', + }, + silentLogger + ); + + const wsOpenPromise = waitForSocketEvent(badClient, 'open', 2500); + + badClient.connectPrivate(); + + expect(wsOpenPromise).rejects.toMatch('Failed to receive'); + + try { + await Promise.all([wsOpenPromise]); + } catch (e) { + // console.error() + } + badClient.closeAll(); + }); }); - afterAll(() => { - wsClient.closeAll(); - }); + describe('with valid API credentails', () => { + let wsClient: WebsocketClient; - it('should open a ws connection', async () => { - const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.linearPrivate, + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); }); - await Promise.all([wsOpenPromise]); - }); + const wsClientOptions: WSClientConfigurableOptions = { + market: 'linear', + key: API_KEY, + secret: API_SECRET, + }; - it('should subscribe to private wallet events', async () => { - const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); - // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); - - const wsTopic = 'wallet'; - expect(wsResponsePromise).resolves.toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, - success: true, + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPrivate(); }); - // No easy way to trigger a private event (other than executing trades) - // expect(wsUpdatePromise).resolves.toMatchObject({ - // topic: wsTopic, - // data: expect.any(Array), - // }); + afterAll(() => { + wsClient.closeAll(); + }); - wsClient.subscribe(wsTopic); + it('should open a ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - await Promise.all([wsResponsePromise]); + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.linearPrivate, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to private wallet events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'wallet'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + + // No easy way to trigger a private event (other than executing trades) + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: wsTopic, + // data: expect.any(Array), + // }); + + wsClient.subscribe(wsTopic); + + await Promise.all([wsResponsePromise]); + }); }); }); diff --git a/test/linear/public.ws.test.ts b/test/linear/public.ws.test.ts index 23e9ecf..e506d5a 100644 --- a/test/linear/public.ws.test.ts +++ b/test/linear/public.ws.test.ts @@ -73,4 +73,28 @@ describe('Public Linear Websocket Client', () => { console.error(`Wait for "${wsTopic}" event exception: `, e); } }); + + it('should fail to subscribe to private events (no keys)', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'wallet'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + + // No easy way to trigger a private event (other than executing trades) + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: wsTopic, + // data: expect.any(Array), + // }); + + wsClient.subscribe(wsTopic); + + await Promise.all([wsResponsePromise]); + }); }); diff --git a/test/ws.util.ts b/test/ws.util.ts index 3eb19eb..4b1ff75 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -13,7 +13,7 @@ export const WS_OPEN_EVENT_PARTIAL = { type: 'open', }; -/** Resolves a promise if an event is seen before a timeout (defaults to 2.5 seconds) */ +/** Resolves a promise if an event is seen before a timeout (defaults to 4.5 seconds) */ export function waitForSocketEvent( wsClient: WebsocketClient, event: WsClientEvent, From f61e79934de2feac58ba5e6871c52c9aa1f49210 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 15 Sep 2022 19:06:35 +0100 Subject: [PATCH 32/74] cleaning around tests --- src/types/websockets.ts | 2 +- src/util/websocket-util.ts | 50 ++++-- src/websocket-client.ts | 144 +++++++++++------- ...{private.ws.test.ts => ws.private.test.ts} | 0 .../{public.ws.test.ts => ws.public.test.ts} | 2 +- ...{private.ws.test.ts => ws.private.test.ts} | 2 +- .../{public.ws.test.ts => ws.public.test.ts} | 5 +- test/spot/ws.private.v1.test.ts | 58 +++++++ test/spot/ws.public.v1.test.ts | 64 ++++++++ test/spot/ws.public.v3.test.ts | 72 +++++++++ 10 files changed, 323 insertions(+), 76 deletions(-) rename test/inverse/{private.ws.test.ts => ws.private.test.ts} (100%) rename test/inverse/{public.ws.test.ts => ws.public.test.ts} (96%) rename test/linear/{private.ws.test.ts => ws.private.test.ts} (97%) rename test/linear/{public.ws.test.ts => ws.public.test.ts} (94%) create mode 100644 test/spot/ws.private.v1.test.ts create mode 100644 test/spot/ws.public.v1.test.ts create mode 100644 test/spot/ws.public.v3.test.ts diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 4b713db..180f518 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,6 +1,6 @@ import { RestClientOptions, WS_KEY_MAP } from '../util'; -export type APIMarket = 'inverse' | 'linear' | 'spot'; //| 'v3'; +export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotV3'; //| 'v3'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 5dafc45..72f6d53 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -1,4 +1,4 @@ -import { WsKey } from '../types'; +import { APIMarket, WsKey } from '../types'; interface NetworkMapV3 { livenet: string; @@ -10,42 +10,52 @@ interface NetworkMapV3 { type PublicPrivateNetwork = 'public' | 'private'; export const WS_BASE_URL_MAP: Record< - string, + APIMarket, Record > = { inverse: { - private: { + public: { livenet: 'wss://stream.bybit.com/realtime', testnet: 'wss://stream-testnet.bybit.com/realtime', }, - public: { + private: { livenet: 'wss://stream.bybit.com/realtime', testnet: 'wss://stream-testnet.bybit.com/realtime', }, }, linear: { - private: { - livenet: 'wss://stream.bybit.com/realtime_private', - livenet2: 'wss://stream.bytick.com/realtime_private', - testnet: 'wss://stream-testnet.bybit.com/realtime_private', - }, public: { livenet: 'wss://stream.bybit.com/realtime_public', livenet2: 'wss://stream.bytick.com/realtime_public', testnet: 'wss://stream-testnet.bybit.com/realtime_public', }, + private: { + livenet: 'wss://stream.bybit.com/realtime_private', + livenet2: 'wss://stream.bytick.com/realtime_private', + testnet: 'wss://stream-testnet.bybit.com/realtime_private', + }, }, spot: { - private: { - livenet: 'wss://stream.bybit.com/spot/ws', - testnet: 'wss://stream-testnet.bybit.com/spot/ws', - }, public: { livenet: 'wss://stream.bybit.com/spot/quote/ws/v1', livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2', testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1', testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2', }, + private: { + livenet: 'wss://stream.bybit.com/spot/ws', + testnet: 'wss://stream-testnet.bybit.com/spot/ws', + }, + }, + spotV3: { + public: { + livenet: 'wss://stream.bybit.com/spot/public/v3', + testnet: 'wss://stream-testnet.bybit.com/spot/public/v3', + }, + private: { + livenet: 'wss://stream.bybit.com/spot/private/v3', + testnet: 'wss://stream-testnet.bybit.com/spot/private/v3', + }, }, }; @@ -55,6 +65,8 @@ export const WS_KEY_MAP = { linearPublic: 'linearPublic', spotPrivate: 'spotPrivate', spotPublic: 'spotPublic', + spotV3Private: 'spotV3Private', + spotV3Public: 'spotV3Public', } as const; export const PUBLIC_WS_KEYS = [ @@ -77,7 +89,10 @@ export function getLinearWsKeyForTopic(topic: string): WsKey { return WS_KEY_MAP.linearPublic; } -export function getSpotWsKeyForTopic(topic: string): WsKey { +export function getSpotWsKeyForTopic( + topic: string, + apiVersion: 'v1' | 'v3' +): WsKey { const privateTopics = [ 'position', 'execution', @@ -88,6 +103,13 @@ export function getSpotWsKeyForTopic(topic: string): WsKey { 'ticketInfo', ]; + if (apiVersion === 'v3') { + if (privateTopics.includes(topic)) { + return WS_KEY_MAP.spotV3Private; + } + return WS_KEY_MAP.spotV3Public; + } + if (privateTopics.includes(topic)) { return WS_KEY_MAP.spotPrivate; } diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 1e81c01..3f5c92e 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -136,6 +136,17 @@ export class WebsocketClient extends EventEmitter { this.connectPublic(); break; } + case 'spotV3': { + this.restClient = new SpotClientV3( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } // if (this.isV3()) { // this.restClient = new SpotClientV3( // undefined, @@ -175,59 +186,6 @@ export class WebsocketClient extends EventEmitter { // return this.options.market === 'v3'; // } - /** - * Add topic/topics to WS subscription list - */ - public subscribe(wsTopics: WsTopic[] | WsTopic) { - const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; - topics.forEach((topic) => - this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic) - ); - - // attempt to send subscription topic per websocket - this.wsStore.getKeys().forEach((wsKey: WsKey) => { - // if connected, send subscription request - if ( - this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) - ) { - return this.requestSubscribeTopics(wsKey, topics); - } - - // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect - if ( - !this.wsStore.isConnectionState( - wsKey, - WsConnectionStateEnum.CONNECTING - ) && - !this.wsStore.isConnectionState( - wsKey, - WsConnectionStateEnum.RECONNECTING - ) - ) { - return this.connect(wsKey); - } - }); - } - - /** - * Remove topic/topics from WS subscription list - */ - public unsubscribe(wsTopics: WsTopic[] | WsTopic) { - const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; - topics.forEach((topic) => - this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic) - ); - - this.wsStore.getKeys().forEach((wsKey: WsKey) => { - // unsubscribe request only necessary if active connection exists - if ( - this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) - ) { - this.requestUnsubscribeTopics(wsKey, topics); - } - }); - } - public close(wsKey: WsKey) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); @@ -263,6 +221,12 @@ export class WebsocketClient extends EventEmitter { this.connect(WS_KEY_MAP.spotPrivate), ]; } + case 'spotV3': { + return [ + this.connect(WS_KEY_MAP.spotV3Public), + this.connect(WS_KEY_MAP.spotV3Private), + ]; + } default: { throw neverGuard(this.options.market, `connectAll(): Unhandled market`); } @@ -280,6 +244,9 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return this.connect(WS_KEY_MAP.spotPublic); } + case 'spotV3': { + return this.connect(WS_KEY_MAP.spotV3Public); + } default: { throw neverGuard( this.options.market, @@ -300,6 +267,9 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return this.connect(WS_KEY_MAP.spotPrivate); } + case 'spotV3': { + return this.connect(WS_KEY_MAP.spotV3Private); + } default: { throw neverGuard( this.options.market, @@ -503,7 +473,7 @@ export class WebsocketClient extends EventEmitter { this.tryWsSend(wsKey, wsMessage); } - private tryWsSend(wsKey: WsKey, wsMessage: string) { + public tryWsSend(wsKey: WsKey, wsMessage: string) { try { this.logger.silly(`Sending upstream ws message: `, { ...loggerCategory, @@ -666,7 +636,13 @@ export class WebsocketClient extends EventEmitter { return WS_BASE_URL_MAP.spot.public[networkKey]; } case WS_KEY_MAP.spotPrivate: { - return WS_BASE_URL_MAP.linear.private[networkKey]; + return WS_BASE_URL_MAP.spot.private[networkKey]; + } + case WS_KEY_MAP.spotV3Public: { + return WS_BASE_URL_MAP.spot.public[networkKey]; + } + case WS_KEY_MAP.spotV3Private: { + return WS_BASE_URL_MAP.spot.private[networkKey]; } case WS_KEY_MAP.inverse: { // private and public are on the same WS connection @@ -691,7 +667,10 @@ export class WebsocketClient extends EventEmitter { return getLinearWsKeyForTopic(topic); } case 'spot': { - return getSpotWsKeyForTopic(topic); + return getSpotWsKeyForTopic(topic, 'v1'); + } + case 'spotV3': { + return getSpotWsKeyForTopic(topic, 'v3'); } default: { throw neverGuard( @@ -708,6 +687,59 @@ export class WebsocketClient extends EventEmitter { ); } + /** + * Add topic/topics to WS subscription list + */ + public subscribe(wsTopics: WsTopic[] | WsTopic) { + const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + topics.forEach((topic) => + this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic) + ); + + // attempt to send subscription topic per websocket + this.wsStore.getKeys().forEach((wsKey: WsKey) => { + // if connected, send subscription request + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { + return this.requestSubscribeTopics(wsKey, topics); + } + + // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect + if ( + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.CONNECTING + ) && + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.RECONNECTING + ) + ) { + return this.connect(wsKey); + } + }); + } + + /** + * Remove topic/topics from WS subscription list + */ + public unsubscribe(wsTopics: WsTopic[] | WsTopic) { + const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + topics.forEach((topic) => + this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic) + ); + + this.wsStore.getKeys().forEach((wsKey: WsKey) => { + // unsubscribe request only necessary if active connection exists + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { + this.requestUnsubscribeTopics(wsKey, topics); + } + }); + } + // TODO: persistance for subbed topics. Look at ftx-api implementation. public subscribePublicSpotTrades(symbol: string, binary?: boolean) { if (!this.isSpot()) { diff --git a/test/inverse/private.ws.test.ts b/test/inverse/ws.private.test.ts similarity index 100% rename from test/inverse/private.ws.test.ts rename to test/inverse/ws.private.test.ts diff --git a/test/inverse/public.ws.test.ts b/test/inverse/ws.public.test.ts similarity index 96% rename from test/inverse/public.ws.test.ts rename to test/inverse/ws.public.test.ts index cb75af7..6d522fc 100644 --- a/test/inverse/public.ws.test.ts +++ b/test/inverse/ws.public.test.ts @@ -27,7 +27,7 @@ describe('Public Inverse Perps Websocket Client', () => { wsClient.closeAll(); }); - it('should open a private ws connection', async () => { + it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); expect(wsOpenPromise).resolves.toMatchObject({ diff --git a/test/linear/private.ws.test.ts b/test/linear/ws.private.test.ts similarity index 97% rename from test/linear/private.ws.test.ts rename to test/linear/ws.private.test.ts index fa197ff..e3b3fe2 100644 --- a/test/linear/private.ws.test.ts +++ b/test/linear/ws.private.test.ts @@ -9,7 +9,7 @@ import { WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; -describe('Private Linear Websocket Client', () => { +describe('Private Linear Perps Websocket Client', () => { const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/linear/public.ws.test.ts b/test/linear/ws.public.test.ts similarity index 94% rename from test/linear/public.ws.test.ts rename to test/linear/ws.public.test.ts index e506d5a..cf57156 100644 --- a/test/linear/public.ws.test.ts +++ b/test/linear/ws.public.test.ts @@ -1,5 +1,4 @@ import { - LinearClient, WebsocketClient, WSClientConfigurableOptions, WS_KEY_MAP, @@ -10,7 +9,7 @@ import { WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; -describe('Public Linear Websocket Client', () => { +describe('Public Linear Perps Websocket Client', () => { let wsClient: WebsocketClient; const wsClientOptions: WSClientConfigurableOptions = { @@ -26,7 +25,7 @@ describe('Public Linear Websocket Client', () => { wsClient.closeAll(); }); - it('should open a private ws connection', async () => { + it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); expect(wsOpenPromise).resolves.toMatchObject({ diff --git a/test/spot/ws.private.v1.test.ts b/test/spot/ws.private.v1.test.ts new file mode 100644 index 0000000..8242c61 --- /dev/null +++ b/test/spot/ws.private.v1.test.ts @@ -0,0 +1,58 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + promiseSleep, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Private Spot V1 Websocket Client', () => { + let wsClient: WebsocketClient; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spot', + key: API_KEY, + secret: API_SECRET, + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + // TODO: how to detect if auth failed for the v1 spot ws + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + wsClient.connectPrivate(); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotPrivate, + }); + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: 'wsTopic', + // data: expect.any(Array), + // }); + await Promise.all([wsOpenPromise]); + // await Promise.all([wsUpdatePromise]); + // await promiseSleep(4000); + }); +}); diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts new file mode 100644 index 0000000..8a9f94a --- /dev/null +++ b/test/spot/ws.public.v1.test.ts @@ -0,0 +1,64 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Spot V1 Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spot', + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPublic(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotPublic, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public orderbook events', async () => { + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const symbol = 'BTCUSDT'; + expect(wsUpdatePromise).resolves.toMatchObject({ + symbol: symbol, + symbolName: symbol, + topic: 'diffDepth', + params: { + realtimeInterval: '24h', + binary: 'false', + }, + data: expect.any(Array), + }); + + wsClient.subscribePublicSpotOrderbook(symbol, 'delta'); + + try { + await wsUpdatePromise; + } catch (e) { + console.error(`Wait for spot v1 orderbook event exception: `, e); + } + }); +}); diff --git a/test/spot/ws.public.v3.test.ts b/test/spot/ws.public.v3.test.ts new file mode 100644 index 0000000..6ae44e9 --- /dev/null +++ b/test/spot/ws.public.v3.test.ts @@ -0,0 +1,72 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Spot V3 Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spotV3', + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPublic(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotV3Public, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public orderbook events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'orderbook.40.BTCUSDT'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + expect(wsUpdatePromise).resolves.toStrictEqual(''); + + wsClient.subscribe(wsTopic); + + try { + await wsResponsePromise; + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + } + + try { + await wsUpdatePromise; + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + } + }); +}); From 7902430a17f8e9ea89a2bf25ef600a1d34d3052f Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:16:44 +0100 Subject: [PATCH 33/74] spot v3 ws support with tests --- src/types/websockets.ts | 3 +- src/util/websocket-util.ts | 9 ++- src/websocket-client.ts | 94 +++++++++++++++++++----- test/spot/ws.private.v3.test.ts | 124 ++++++++++++++++++++++++++++++++ test/spot/ws.public.v1.test.ts | 3 +- test/spot/ws.public.v3.test.ts | 27 +++++-- test/ws.util.ts | 36 +++++++--- 7 files changed, 258 insertions(+), 38 deletions(-) create mode 100644 test/spot/ws.private.v3.test.ts diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 180f518..b85ca02 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,6 +1,7 @@ import { RestClientOptions, WS_KEY_MAP } from '../util'; -export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotV3'; //| 'v3'; +/** For spot markets, spotV3 is recommended */ +export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3'; //| 'v3'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 72f6d53..f88d4c2 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -47,7 +47,7 @@ export const WS_BASE_URL_MAP: Record< testnet: 'wss://stream-testnet.bybit.com/spot/ws', }, }, - spotV3: { + spotv3: { public: { livenet: 'wss://stream.bybit.com/spot/public/v3', testnet: 'wss://stream-testnet.bybit.com/spot/public/v3', @@ -69,6 +69,8 @@ export const WS_KEY_MAP = { spotV3Public: 'spotV3Public', } as const; +export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; + export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.linearPublic, WS_KEY_MAP.spotPublic, @@ -115,3 +117,8 @@ export function getSpotWsKeyForTopic( } return WS_KEY_MAP.spotPublic; } + +export const WS_ERROR_ENUM = { + NOT_AUTHENTICATED_SPOT_V3: '-1004', + BAD_API_KEY_SPOT_V3: '10003', +}; diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 3f5c92e..349d617 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -26,6 +26,7 @@ import { getSpotWsKeyForTopic, WsConnectionStateEnum, PUBLIC_WS_KEYS, + WS_AUTH_ON_CONNECT_KEYS, WS_KEY_MAP, DefaultLogger, WS_BASE_URL_MAP, @@ -136,7 +137,7 @@ export class WebsocketClient extends EventEmitter { this.connectPublic(); break; } - case 'spotV3': { + case 'spotv3': { this.restClient = new SpotClientV3( undefined, undefined, @@ -221,7 +222,7 @@ export class WebsocketClient extends EventEmitter { this.connect(WS_KEY_MAP.spotPrivate), ]; } - case 'spotV3': { + case 'spotv3': { return [ this.connect(WS_KEY_MAP.spotV3Public), this.connect(WS_KEY_MAP.spotV3Private), @@ -244,7 +245,7 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return this.connect(WS_KEY_MAP.spotPublic); } - case 'spotV3': { + case 'spotv3': { return this.connect(WS_KEY_MAP.spotV3Public); } default: { @@ -267,7 +268,7 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return this.connect(WS_KEY_MAP.spotPrivate); } - case 'spotV3': { + case 'spotv3': { return this.connect(WS_KEY_MAP.spotV3Private); } default: { @@ -354,12 +355,49 @@ export class WebsocketClient extends EventEmitter { return ''; } + try { + const { signature, expiresAt } = await this.getWsAuthSignature(wsKey); + + const authParams = { + api_key: this.options.key, + expires: expiresAt, + signature, + }; + + return '?' + serializeParams(authParams); + } catch (e) { + this.logger.error(e, { ...loggerCategory, wsKey }); + return ''; + } + } + + private async sendAuthRequest(wsKey: WsKey): Promise { + try { + const { signature, expiresAt } = await this.getWsAuthSignature(wsKey); + + const request = { + op: 'auth', + args: [this.options.key, expiresAt, signature], + req_id: `${wsKey}-auth`, + }; + + return this.tryWsSend(wsKey, JSON.stringify(request)); + } catch (e) { + this.logger.error(e, { ...loggerCategory, wsKey }); + } + } + + private async getWsAuthSignature( + wsKey: WsKey + ): Promise<{ expiresAt: number; signature: string }> { + const { key, secret } = this.options; + if (!key || !secret) { this.logger.warning( 'Cannot authenticate websocket, either api or private keys missing.', { ...loggerCategory, wsKey } ); - return ''; + throw new Error(`Cannot auth - missing api or secret in config`); } this.logger.debug("Getting auth'd request params", { @@ -378,13 +416,10 @@ export class WebsocketClient extends EventEmitter { secret ); - const authParams = { - api_key: this.options.key, - expires: signatureExpiresAt, + return { + expiresAt: signatureExpiresAt, signature, }; - - return '?' + serializeParams(authParams); } private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) { @@ -451,6 +486,7 @@ export class WebsocketClient extends EventEmitter { return; } const wsMessage = JSON.stringify({ + req_id: topics.join(','), op: 'subscribe', args: topics, }); @@ -518,7 +554,7 @@ export class WebsocketClient extends EventEmitter { return ws; } - private onWsOpen(event, wsKey: WsKey) { + private async onWsOpen(event, wsKey: WsKey) { if ( this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING) ) { @@ -538,8 +574,14 @@ export class WebsocketClient extends EventEmitter { this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED); - // TODO: persistence not working yet for spot topics - if (wsKey !== 'spotPublic' && wsKey !== 'spotPrivate') { + // Some websockets require an auth packet to be sent after opening the connection + if (WS_AUTH_ON_CONNECT_KEYS.includes(wsKey)) { + this.logger.info(`Sending auth request...`); + await this.sendAuthRequest(wsKey); + } + + // TODO: persistence not working yet for spot v1 topics + if (wsKey !== WS_KEY_MAP.spotPublic && wsKey !== WS_KEY_MAP.spotPrivate) { this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]); } @@ -554,6 +596,8 @@ export class WebsocketClient extends EventEmitter { // any message can clear the pong timer - wouldn't get a message if the ws dropped this.clearPongTimer(wsKey); + // this.logger.silly('Received event', { ...this.logger, wsKey, event }); + const msg = JSON.parse((event && event.data) || event); if (msg['success'] || msg?.pong) { if (isWsPong(msg)) { @@ -564,11 +608,20 @@ export class WebsocketClient extends EventEmitter { return; } - if (msg.topic) { + if (msg?.topic) { return this.emit('update', msg); } - this.logger.warning('Got unhandled ws message', { + if ( + // spot v1 + msg?.code || + // spot v3 + msg?.type === 'error' + ) { + return this.emit('error', msg); + } + + this.logger.warning('Unhandled/unrecognised ws event message', { ...loggerCategory, message: msg, event, @@ -639,10 +692,10 @@ export class WebsocketClient extends EventEmitter { return WS_BASE_URL_MAP.spot.private[networkKey]; } case WS_KEY_MAP.spotV3Public: { - return WS_BASE_URL_MAP.spot.public[networkKey]; + return WS_BASE_URL_MAP.spotv3.public[networkKey]; } case WS_KEY_MAP.spotV3Private: { - return WS_BASE_URL_MAP.spot.private[networkKey]; + return WS_BASE_URL_MAP.spotv3.private[networkKey]; } case WS_KEY_MAP.inverse: { // private and public are on the same WS connection @@ -669,7 +722,7 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return getSpotWsKeyForTopic(topic, 'v1'); } - case 'spotV3': { + case 'spotv3': { return getSpotWsKeyForTopic(topic, 'v3'); } default: { @@ -740,7 +793,7 @@ export class WebsocketClient extends EventEmitter { }); } - // TODO: persistance for subbed topics. Look at ftx-api implementation. + /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotTrades(symbol: string, binary?: boolean) { if (!this.isSpot()) { throw this.wrongMarketError('spot'); @@ -759,6 +812,7 @@ export class WebsocketClient extends EventEmitter { ); } + /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) { if (!this.isSpot()) { throw this.wrongMarketError('spot'); @@ -777,6 +831,7 @@ export class WebsocketClient extends EventEmitter { ); } + /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotV1Kline( symbol: string, candleSize: KlineInterval, @@ -803,6 +858,7 @@ export class WebsocketClient extends EventEmitter { //ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}'); //ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}'); + /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotOrderbook( symbol: string, depth: 'full' | 'merge' | 'delta', diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts new file mode 100644 index 0000000..f867f1e --- /dev/null +++ b/test/spot/ws.private.v3.test.ts @@ -0,0 +1,124 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_ERROR_ENUM, + WS_KEY_MAP, +} from '../../src'; +import { + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Private Spot V3 Websocket Client', () => { + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spotv3', + key: API_KEY, + secret: API_SECRET, + }; + const wsTopic = `outboundAccountInfo`; + + describe('with invalid credentials', () => { + it('should reject private subscribe if keys/signature are incorrect', async () => { + const badClient = new WebsocketClient( + { + ...wsClientOptions, + key: 'bad', + secret: 'bad', + }, + silentLogger + ); + + // const wsOpenPromise = waitForSocketEvent(badClient, 'open'); + const wsResponsePromise = waitForSocketEvent(badClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + badClient.connectPrivate(); + badClient.subscribe(wsTopic); + + expect(wsResponsePromise).rejects.toMatchObject({ + ret_code: WS_ERROR_ENUM.BAD_API_KEY_SPOT_V3, + ret_msg: expect.any(String), + type: 'error', + }); + + try { + await Promise.all([wsResponsePromise]); + } catch (e) { + // console.error() + } + badClient.closeAll(); + }); + }); + + describe('with valid API credentails', () => { + let wsClient: WebsocketClient; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPrivate(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotV3Private, + }); + + try { + await Promise.all([wsOpenPromise]); + } catch (e) { + expect(e).toBeFalsy(); + } + + try { + expect(await wsResponsePromise).toMatchObject({ + op: 'auth', + success: true, + req_id: `${WS_KEY_MAP.spotV3Private}-auth`, + }); + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + expect(e).toBeFalsy(); + } + }); + + it('should subscribe to private outboundAccountInfo events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + + // expect(wsUpdatePromise).resolves.toStrictEqual(''); + wsClient.subscribe(wsTopic); + + try { + expect(await wsResponsePromise).toMatchObject({ + op: 'subscribe', + success: true, + ret_msg: '', + req_id: wsTopic, + }); + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + expect(e).toBeFalsy(); + } + }); + }); +}); diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts index 8a9f94a..97d24bf 100644 --- a/test/spot/ws.public.v1.test.ts +++ b/test/spot/ws.public.v1.test.ts @@ -6,6 +6,7 @@ import { import { logAllEvents, silentLogger, + fullLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -20,7 +21,7 @@ describe('Public Spot V1 Websocket Client', () => { beforeAll(() => { wsClient = new WebsocketClient(wsClientOptions, silentLogger); wsClient.connectPublic(); - // logAllEvents(wsClient); + logAllEvents(wsClient); }); afterAll(() => { diff --git a/test/spot/ws.public.v3.test.ts b/test/spot/ws.public.v3.test.ts index 6ae44e9..8fe07b0 100644 --- a/test/spot/ws.public.v3.test.ts +++ b/test/spot/ws.public.v3.test.ts @@ -6,6 +6,7 @@ import { import { logAllEvents, silentLogger, + fullLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -14,7 +15,7 @@ describe('Public Spot V3 Websocket Client', () => { let wsClient: WebsocketClient; const wsClientOptions: WSClientConfigurableOptions = { - market: 'spotV3', + market: 'spotv3', }; beforeAll(() => { @@ -42,15 +43,27 @@ describe('Public Spot V3 Websocket Client', () => { const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); - const wsTopic = 'orderbook.40.BTCUSDT'; + const symbol = 'BTCUSDT'; + const wsTopic = `orderbook.40.${symbol}`; + expect(wsResponsePromise).resolves.toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, + op: 'subscribe', success: true, + ret_msg: 'subscribe', + req_id: wsTopic, + }); + + expect(wsUpdatePromise).resolves.toMatchObject({ + data: { + a: expect.any(Array), + b: expect.any(Array), + s: symbol, + t: expect.any(Number), + }, + topic: wsTopic, + ts: expect.any(Number), + type: 'delta', }); - expect(wsUpdatePromise).resolves.toStrictEqual(''); wsClient.subscribe(wsTopic); diff --git a/test/ws.util.ts b/test/ws.util.ts index 4b1ff75..5cb4bc5 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -5,8 +5,17 @@ export const silentLogger = { debug: () => {}, notice: () => {}, info: () => {}, - warning: () => {}, - error: () => {}, + warning: (...params) => console.warn('warning', ...params), + error: (...params) => console.error('error', ...params), +}; + +export const fullLogger = { + silly: (...params) => console.log('silly', ...params), + debug: (...params) => console.log('debug', ...params), + notice: (...params) => console.log('notice', ...params), + info: (...params) => console.info('info', ...params), + warning: (...params) => console.warn('warning', ...params), + error: (...params) => console.error('error', ...params), }; export const WS_OPEN_EVENT_PARTIAL = { @@ -26,20 +35,29 @@ export function waitForSocketEvent( ); }, timeoutMs); + function cleanup() { + clearTimeout(timeout); + resolvedOnce = true; + wsClient.removeListener(event, (e) => resolver(e)); + wsClient.removeListener('error', (e) => rejector(e)); + } + let resolvedOnce = false; - wsClient.on(event, (event) => { - clearTimeout(timeout); + function resolver(event) { resolve(event); - resolvedOnce = true; - }); + cleanup(); + } - wsClient.on('error', (event) => { - clearTimeout(timeout); + function rejector(event) { if (!resolvedOnce) { reject(event); } - }); + cleanup(); + } + + wsClient.on(event, (e) => resolver(e)); + wsClient.on('error', (e) => rejector(e)); // if (event !== 'close') { // wsClient.on('close', (event) => { From 8813a968a6aac94b25b53966e55c44f0e43db71b Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:33:49 +0100 Subject: [PATCH 34/74] add market map --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 52a5852..32401fa 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,19 @@ client.getOrderBook({ symbol: 'BTCUSD' }) ## WebSockets All API groups can be used via a shared `WebsocketClient`. However, make sure to make one instance of the WebsocketClient per API group (spot vs inverse vs linear vs linearfutures etc): +The WebsocketClient can be configured to a specific API group using the market parameter. These are the currently available API groups: +| API Category | Market | Description | +|:----------------------------: |:-------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Unified Margin | TBC | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. | +| Futures v2 - Inverse Perps | `market: 'inverse'` | The [inverse v2 perps](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-websocket) category. | +| Futures v2 - USDT Perps | `market: 'linear'` | The [USDT/linear v2 perps](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-websocket) category. | +| Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | +| Spot v3 | `market: 'spotv3'` | The [spot v3](https://bybit-exchange.github.io/docs/spot/v3/#t-websocket) category. | +| Spot v1 | `market: 'spot'` | The older [spot v1](https://bybit-exchange.github.io/docs/spot/v1/#t-websocket) category. Use the `spotv3` market if possible, as the v1 category does not have automatic re-subscribe if reconnected. | +| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to private topics. | +| USDC Perps | TBC | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | +| USDC Options | TBC | The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | + ```javascript const { WebsocketClient } = require('bybit-api'); From 921152fa3b1fd87b12de9fed14d79112eca8216a Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:36:32 +0100 Subject: [PATCH 35/74] readme tweaks --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 32401fa..07e213b 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Node.js connector for the Bybit APIs and WebSockets: - Complete integration with all bybit APIs. - TypeScript support (with type declarations for most API requests & responses). -- Over 200 integration tests making real API calls, validating any changes before they reach npm. -- Robust WebSocket integration with connection heartbeats & automatic reconnection. +- Over 300 integration tests making real API calls & WebSocket connections, validating any changes before they reach npm. +- Robust WebSocket integration with configurable connection heartbeats & automatic reconnect then resubscribe workflows. - Browser support (via webpack bundle - see "Browser Usage" below). ## Installation @@ -45,7 +45,7 @@ This connector is fully compatible with both TypeScript and pure JavaScript proj - [src](./src) - the whole connector written in TypeScript - [lib](./lib) - the JavaScript version of the project (built from TypeScript). This should not be edited directly, as it will be overwritten with each release. -- [dist](./dist) - the packed bundle of the project for use in browser environments. +- [dist](./dist) - the web-packed bundle of the project for use in browser environments. - [examples](./examples) - some implementation examples & demonstrations. Contributions are welcome! --- From 2ada7eb664347ce5f650a0dbfc32fa4d5d3e32be Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:43:57 +0100 Subject: [PATCH 36/74] ws test stability fixes --- src/websocket-client.ts | 4 ++-- test/inverse/ws.private.test.ts | 5 ++--- test/linear/ws.private.test.ts | 6 ++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 349d617..ce4ed1c 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -321,6 +321,7 @@ export class WebsocketClient extends EventEmitter { private parseWsError(context: string, error: any, wsKey: WsKey) { if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); + this.emit('error', error); return; } @@ -339,14 +340,13 @@ export class WebsocketClient extends EventEmitter { ); break; } + this.emit('error', error); } /** * Return params required to make authorized request */ private async getAuthParams(wsKey: WsKey): Promise { - const { key, secret } = this.options; - if (PUBLIC_WS_KEYS.includes(wsKey)) { this.logger.debug('Starting public only websocket client.', { ...loggerCategory, diff --git a/test/inverse/ws.private.test.ts b/test/inverse/ws.private.test.ts index 7a4c325..1c8fb2d 100644 --- a/test/inverse/ws.private.test.ts +++ b/test/inverse/ws.private.test.ts @@ -34,12 +34,11 @@ describe('Private Inverse Perps Websocket Client', () => { badClient.connectPrivate(); - expect(wsOpenPromise).rejects.toMatch('Failed to receive'); - try { - await Promise.all([wsOpenPromise]); + expect(await wsOpenPromise).toMatch('Failed to receive'); } catch (e) { // console.error() + expect(e?.message).toStrictEqual('Unexpected server response: 401'); } badClient.closeAll(); }); diff --git a/test/linear/ws.private.test.ts b/test/linear/ws.private.test.ts index e3b3fe2..8e8cecf 100644 --- a/test/linear/ws.private.test.ts +++ b/test/linear/ws.private.test.ts @@ -34,12 +34,10 @@ describe('Private Linear Perps Websocket Client', () => { badClient.connectPrivate(); - expect(wsOpenPromise).rejects.toMatch('Failed to receive'); - try { - await Promise.all([wsOpenPromise]); + expect(await wsOpenPromise).toMatch('Failed to receive'); } catch (e) { - // console.error() + expect(e?.message).toStrictEqual('Unexpected server response: 401'); } badClient.closeAll(); }); From bd09cf81096d7f8cc3d5d93f3d829f9fb586df74 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:47:16 +0100 Subject: [PATCH 37/74] mute excessive logs --- test/spot/ws.public.v1.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts index 97d24bf..a17a32c 100644 --- a/test/spot/ws.public.v1.test.ts +++ b/test/spot/ws.public.v1.test.ts @@ -21,7 +21,7 @@ describe('Public Spot V1 Websocket Client', () => { beforeAll(() => { wsClient = new WebsocketClient(wsClientOptions, silentLogger); wsClient.connectPublic(); - logAllEvents(wsClient); + // logAllEvents(wsClient); }); afterAll(() => { From a9dd6face97912f51f39d13f0c5aeda8ac96dc35 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:51:18 +0100 Subject: [PATCH 38/74] slight logger improvement for test --- test/inverse/ws.private.test.ts | 9 ++++++--- test/linear/ws.private.test.ts | 9 ++++++--- test/spot/ws.private.v1.test.ts | 7 +++++-- test/spot/ws.private.v3.test.ts | 9 ++++++--- test/spot/ws.public.v1.test.ts | 7 +++++-- test/spot/ws.public.v3.test.ts | 7 +++++-- test/ws.util.ts | 18 ++++++++++-------- 7 files changed, 43 insertions(+), 23 deletions(-) diff --git a/test/inverse/ws.private.test.ts b/test/inverse/ws.private.test.ts index 1c8fb2d..c95a3b5 100644 --- a/test/inverse/ws.private.test.ts +++ b/test/inverse/ws.private.test.ts @@ -4,7 +4,7 @@ import { WS_KEY_MAP, } from '../../src'; import { - silentLogger, + getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -27,7 +27,7 @@ describe('Private Inverse Perps Websocket Client', () => { key: 'bad', secret: 'bad', }, - silentLogger + getSilentLogger('expect401') ); const wsOpenPromise = waitForSocketEvent(badClient, 'open', 2500); @@ -53,7 +53,10 @@ describe('Private Inverse Perps Websocket Client', () => { }); beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); wsClient.connectPrivate(); }); diff --git a/test/linear/ws.private.test.ts b/test/linear/ws.private.test.ts index 8e8cecf..1f9336f 100644 --- a/test/linear/ws.private.test.ts +++ b/test/linear/ws.private.test.ts @@ -4,7 +4,7 @@ import { WS_KEY_MAP, } from '../../src'; import { - silentLogger, + getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -27,7 +27,7 @@ describe('Private Linear Perps Websocket Client', () => { key: 'bad', secret: 'bad', }, - silentLogger + getSilentLogger('expect401') ); const wsOpenPromise = waitForSocketEvent(badClient, 'open', 2500); @@ -58,7 +58,10 @@ describe('Private Linear Perps Websocket Client', () => { }; beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); wsClient.connectPrivate(); }); diff --git a/test/spot/ws.private.v1.test.ts b/test/spot/ws.private.v1.test.ts index 8242c61..3cb62f3 100644 --- a/test/spot/ws.private.v1.test.ts +++ b/test/spot/ws.private.v1.test.ts @@ -6,7 +6,7 @@ import { import { logAllEvents, promiseSleep, - silentLogger, + getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -28,7 +28,10 @@ describe('Private Spot V1 Websocket Client', () => { }; beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); logAllEvents(wsClient); }); diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts index f867f1e..b430b29 100644 --- a/test/spot/ws.private.v3.test.ts +++ b/test/spot/ws.private.v3.test.ts @@ -5,7 +5,7 @@ import { WS_KEY_MAP, } from '../../src'; import { - silentLogger, + getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -29,7 +29,7 @@ describe('Private Spot V3 Websocket Client', () => { key: 'bad', secret: 'bad', }, - silentLogger + getSilentLogger('expect401') ); // const wsOpenPromise = waitForSocketEvent(badClient, 'open'); @@ -63,7 +63,10 @@ describe('Private Spot V3 Websocket Client', () => { }); beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); wsClient.connectPrivate(); // logAllEvents(wsClient); }); diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts index a17a32c..cdb48d4 100644 --- a/test/spot/ws.public.v1.test.ts +++ b/test/spot/ws.public.v1.test.ts @@ -5,7 +5,7 @@ import { } from '../../src'; import { logAllEvents, - silentLogger, + getSilentLogger, fullLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, @@ -19,7 +19,10 @@ describe('Public Spot V1 Websocket Client', () => { }; beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); wsClient.connectPublic(); // logAllEvents(wsClient); }); diff --git a/test/spot/ws.public.v3.test.ts b/test/spot/ws.public.v3.test.ts index 8fe07b0..0cd86f1 100644 --- a/test/spot/ws.public.v3.test.ts +++ b/test/spot/ws.public.v3.test.ts @@ -5,7 +5,7 @@ import { } from '../../src'; import { logAllEvents, - silentLogger, + getSilentLogger, fullLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, @@ -19,7 +19,10 @@ describe('Public Spot V3 Websocket Client', () => { }; beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); wsClient.connectPublic(); // logAllEvents(wsClient); }); diff --git a/test/ws.util.ts b/test/ws.util.ts index 5cb4bc5..952e9fc 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -1,13 +1,15 @@ import { WebsocketClient, WsClientEvent } from '../src'; -export const silentLogger = { - silly: () => {}, - debug: () => {}, - notice: () => {}, - info: () => {}, - warning: (...params) => console.warn('warning', ...params), - error: (...params) => console.error('error', ...params), -}; +export function getSilentLogger(logHint?: string) { + return { + silly: () => {}, + debug: () => {}, + notice: () => {}, + info: () => {}, + warning: (...params) => console.warn('warning', logHint, ...params), + error: (...params) => console.error('error', logHint, ...params), + }; +} export const fullLogger = { silly: (...params) => console.log('silly', ...params), From 28485c00683fff4d406836f56a11d86e6853056d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 00:56:00 +0100 Subject: [PATCH 39/74] fix tests --- test/inverse/ws.public.test.ts | 4 ++-- test/linear/ws.public.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/inverse/ws.public.test.ts b/test/inverse/ws.public.test.ts index 6d522fc..709421c 100644 --- a/test/inverse/ws.public.test.ts +++ b/test/inverse/ws.public.test.ts @@ -6,7 +6,7 @@ import { } from '../../src'; import { promiseSleep, - silentLogger, + getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -19,7 +19,7 @@ describe('Public Inverse Perps Websocket Client', () => { }; beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient(wsClientOptions, getSilentLogger('public')); wsClient.connectPublic(); }); diff --git a/test/linear/ws.public.test.ts b/test/linear/ws.public.test.ts index cf57156..ee1dd27 100644 --- a/test/linear/ws.public.test.ts +++ b/test/linear/ws.public.test.ts @@ -4,7 +4,7 @@ import { WS_KEY_MAP, } from '../../src'; import { - silentLogger, + getSilentLogger, waitForSocketEvent, WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; @@ -17,7 +17,7 @@ describe('Public Linear Perps Websocket Client', () => { }; beforeAll(() => { - wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient = new WebsocketClient(wsClientOptions, getSilentLogger('public')); wsClient.connectPublic(); }); From d2ba5d3e0174e8a3829fe97bd0e6e2d542d3afc5 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 13:13:49 +0100 Subject: [PATCH 40/74] usdc options public ws test --- README.md | 2 +- examples/ws-public.ts | 16 ++- src/types/shared.ts | 4 +- src/types/websockets.ts | 2 +- src/util/requestUtils.ts | 20 ++-- src/util/websocket-util.ts | 136 +++++++++++++++++++------- src/websocket-client.ts | 145 +++++++++++++++------------- test/usdc/options/ws.public.test.ts | 79 +++++++++++++++ test/ws.util.ts | 30 ++++++ 9 files changed, 319 insertions(+), 115 deletions(-) create mode 100644 test/usdc/options/ws.public.test.ts diff --git a/README.md b/README.md index 07e213b..8641a9b 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The WebsocketClient can be configured to a specific API group using the market p | Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | | Spot v3 | `market: 'spotv3'` | The [spot v3](https://bybit-exchange.github.io/docs/spot/v3/#t-websocket) category. | | Spot v1 | `market: 'spot'` | The older [spot v1](https://bybit-exchange.github.io/docs/spot/v1/#t-websocket) category. Use the `spotv3` market if possible, as the v1 category does not have automatic re-subscribe if reconnected. | -| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to private topics. | +| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. | | USDC Perps | TBC | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | | USDC Options | TBC | The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | diff --git a/examples/ws-public.ts b/examples/ws-public.ts index 2aa1cd7..1313cd9 100644 --- a/examples/ws-public.ts +++ b/examples/ws-public.ts @@ -13,9 +13,10 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; { // key: key, // secret: secret, - market: 'linear', + // market: 'linear', // market: 'inverse', // market: 'spot', + market: 'usdcOption', }, logger ); @@ -51,10 +52,15 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; // Linear wsClient.subscribe('trade.BTCUSDT'); - setTimeout(() => { - console.log('unsubscribing'); - wsClient.unsubscribe('trade.BTCUSDT'); - }, 5 * 1000); + // usdc options + wsClient.subscribe(`recenttrades.BTC`); + wsClient.subscribe(`recenttrades.ETH`); + wsClient.subscribe(`recenttrades.SOL`); + + // setTimeout(() => { + // console.log('unsubscribing'); + // wsClient.unsubscribe('trade.BTCUSDT'); + // }, 5 * 1000); // For spot, request public connection first then send required topics on 'open' // wsClient.connectPublic(); diff --git a/src/types/shared.ts b/src/types/shared.ts index 2f8dd65..77bd21f 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -2,12 +2,14 @@ import { InverseClient } from '../inverse-client'; import { LinearClient } from '../linear-client'; import { SpotClient } from '../spot-client'; import { SpotClientV3 } from '../spot-client-v3'; +import { USDCOptionClient } from '../usdc-option-client'; export type RESTClient = | InverseClient | LinearClient | SpotClient - | SpotClientV3; + | SpotClientV3 + | USDCOptionClient; export type numberInString = string; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index b85ca02..67b86d4 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,7 +1,7 @@ import { RestClientOptions, WS_KEY_MAP } from '../util'; /** For spot markets, spotV3 is recommended */ -export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3'; //| 'v3'; +export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3' | 'usdcOption'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index d3d6eb7..a17f752 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -61,15 +61,23 @@ export function getRestBaseUrl( return exchangeBaseUrls.testnet; } -export function isWsPong(response: any) { - if (response.pong || response.ping) { +export function isWsPong(msg: any): boolean { + if (!msg) { + return false; + } + if (msg.pong || msg.ping) { return true; } + + if (msg['op'] === 'pong') { + return true; + } + return ( - response.request && - response.request.op === 'ping' && - response.ret_msg === 'pong' && - response.success === true + msg.request && + msg.request.op === 'ping' && + msg.ret_msg === 'pong' && + msg.success === true ); } diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index f88d4c2..7efc262 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -57,6 +57,18 @@ export const WS_BASE_URL_MAP: Record< testnet: 'wss://stream-testnet.bybit.com/spot/private/v3', }, }, + usdcOption: { + public: { + livenet: 'wss://stream.bybit.com/trade/option/usdc/public/v1', + livenet2: 'wss://stream.bytick.com/trade/option/usdc/public/v1', + testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/public/v1', + }, + private: { + livenet: 'wss://stream.bybit.com/trade/option/usdc/private/v1', + livenet2: 'wss://stream.bytick.com/trade/option/usdc/private/v1', + testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/private/v1', + }, + }, }; export const WS_KEY_MAP = { @@ -67,6 +79,10 @@ export const WS_KEY_MAP = { spotPublic: 'spotPublic', spotV3Private: 'spotV3Private', spotV3Public: 'spotV3Public', + usdcOptionPrivate: 'usdcOptionPrivate', + usdcOptionPublic: 'usdcOptionPublic', + // usdcPerpPrivate: 'usdcPerpPrivate', + // usdcPerpPublic: 'usdcPerpPublic', } as const; export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; @@ -74,51 +90,103 @@ export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.linearPublic, WS_KEY_MAP.spotPublic, + WS_KEY_MAP.spotV3Public, + WS_KEY_MAP.usdcOptionPublic, ] as string[]; -export function getLinearWsKeyForTopic(topic: string): WsKey { - const privateTopics = [ - 'position', - 'execution', - 'order', - 'stop_order', - 'wallet', - ]; - if (privateTopics.includes(topic)) { - return WS_KEY_MAP.linearPrivate; - } +/** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */ +const PRIVATE_TOPICS = [ + 'position', + 'execution', + 'order', + 'stop_order', + 'wallet', + 'outboundAccountInfo', + 'executionReport', + 'ticketInfo', + // copy trading apis + 'copyTradePosition', + 'copyTradeOrder', + 'copyTradeExecution', + 'copyTradeWallet', + // usdc options + 'user.openapi.option.position', + 'user.openapi.option.trade', + 'user.order', + 'user.openapi.option.order', + 'user.service', + 'user.openapi.greeks', + 'user.mmp.event', + // usdc perps + 'user.openapi.perp.position', + 'user.openapi.perp.trade', + 'user.openapi.perp.order', + 'user.service', + // unified margin + 'user.position.unifiedAccount', + 'user.execution.unifiedAccount', + 'user.order.unifiedAccount', + 'user.wallet.unifiedAccount', + 'user.greeks.unifiedAccount', +]; - return WS_KEY_MAP.linearPublic; +export function getWsKeyForTopic( + market: APIMarket, + topic: string, + isPrivate?: boolean +): WsKey { + const isPrivateTopic = isPrivate === true || PRIVATE_TOPICS.includes(topic); + switch (market) { + case 'inverse': { + return WS_KEY_MAP.inverse; + } + case 'linear': { + return isPrivateTopic + ? WS_KEY_MAP.linearPrivate + : WS_KEY_MAP.linearPublic; + } + case 'spot': { + return isPrivateTopic ? WS_KEY_MAP.spotPrivate : WS_KEY_MAP.spotPublic; + } + case 'spotv3': { + return isPrivateTopic + ? WS_KEY_MAP.spotV3Private + : WS_KEY_MAP.spotV3Public; + } + case 'usdcOption': { + return isPrivateTopic + ? WS_KEY_MAP.usdcOptionPrivate + : WS_KEY_MAP.usdcOptionPublic; + } + default: { + throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`); + } + } } -export function getSpotWsKeyForTopic( +export function getUsdcWsKeyForTopic( topic: string, - apiVersion: 'v1' | 'v3' + subGroup: 'option' | 'perp' ): WsKey { - const privateTopics = [ - 'position', - 'execution', - 'order', - 'stop_order', - 'outboundAccountInfo', - 'executionReport', - 'ticketInfo', - ]; - - if (apiVersion === 'v3') { - if (privateTopics.includes(topic)) { - return WS_KEY_MAP.spotV3Private; - } - return WS_KEY_MAP.spotV3Public; + const isPrivateTopic = PRIVATE_TOPICS.includes(topic); + if (subGroup === 'option') { + return isPrivateTopic + ? WS_KEY_MAP.usdcOptionPrivate + : WS_KEY_MAP.usdcOptionPublic; } - - if (privateTopics.includes(topic)) { - return WS_KEY_MAP.spotPrivate; - } - return WS_KEY_MAP.spotPublic; + return isPrivateTopic + ? WS_KEY_MAP.usdcOptionPrivate + : WS_KEY_MAP.usdcOptionPublic; + // return isPrivateTopic + // ? WS_KEY_MAP.usdcPerpPrivate + // : WS_KEY_MAP.usdcPerpPublic; } export const WS_ERROR_ENUM = { NOT_AUTHENTICATED_SPOT_V3: '-1004', BAD_API_KEY_SPOT_V3: '10003', }; + +export function neverGuard(x: never, msg: string): Error { + return new Error(`Unhandled value exception "x", ${msg}`); +} diff --git a/src/websocket-client.ts b/src/websocket-client.ts index ce4ed1c..99610cf 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -22,19 +22,16 @@ import { import { serializeParams, isWsPong, - getLinearWsKeyForTopic, - getSpotWsKeyForTopic, WsConnectionStateEnum, PUBLIC_WS_KEYS, WS_AUTH_ON_CONNECT_KEYS, WS_KEY_MAP, DefaultLogger, WS_BASE_URL_MAP, + getWsKeyForTopic, + neverGuard, } from './util'; - -function neverGuard(x: never, msg: string): Error { - return new Error(`Unhandled value exception "x", ${msg}`); -} +import { USDCOptionClient } from './usdc-option-client'; const loggerCategory = { category: 'bybit-ws' }; @@ -94,9 +91,7 @@ export class WebsocketClient extends EventEmitter { ...options, }; - if (this.options.fetchTimeOffsetBeforeAuth) { - this.prepareRESTClient(); - } + this.prepareRESTClient(); } /** @@ -148,15 +143,28 @@ export class WebsocketClient extends EventEmitter { this.connectPublic(); break; } - // if (this.isV3()) { - // this.restClient = new SpotClientV3( - // undefined, - // undefined, - // this.isLivenet(), - // this.options.restOptions, - // this.options.requestOptions - // ); - // } + case 'spotv3': { + this.restClient = new SpotClientV3( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } + case 'usdcOption': { + this.restClient = new USDCOptionClient( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } default: { throw neverGuard( this.options.market, @@ -208,25 +216,15 @@ export class WebsocketClient extends EventEmitter { public connectAll(): Promise[] { switch (this.options.market) { case 'inverse': { - return [this.connect(WS_KEY_MAP.inverse)]; + // only one for inverse + return [this.connectPublic()]; } - case 'linear': { - return [ - this.connect(WS_KEY_MAP.linearPublic), - this.connect(WS_KEY_MAP.linearPrivate), - ]; - } - case 'spot': { - return [ - this.connect(WS_KEY_MAP.spotPublic), - this.connect(WS_KEY_MAP.spotPrivate), - ]; - } - case 'spotv3': { - return [ - this.connect(WS_KEY_MAP.spotV3Public), - this.connect(WS_KEY_MAP.spotV3Private), - ]; + // these all have separate public & private ws endpoints + case 'linear': + case 'spot': + case 'spotv3': + case 'usdcOption': { + return [this.connectPublic(), this.connectPrivate()]; } default: { throw neverGuard(this.options.market, `connectAll(): Unhandled market`); @@ -248,6 +246,9 @@ export class WebsocketClient extends EventEmitter { case 'spotv3': { return this.connect(WS_KEY_MAP.spotV3Public); } + case 'usdcOption': { + return this.connect(WS_KEY_MAP.usdcOptionPublic); + } default: { throw neverGuard( this.options.market, @@ -257,7 +258,7 @@ export class WebsocketClient extends EventEmitter { } } - public connectPrivate(): Promise | undefined { + public connectPrivate(): Promise { switch (this.options.market) { case 'inverse': { return this.connect(WS_KEY_MAP.inverse); @@ -271,6 +272,9 @@ export class WebsocketClient extends EventEmitter { case 'spotv3': { return this.connect(WS_KEY_MAP.spotV3Private); } + case 'usdcOption': { + return this.connect(WS_KEY_MAP.usdcOptionPrivate); + } default: { throw neverGuard( this.options.market, @@ -596,10 +600,15 @@ export class WebsocketClient extends EventEmitter { // any message can clear the pong timer - wouldn't get a message if the ws dropped this.clearPongTimer(wsKey); - // this.logger.silly('Received event', { ...this.logger, wsKey, event }); - const msg = JSON.parse((event && event.data) || event); - if (msg['success'] || msg?.pong) { + this.logger.silly('Received event', { + ...this.logger, + wsKey, + msg: JSON.stringify(msg, null, 2), + }); + + // TODO: cleanme + if (msg['success'] || msg?.pong || isWsPong(msg)) { if (isWsPong(msg)) { this.logger.silly('Received pong', { ...loggerCategory, wsKey }); } else { @@ -608,6 +617,9 @@ export class WebsocketClient extends EventEmitter { return; } + if (msg['finalFragment']) { + return this.emit('response', msg); + } if (msg?.topic) { return this.emit('update', msg); } @@ -701,6 +713,18 @@ export class WebsocketClient extends EventEmitter { // private and public are on the same WS connection return WS_BASE_URL_MAP.inverse.public[networkKey]; } + case WS_KEY_MAP.usdcOptionPublic: { + return WS_BASE_URL_MAP.usdcOption.public[networkKey]; + } + case WS_KEY_MAP.usdcOptionPrivate: { + return WS_BASE_URL_MAP.usdcOption.private[networkKey]; + } + // case WS_KEY_MAP.usdcPerpPublic: { + // return WS_BASE_URL_MAP.usdcOption.public[networkKey]; + // } + // case WS_KEY_MAP.usdcPerpPrivate: { + // return WS_BASE_URL_MAP.usdcOption.private[networkKey]; + // } default: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { ...loggerCategory, @@ -711,29 +735,6 @@ export class WebsocketClient extends EventEmitter { } } - private getWsKeyForTopic(topic: string): WsKey { - switch (this.options.market) { - case 'inverse': { - return WS_KEY_MAP.inverse; - } - case 'linear': { - return getLinearWsKeyForTopic(topic); - } - case 'spot': { - return getSpotWsKeyForTopic(topic, 'v1'); - } - case 'spotv3': { - return getSpotWsKeyForTopic(topic, 'v3'); - } - default: { - throw neverGuard( - this.options.market, - `connectPublic(): Unhandled market` - ); - } - } - } - private wrongMarketError(market: APIMarket) { return new Error( `This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}' to listen to spot topics` @@ -742,11 +743,16 @@ export class WebsocketClient extends EventEmitter { /** * Add topic/topics to WS subscription list + * @param wsTopics topic or list of topics + * @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) */ - public subscribe(wsTopics: WsTopic[] | WsTopic) { + public subscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; topics.forEach((topic) => - this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic) + this.wsStore.addTopic( + getWsKeyForTopic(this.options.market, topic, isPrivateTopic), + topic + ) ); // attempt to send subscription topic per websocket @@ -776,11 +782,16 @@ export class WebsocketClient extends EventEmitter { /** * Remove topic/topics from WS subscription list + * @param wsTopics topic or list of topics + * @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) */ - public unsubscribe(wsTopics: WsTopic[] | WsTopic) { + public unsubscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; topics.forEach((topic) => - this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic) + this.wsStore.deleteTopic( + getWsKeyForTopic(this.options.market, topic, isPrivateTopic), + topic + ) ); this.wsStore.getKeys().forEach((wsKey: WsKey) => { diff --git a/test/usdc/options/ws.public.test.ts b/test/usdc/options/ws.public.test.ts new file mode 100644 index 0000000..8aeb862 --- /dev/null +++ b/test/usdc/options/ws.public.test.ts @@ -0,0 +1,79 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../../src'; +import { + logAllEvents, + getSilentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../../ws.util'; + +describe('Public USDC Option Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'usdcOption', + }; + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccessNoAuth') + ); + // logAllEvents(wsClient); + }); + + beforeEach(() => { + wsClient.removeAllListeners(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcOptionPublic, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public trade events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + wsClient.subscribe([ + 'recenttrades.BTC', + 'recenttrades.ETH', + 'recenttrades.SOL', + ]); + + try { + expect(await wsResponsePromise).toMatchObject({ + success: true, + data: { + failTopics: [], + successTopics: expect.any(Array), + }, + type: 'COMMAND_RESP', + }); + } catch (e) { + // sub failed + expect(e).toBeFalsy(); + } + + // Takes a while to get an event from USDC options - testing this manually for now + // try { + // expect(await wsUpdatePromise).toStrictEqual('asdfasdf'); + // } catch (e) { + // // no data + // expect(e).toBeFalsy(); + // } + }); +}); diff --git a/test/ws.util.ts b/test/ws.util.ts index 952e9fc..855dee5 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -73,6 +73,36 @@ export function waitForSocketEvent( }); } +export function listenToSocketEvents(wsClient: WebsocketClient) { + const retVal: Record< + 'update' | 'open' | 'response' | 'close' | 'error', + typeof jest.fn + > = { + open: jest.fn(), + response: jest.fn(), + update: jest.fn(), + close: jest.fn(), + error: jest.fn(), + }; + + wsClient.on('open', retVal.open); + wsClient.on('response', retVal.response); + wsClient.on('update', retVal.update); + wsClient.on('close', retVal.close); + wsClient.on('error', retVal.error); + + return { + ...retVal, + cleanup: () => { + wsClient.removeListener('open', retVal.open); + wsClient.removeListener('response', retVal.response); + wsClient.removeListener('update', retVal.update); + wsClient.removeListener('close', retVal.close); + wsClient.removeListener('error', retVal.error); + }, + }; +} + export function logAllEvents(wsClient: WebsocketClient) { wsClient.on('update', (data) => { console.log('wsUpdate: ', JSON.stringify(data, null, 2)); From 9b673f08d5b331e778a7f36b44a15197a2e57a88 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 13:25:25 +0100 Subject: [PATCH 41/74] public usdc perp ws test --- src/types/shared.ts | 4 +- src/types/websockets.ts | 8 ++- src/util/websocket-util.ts | 22 ++++++- src/websocket-client.ts | 33 ++++++++--- test/usdc/options/ws.public.test.ts | 2 +- test/usdc/perpetual/ws.public.test.ts | 82 +++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 test/usdc/perpetual/ws.public.test.ts diff --git a/src/types/shared.ts b/src/types/shared.ts index 77bd21f..10aa83d 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -3,13 +3,15 @@ import { LinearClient } from '../linear-client'; import { SpotClient } from '../spot-client'; import { SpotClientV3 } from '../spot-client-v3'; import { USDCOptionClient } from '../usdc-option-client'; +import { USDCPerpetualClient } from '../usdc-perpetual-client'; export type RESTClient = | InverseClient | LinearClient | SpotClient | SpotClientV3 - | USDCOptionClient; + | USDCOptionClient + | USDCPerpetualClient; export type numberInString = string; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 67b86d4..a44a737 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,7 +1,13 @@ import { RestClientOptions, WS_KEY_MAP } from '../util'; /** For spot markets, spotV3 is recommended */ -export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3' | 'usdcOption'; +export type APIMarket = + | 'inverse' + | 'linear' + | 'spot' + | 'spotv3' + | 'usdcOption' + | 'usdcPerp'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 7efc262..7507529 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -69,6 +69,18 @@ export const WS_BASE_URL_MAP: Record< testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/private/v1', }, }, + usdcPerp: { + public: { + livenet: 'wss://stream.bybit.com/perpetual/ws/v1/realtime_public', + livenet2: 'wss://stream.bytick.com/perpetual/ws/v1/realtime_public', + testnet: 'wss://stream-testnet.bybit.com/perpetual/ws/v1/realtime_public', + }, + private: { + livenet: 'wss://stream.bybit.com/trade/option/usdc/private/v1', + livenet2: 'wss://stream.bytick.com/trade/option/usdc/private/v1', + testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/private/v1', + }, + }, }; export const WS_KEY_MAP = { @@ -81,8 +93,8 @@ export const WS_KEY_MAP = { spotV3Public: 'spotV3Public', usdcOptionPrivate: 'usdcOptionPrivate', usdcOptionPublic: 'usdcOptionPublic', - // usdcPerpPrivate: 'usdcPerpPrivate', - // usdcPerpPublic: 'usdcPerpPublic', + usdcPerpPrivate: 'usdcPerpPrivate', + usdcPerpPublic: 'usdcPerpPublic', } as const; export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; @@ -92,6 +104,7 @@ export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.spotPublic, WS_KEY_MAP.spotV3Public, WS_KEY_MAP.usdcOptionPublic, + WS_KEY_MAP.usdcPerpPublic, ] as string[]; /** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */ @@ -158,6 +171,11 @@ export function getWsKeyForTopic( ? WS_KEY_MAP.usdcOptionPrivate : WS_KEY_MAP.usdcOptionPublic; } + case 'usdcPerp': { + return isPrivateTopic + ? WS_KEY_MAP.usdcPerpPrivate + : WS_KEY_MAP.usdcPerpPublic; + } default: { throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`); } diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 99610cf..fe73c3e 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -32,6 +32,7 @@ import { neverGuard, } from './util'; import { USDCOptionClient } from './usdc-option-client'; +import { USDCPerpetualClient } from './usdc-perpetual-client'; const loggerCategory = { category: 'bybit-ws' }; @@ -165,6 +166,17 @@ export class WebsocketClient extends EventEmitter { this.connectPublic(); break; } + case 'usdcPerp': { + this.restClient = new USDCPerpetualClient( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } default: { throw neverGuard( this.options.market, @@ -223,7 +235,8 @@ export class WebsocketClient extends EventEmitter { case 'linear': case 'spot': case 'spotv3': - case 'usdcOption': { + case 'usdcOption': + case 'usdcPerp': { return [this.connectPublic(), this.connectPrivate()]; } default: { @@ -249,6 +262,9 @@ export class WebsocketClient extends EventEmitter { case 'usdcOption': { return this.connect(WS_KEY_MAP.usdcOptionPublic); } + case 'usdcPerp': { + return this.connect(WS_KEY_MAP.usdcPerpPublic); + } default: { throw neverGuard( this.options.market, @@ -275,6 +291,9 @@ export class WebsocketClient extends EventEmitter { case 'usdcOption': { return this.connect(WS_KEY_MAP.usdcOptionPrivate); } + case 'usdcPerp': { + return this.connect(WS_KEY_MAP.usdcPerpPrivate); + } default: { throw neverGuard( this.options.market, @@ -719,12 +738,12 @@ export class WebsocketClient extends EventEmitter { case WS_KEY_MAP.usdcOptionPrivate: { return WS_BASE_URL_MAP.usdcOption.private[networkKey]; } - // case WS_KEY_MAP.usdcPerpPublic: { - // return WS_BASE_URL_MAP.usdcOption.public[networkKey]; - // } - // case WS_KEY_MAP.usdcPerpPrivate: { - // return WS_BASE_URL_MAP.usdcOption.private[networkKey]; - // } + case WS_KEY_MAP.usdcPerpPublic: { + return WS_BASE_URL_MAP.usdcPerp.public[networkKey]; + } + case WS_KEY_MAP.usdcPerpPrivate: { + return WS_BASE_URL_MAP.usdcPerp.private[networkKey]; + } default: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { ...loggerCategory, diff --git a/test/usdc/options/ws.public.test.ts b/test/usdc/options/ws.public.test.ts index 8aeb862..0771099 100644 --- a/test/usdc/options/ws.public.test.ts +++ b/test/usdc/options/ws.public.test.ts @@ -22,11 +22,11 @@ describe('Public USDC Option Websocket Client', () => { wsClientOptions, getSilentLogger('expectSuccessNoAuth') ); - // logAllEvents(wsClient); }); beforeEach(() => { wsClient.removeAllListeners(); + // logAllEvents(wsClient); }); afterAll(() => { diff --git a/test/usdc/perpetual/ws.public.test.ts b/test/usdc/perpetual/ws.public.test.ts new file mode 100644 index 0000000..4707645 --- /dev/null +++ b/test/usdc/perpetual/ws.public.test.ts @@ -0,0 +1,82 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../../src'; +import { + logAllEvents, + getSilentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../../ws.util'; + +describe('Public USDC Perp Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'usdcPerp', + }; + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccessNoAuth') + ); + // logAllEvents(wsClient); + }); + + beforeEach(() => { + wsClient.removeAllListeners(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcPerpPublic, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public trade events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const topic = 'orderBook_200.100ms.BTCPERP'; + wsClient.subscribe(topic); + + try { + expect(await wsResponsePromise).toMatchObject({ + success: true, + ret_msg: '', + request: { + op: 'subscribe', + args: [topic], + }, + }); + } catch (e) { + // sub failed + expect(e).toBeFalsy(); + } + + try { + expect(await wsUpdatePromise).toMatchSnapshot({ + crossSeq: expect.any(String), + data: { orderBook: expect.any(Array) }, + timestampE6: expect.any(String), + topic: topic, + type: 'snapshot', + }); + } catch (e) { + // no data + expect(e).toBeFalsy(); + } + }); +}); From f5232605a150ae4d8ac3217dc8debd962b3a009c Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 13:26:29 +0100 Subject: [PATCH 42/74] fixes --- src/websocket-client.ts | 11 ----------- test/ws.util.ts | 4 ++-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/websocket-client.ts b/src/websocket-client.ts index fe73c3e..18898b9 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -144,17 +144,6 @@ export class WebsocketClient extends EventEmitter { this.connectPublic(); break; } - case 'spotv3': { - this.restClient = new SpotClientV3( - undefined, - undefined, - !this.isTestnet(), - this.options.restOptions, - this.options.requestOptions - ); - this.connectPublic(); - break; - } case 'usdcOption': { this.restClient = new USDCOptionClient( undefined, diff --git a/test/ws.util.ts b/test/ws.util.ts index 855dee5..c954169 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -37,6 +37,8 @@ export function waitForSocketEvent( ); }, timeoutMs); + let resolvedOnce = false; + function cleanup() { clearTimeout(timeout); resolvedOnce = true; @@ -44,8 +46,6 @@ export function waitForSocketEvent( wsClient.removeListener('error', (e) => rejector(e)); } - let resolvedOnce = false; - function resolver(event) { resolve(event); cleanup(); From dd047326bfdadd606a22797b4fe7e8673271d738 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 13:30:49 +0100 Subject: [PATCH 43/74] fix test typo --- test/usdc/perpetual/ws.public.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/usdc/perpetual/ws.public.test.ts b/test/usdc/perpetual/ws.public.test.ts index 4707645..b3fce40 100644 --- a/test/usdc/perpetual/ws.public.test.ts +++ b/test/usdc/perpetual/ws.public.test.ts @@ -67,7 +67,7 @@ describe('Public USDC Perp Websocket Client', () => { } try { - expect(await wsUpdatePromise).toMatchSnapshot({ + expect(await wsUpdatePromise).toMatchObject({ crossSeq: expect.any(String), data: { orderBook: expect.any(Array) }, timestampE6: expect.any(String), From d16dee8caa0d42763e2a8e7da9b4e12175acc49e Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 14:09:01 +0100 Subject: [PATCH 44/74] usdc private test --- src/util/WsStore.ts | 2 + src/util/websocket-util.ts | 10 +- src/websocket-client.ts | 52 ++++------ test/spot/ws.private.v3.test.ts | 2 +- test/usdc/options/ws.private.test.ts | 150 +++++++++++++++++++++++++++ test/ws.util.ts | 6 +- 6 files changed, 183 insertions(+), 39 deletions(-) create mode 100644 test/usdc/options/ws.private.test.ts diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index 9539079..9d68743 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -29,6 +29,8 @@ interface WsStoredState { activePingTimer?: ReturnType | undefined; /** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ activePongTimer?: ReturnType | undefined; + /** If a reconnection is in progress, this will have the timer for the delayed reconnect */ + activeReconnectTimer?: ReturnType | undefined; /** * All the topics we are expected to be subscribed to (and we automatically resubscribe to if the connection drops) */ diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 7507529..dbc743e 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -97,7 +97,11 @@ export const WS_KEY_MAP = { usdcPerpPublic: 'usdcPerpPublic', } as const; -export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; +export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ + WS_KEY_MAP.spotV3Private, + WS_KEY_MAP.usdcOptionPrivate, + WS_KEY_MAP.usdcPerpPrivate, +]; export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.linearPublic, @@ -202,7 +206,9 @@ export function getUsdcWsKeyForTopic( export const WS_ERROR_ENUM = { NOT_AUTHENTICATED_SPOT_V3: '-1004', - BAD_API_KEY_SPOT_V3: '10003', + API_ERROR_GENERIC: '10001', + API_SIGN_AUTH_FAILED: '10003', + USDC_OPTION_AUTH_FAILED: '3303006', }; export function neverGuard(x: never, msg: string): Error { diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 18898b9..1dc88c7 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -40,7 +40,7 @@ export type WsClientEvent = | 'open' | 'update' | 'close' - | 'error' + | 'errorEvent' | 'reconnect' | 'reconnected' | 'response'; @@ -52,7 +52,7 @@ interface WebsocketClientEvents { close: (evt: { wsKey: WsKey; event: any }) => void; response: (response: any) => void; update: (response: any) => void; - error: (response: any) => void; + errorEvent: (response: any) => void; } // Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837 @@ -141,7 +141,6 @@ export class WebsocketClient extends EventEmitter { this.options.restOptions, this.options.requestOptions ); - this.connectPublic(); break; } case 'usdcOption': { @@ -152,7 +151,6 @@ export class WebsocketClient extends EventEmitter { this.options.restOptions, this.options.requestOptions ); - this.connectPublic(); break; } case 'usdcPerp': { @@ -163,7 +161,6 @@ export class WebsocketClient extends EventEmitter { this.options.restOptions, this.options.requestOptions ); - this.connectPublic(); break; } default: { @@ -179,23 +176,6 @@ export class WebsocketClient extends EventEmitter { return this.options.testnet === true; } - public isLinear(): boolean { - return this.options.market === 'linear'; - } - - public isSpot(): boolean { - return this.options.market === 'spot'; - } - - public isInverse(): boolean { - return this.options.market === 'inverse'; - } - - /** USDC, spot v3, unified margin, account asset */ - // public isV3(): boolean { - // return this.options.market === 'v3'; - // } - public close(wsKey: WsKey) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); @@ -333,7 +313,7 @@ export class WebsocketClient extends EventEmitter { private parseWsError(context: string, error: any, wsKey: WsKey) { if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); - this.emit('error', error); + this.emit('errorEvent', error); return; } @@ -352,7 +332,7 @@ export class WebsocketClient extends EventEmitter { ); break; } - this.emit('error', error); + this.emit('errorEvent', error); } /** @@ -443,7 +423,7 @@ export class WebsocketClient extends EventEmitter { this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); } - setTimeout(() => { + this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.logger.info('Reconnecting to websocket', { ...loggerCategory, wsKey, @@ -458,7 +438,7 @@ export class WebsocketClient extends EventEmitter { this.logger.silly('Sending ping', { ...loggerCategory, wsKey }); this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' })); - this.wsStore.get(wsKey, true)!.activePongTimer = setTimeout(() => { + this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { this.logger.info('Pong timeout - closing socket to reconnect', { ...loggerCategory, wsKey, @@ -470,6 +450,10 @@ export class WebsocketClient extends EventEmitter { private clearTimers(wsKey: WsKey) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); + const wsState = this.wsStore.get(wsKey); + if (wsState?.activeReconnectTimer) { + clearTimeout(wsState.activeReconnectTimer); + } } // Send a ping at intervals @@ -636,9 +620,11 @@ export class WebsocketClient extends EventEmitter { // spot v1 msg?.code || // spot v3 - msg?.type === 'error' + msg?.type === 'error' || + // usdc options + msg?.success === false ) { - return this.emit('error', msg); + return this.emit('errorEvent', msg); } this.logger.warning('Unhandled/unrecognised ws event message', { @@ -662,7 +648,7 @@ export class WebsocketClient extends EventEmitter { if ( this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) ) { - this.emit('error', error); + this.emit('errorEvent', error); } } @@ -814,7 +800,7 @@ export class WebsocketClient extends EventEmitter { /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotTrades(symbol: string, binary?: boolean) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } @@ -833,7 +819,7 @@ export class WebsocketClient extends EventEmitter { /** @deprecated use "market: 'spotv3" client */ public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } @@ -856,7 +842,7 @@ export class WebsocketClient extends EventEmitter { candleSize: KlineInterval, binary?: boolean ) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } @@ -884,7 +870,7 @@ export class WebsocketClient extends EventEmitter { dumpScale?: number, binary?: boolean ) { - if (!this.isSpot()) { + if (this.options.market !== 'spot') { throw this.wrongMarketError('spot'); } diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts index b430b29..16836fa 100644 --- a/test/spot/ws.private.v3.test.ts +++ b/test/spot/ws.private.v3.test.ts @@ -40,7 +40,7 @@ describe('Private Spot V3 Websocket Client', () => { badClient.subscribe(wsTopic); expect(wsResponsePromise).rejects.toMatchObject({ - ret_code: WS_ERROR_ENUM.BAD_API_KEY_SPOT_V3, + ret_code: WS_ERROR_ENUM.API_SIGN_AUTH_FAILED, ret_msg: expect.any(String), type: 'error', }); diff --git a/test/usdc/options/ws.private.test.ts b/test/usdc/options/ws.private.test.ts new file mode 100644 index 0000000..4f94136 --- /dev/null +++ b/test/usdc/options/ws.private.test.ts @@ -0,0 +1,150 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_ERROR_ENUM, + WS_KEY_MAP, +} from '../../../src'; +import { + fullLogger, + getSilentLogger, + logAllEvents, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../../ws.util'; + +describe('Private USDC Option Websocket Client', () => { + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'usdcOption', + key: API_KEY, + secret: API_SECRET, + }; + + const wsTopic = `user.openapi.option.position`; + + describe('with invalid credentials', () => { + it('should reject private subscribe if keys/signature are incorrect', async () => { + const badClient = new WebsocketClient( + { + ...wsClientOptions, + key: 'bad', + secret: 'bad', + reconnectTimeout: 10000, + }, + // fullLogger + getSilentLogger('expect401') + ); + // logAllEvents(badClient); + + // const wsOpenPromise = waitForSocketEvent(badClient, 'open'); + const wsResponsePromise = waitForSocketEvent(badClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + badClient.connectPrivate(); + + const responsePartial = { + ret_msg: WS_ERROR_ENUM.USDC_OPTION_AUTH_FAILED, + success: false, + type: 'AUTH_RESP', + }; + expect(wsResponsePromise).rejects.toMatchObject(responsePartial); + + try { + await Promise.all([wsResponsePromise]); + } catch (e) { + // console.error() + expect(e).toMatchObject(responsePartial); + } + + // badClient.subscribe(wsTopic); + badClient.removeAllListeners(); + badClient.closeAll(); + }); + }); + + describe('with valid API credentails', () => { + let wsClient: WebsocketClient; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); + wsClient.connectPrivate(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcOptionPrivate, + }); + + try { + await Promise.all([wsOpenPromise]); + } catch (e) { + expect(e).toBeFalsy(); + } + + try { + expect(await wsResponsePromise).toMatchObject({ + ret_msg: '0', + success: true, + type: 'AUTH_RESP', + }); + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + expect(e).toBeFalsy(); + } + }); + + it(`should subscribe to private "${wsTopic}" events`, async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + // expect(wsUpdatePromise).resolves.toStrictEqual(''); + wsClient.subscribe(wsTopic); + + try { + expect(await wsResponsePromise).toMatchObject({ + data: { + failTopics: [], + successTopics: [wsTopic], + }, + success: true, + type: 'COMMAND_RESP', + }); + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + expect(e).toBeFalsy(); + } + expect(await wsUpdatePromise).toMatchObject({ + creationTime: expect.any(Number), + data: { + baseLine: expect.any(Number), + dataType: expect.any(String), + result: expect.any(Array), + version: expect.any(Number), + }, + topic: wsTopic, + }); + }); + }); +}); diff --git a/test/ws.util.ts b/test/ws.util.ts index c954169..ae0302c 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -59,7 +59,7 @@ export function waitForSocketEvent( } wsClient.on(event, (e) => resolver(e)); - wsClient.on('error', (e) => rejector(e)); + wsClient.on('errorEvent', (e) => rejector(e)); // if (event !== 'close') { // wsClient.on('close', (event) => { @@ -89,7 +89,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { wsClient.on('response', retVal.response); wsClient.on('update', retVal.update); wsClient.on('close', retVal.close); - wsClient.on('error', retVal.error); + wsClient.on('errorEvent', retVal.error); return { ...retVal, @@ -98,7 +98,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { wsClient.removeListener('response', retVal.response); wsClient.removeListener('update', retVal.update); wsClient.removeListener('close', retVal.close); - wsClient.removeListener('error', retVal.error); + wsClient.removeListener('errorEvent', retVal.error); }, }; } From 08f0914740cc5eea4e38a0e49dc6c5b95e721fb5 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 14:10:59 +0100 Subject: [PATCH 45/74] usdc perp ws test private --- test/usdc/perpetual/ws.private.test.ts | 150 +++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 test/usdc/perpetual/ws.private.test.ts diff --git a/test/usdc/perpetual/ws.private.test.ts b/test/usdc/perpetual/ws.private.test.ts new file mode 100644 index 0000000..84d0f4b --- /dev/null +++ b/test/usdc/perpetual/ws.private.test.ts @@ -0,0 +1,150 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_ERROR_ENUM, + WS_KEY_MAP, +} from '../../../src'; +import { + fullLogger, + getSilentLogger, + logAllEvents, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../../ws.util'; + +describe('Private USDC Perp Websocket Client', () => { + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'usdcPerp', + key: API_KEY, + secret: API_SECRET, + }; + + const wsTopic = `user.openapi.perp.position`; + + describe('with invalid credentials', () => { + it('should reject private subscribe if keys/signature are incorrect', async () => { + const badClient = new WebsocketClient( + { + ...wsClientOptions, + key: 'bad', + secret: 'bad', + reconnectTimeout: 10000, + }, + // fullLogger + getSilentLogger('expect401') + ); + // logAllEvents(badClient); + + // const wsOpenPromise = waitForSocketEvent(badClient, 'open'); + const wsResponsePromise = waitForSocketEvent(badClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + badClient.connectPrivate(); + + const responsePartial = { + ret_msg: WS_ERROR_ENUM.USDC_OPTION_AUTH_FAILED, + success: false, + type: 'AUTH_RESP', + }; + expect(wsResponsePromise).rejects.toMatchObject(responsePartial); + + try { + await Promise.all([wsResponsePromise]); + } catch (e) { + // console.error() + expect(e).toMatchObject(responsePartial); + } + + // badClient.subscribe(wsTopic); + badClient.removeAllListeners(); + badClient.closeAll(); + }); + }); + + describe('with valid API credentails', () => { + let wsClient: WebsocketClient; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccess') + ); + wsClient.connectPrivate(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcPerpPrivate, + }); + + try { + await Promise.all([wsOpenPromise]); + } catch (e) { + expect(e).toBeFalsy(); + } + + try { + expect(await wsResponsePromise).toMatchObject({ + ret_msg: '0', + success: true, + type: 'AUTH_RESP', + }); + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + expect(e).toBeFalsy(); + } + }); + + it(`should subscribe to private "${wsTopic}" events`, async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + // expect(wsUpdatePromise).resolves.toStrictEqual(''); + wsClient.subscribe(wsTopic); + + try { + expect(await wsResponsePromise).toMatchObject({ + data: { + failTopics: [], + successTopics: [wsTopic], + }, + success: true, + type: 'COMMAND_RESP', + }); + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + expect(e).toBeFalsy(); + } + expect(await wsUpdatePromise).toMatchObject({ + creationTime: expect.any(Number), + data: { + baseLine: expect.any(Number), + dataType: expect.any(String), + result: expect.any(Array), + version: expect.any(Number), + }, + topic: wsTopic, + }); + }); + }); +}); From f4a569dcbacb0d6920309676a3712b29208ad19f Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 14:21:11 +0100 Subject: [PATCH 46/74] fix tests --- test/inverse/ws.private.test.ts | 16 ++++++------ test/linear/ws.private.test.ts | 1 - test/linear/ws.public.test.ts | 6 ++++- test/spot/ws.private.v3.test.ts | 35 +++++++++------------------ test/usdc/options/ws.public.test.ts | 6 +---- test/usdc/perpetual/ws.public.test.ts | 6 +---- test/ws.util.ts | 4 +-- 7 files changed, 27 insertions(+), 47 deletions(-) diff --git a/test/inverse/ws.private.test.ts b/test/inverse/ws.private.test.ts index c95a3b5..23c703d 100644 --- a/test/inverse/ws.private.test.ts +++ b/test/inverse/ws.private.test.ts @@ -81,14 +81,6 @@ describe('Private Inverse Perps Websocket Client', () => { // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); const wsTopic = 'wallet'; - expect(wsResponsePromise).resolves.toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, - success: true, - }); - // No easy way to trigger a private event (other than executing trades) // expect(wsUpdatePromise).resolves.toMatchObject({ // topic: wsTopic, @@ -97,7 +89,13 @@ describe('Private Inverse Perps Websocket Client', () => { wsClient.subscribe(wsTopic); - await Promise.all([wsResponsePromise]); + expect(await wsResponsePromise).toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); }); }); }); diff --git a/test/linear/ws.private.test.ts b/test/linear/ws.private.test.ts index 1f9336f..fca6ea2 100644 --- a/test/linear/ws.private.test.ts +++ b/test/linear/ws.private.test.ts @@ -31,7 +31,6 @@ describe('Private Linear Perps Websocket Client', () => { ); const wsOpenPromise = waitForSocketEvent(badClient, 'open', 2500); - badClient.connectPrivate(); try { diff --git a/test/linear/ws.public.test.ts b/test/linear/ws.public.test.ts index ee1dd27..7d3b195 100644 --- a/test/linear/ws.public.test.ts +++ b/test/linear/ws.public.test.ts @@ -94,6 +94,10 @@ describe('Public Linear Perps Websocket Client', () => { wsClient.subscribe(wsTopic); - await Promise.all([wsResponsePromise]); + try { + await Promise.all([wsResponsePromise]); + } catch (e) { + // + } }); }); diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts index 16836fa..1a5d614 100644 --- a/test/spot/ws.private.v3.test.ts +++ b/test/spot/ws.private.v3.test.ts @@ -90,16 +90,11 @@ describe('Private Spot V3 Websocket Client', () => { expect(e).toBeFalsy(); } - try { - expect(await wsResponsePromise).toMatchObject({ - op: 'auth', - success: true, - req_id: `${WS_KEY_MAP.spotV3Private}-auth`, - }); - } catch (e) { - console.error(`Wait for "${wsTopic}" event exception: `, e); - expect(e).toBeFalsy(); - } + expect(await wsResponsePromise).toMatchObject({ + op: 'auth', + success: true, + req_id: `${WS_KEY_MAP.spotV3Private}-auth`, + }); }); it('should subscribe to private outboundAccountInfo events', async () => { @@ -108,20 +103,12 @@ describe('Private Spot V3 Websocket Client', () => { // expect(wsUpdatePromise).resolves.toStrictEqual(''); wsClient.subscribe(wsTopic); - try { - expect(await wsResponsePromise).toMatchObject({ - op: 'subscribe', - success: true, - ret_msg: '', - req_id: wsTopic, - }); - } catch (e) { - console.error( - `Wait for "${wsTopic}" subscription response exception: `, - e - ); - expect(e).toBeFalsy(); - } + expect(await wsResponsePromise).toMatchObject({ + op: 'subscribe', + success: true, + ret_msg: '', + req_id: wsTopic, + }); }); }); }); diff --git a/test/usdc/options/ws.public.test.ts b/test/usdc/options/ws.public.test.ts index 0771099..48c4fbf 100644 --- a/test/usdc/options/ws.public.test.ts +++ b/test/usdc/options/ws.public.test.ts @@ -22,11 +22,7 @@ describe('Public USDC Option Websocket Client', () => { wsClientOptions, getSilentLogger('expectSuccessNoAuth') ); - }); - - beforeEach(() => { - wsClient.removeAllListeners(); - // logAllEvents(wsClient); + wsClient.connectPublic(); }); afterAll(() => { diff --git a/test/usdc/perpetual/ws.public.test.ts b/test/usdc/perpetual/ws.public.test.ts index b3fce40..5b67c0f 100644 --- a/test/usdc/perpetual/ws.public.test.ts +++ b/test/usdc/perpetual/ws.public.test.ts @@ -22,11 +22,7 @@ describe('Public USDC Perp Websocket Client', () => { wsClientOptions, getSilentLogger('expectSuccessNoAuth') ); - // logAllEvents(wsClient); - }); - - beforeEach(() => { - wsClient.removeAllListeners(); + wsClient.connectPublic(); // logAllEvents(wsClient); }); diff --git a/test/ws.util.ts b/test/ws.util.ts index ae0302c..1fa49da 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -6,8 +6,8 @@ export function getSilentLogger(logHint?: string) { debug: () => {}, notice: () => {}, info: () => {}, - warning: (...params) => console.warn('warning', logHint, ...params), - error: (...params) => console.error('error', logHint, ...params), + warning: () => {}, + error: () => {}, }; } From 1c3707b6d25ec37d12e034beb1ffc1d9b66385a4 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 15:09:39 +0100 Subject: [PATCH 47/74] cleaning for ws tests --- test/inverse/ws.private.test.ts | 4 +- test/inverse/ws.public.test.ts | 14 ++++--- test/linear/ws.private.test.ts | 14 ++++--- test/linear/ws.public.test.ts | 56 +++++++++++--------------- test/spot/ws.private.v1.test.ts | 15 ++++--- test/spot/ws.private.v3.test.ts | 10 ++--- test/spot/ws.public.v1.test.ts | 14 ++++--- test/spot/ws.public.v3.test.ts | 14 ++++--- test/usdc/options/ws.private.test.ts | 13 +++--- test/usdc/options/ws.public.test.ts | 15 +++---- test/usdc/perpetual/ws.private.test.ts | 10 ++--- test/usdc/perpetual/ws.public.test.ts | 4 +- 12 files changed, 90 insertions(+), 93 deletions(-) diff --git a/test/inverse/ws.private.test.ts b/test/inverse/ws.private.test.ts index 23c703d..83b01c3 100644 --- a/test/inverse/ws.private.test.ts +++ b/test/inverse/ws.private.test.ts @@ -68,12 +68,10 @@ describe('Private Inverse Perps Websocket Client', () => { it('should open a ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ + expect(await wsOpenPromise).toMatchObject({ event: WS_OPEN_EVENT_PARTIAL, wsKey: WS_KEY_MAP.inverse, }); - - await Promise.all([wsOpenPromise]); }); it('should subscribe to private wallet events', async () => { diff --git a/test/inverse/ws.public.test.ts b/test/inverse/ws.public.test.ts index 709421c..8d02b6d 100644 --- a/test/inverse/ws.public.test.ts +++ b/test/inverse/ws.public.test.ts @@ -30,12 +30,14 @@ describe('Public Inverse Perps Websocket Client', () => { it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.inverse, - }); - - await Promise.all([wsOpenPromise]); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.inverse, + }); + } catch (e) { + expect(e).toBeFalsy(); + } }); it('should subscribe to public orderBookL2_25 events', async () => { diff --git a/test/linear/ws.private.test.ts b/test/linear/ws.private.test.ts index fca6ea2..09290b1 100644 --- a/test/linear/ws.private.test.ts +++ b/test/linear/ws.private.test.ts @@ -71,12 +71,14 @@ describe('Private Linear Perps Websocket Client', () => { it('should open a ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.linearPrivate, - }); - - await Promise.all([wsOpenPromise]); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.linearPrivate, + }); + } catch (e) { + expect(e).toBeFalsy(); + } }); it('should subscribe to private wallet events', async () => { diff --git a/test/linear/ws.public.test.ts b/test/linear/ws.public.test.ts index 7d3b195..6281c07 100644 --- a/test/linear/ws.public.test.ts +++ b/test/linear/ws.public.test.ts @@ -23,17 +23,16 @@ describe('Public Linear Perps Websocket Client', () => { afterAll(() => { wsClient.closeAll(); + wsClient.removeAllListeners(); }); it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ + expect(await wsOpenPromise).toMatchObject({ event: WS_OPEN_EVENT_PARTIAL, wsKey: WS_KEY_MAP.linearPublic, }); - - await Promise.all([wsOpenPromise]); }); it('should subscribe to public orderBookL2_25 events', async () => { @@ -41,33 +40,27 @@ describe('Public Linear Perps Websocket Client', () => { const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); const wsTopic = 'orderBookL2_25.BTCUSDT'; - expect(wsResponsePromise).resolves.toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, - success: true, - }); - expect(wsUpdatePromise).resolves.toMatchObject({ - topic: wsTopic, - data: { - order_book: expect.any(Array), - }, - }); - wsClient.subscribe(wsTopic); try { - await wsResponsePromise; + expect(await wsResponsePromise).toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); } catch (e) { - console.error( - `Wait for "${wsTopic}" subscription response exception: `, - e - ); + expect(e).toBeFalsy(); } try { - await wsUpdatePromise; + expect(await wsUpdatePromise).toMatchObject({ + topic: wsTopic, + data: { + order_book: expect.any(Array), + }, + }); } catch (e) { console.error(`Wait for "${wsTopic}" event exception: `, e); } @@ -78,13 +71,6 @@ describe('Public Linear Perps Websocket Client', () => { // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); const wsTopic = 'wallet'; - expect(wsResponsePromise).resolves.toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, - success: true, - }); // No easy way to trigger a private event (other than executing trades) // expect(wsUpdatePromise).resolves.toMatchObject({ @@ -95,9 +81,15 @@ describe('Public Linear Perps Websocket Client', () => { wsClient.subscribe(wsTopic); try { - await Promise.all([wsResponsePromise]); + expect(await wsResponsePromise).toBeFalsy(); } catch (e) { - // + expect(e).toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: false, + }); } }); }); diff --git a/test/spot/ws.private.v1.test.ts b/test/spot/ws.private.v1.test.ts index 3cb62f3..7361a36 100644 --- a/test/spot/ws.private.v1.test.ts +++ b/test/spot/ws.private.v1.test.ts @@ -46,15 +46,20 @@ describe('Private Spot V1 Websocket Client', () => { wsClient.connectPrivate(); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.spotPrivate, - }); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + // wsKey: WS_KEY_MAP.spotPrivate, + // also opens public conn automatically, which can confuse the test + }); + } catch (e) { + expect(e).toBeFalsy(); + } // expect(wsUpdatePromise).resolves.toMatchObject({ // topic: 'wsTopic', // data: expect.any(Array), // }); - await Promise.all([wsOpenPromise]); + // await Promise.all([wsUpdatePromise]); // await promiseSleep(4000); }); diff --git a/test/spot/ws.private.v3.test.ts b/test/spot/ws.private.v3.test.ts index 1a5d614..6fb8442 100644 --- a/test/spot/ws.private.v3.test.ts +++ b/test/spot/ws.private.v3.test.ts @@ -79,13 +79,11 @@ describe('Private Spot V3 Websocket Client', () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.spotV3Private, - }); - try { - await Promise.all([wsOpenPromise]); + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotV3Private, + }); } catch (e) { expect(e).toBeFalsy(); } diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts index cdb48d4..3b021b2 100644 --- a/test/spot/ws.public.v1.test.ts +++ b/test/spot/ws.public.v1.test.ts @@ -34,12 +34,14 @@ describe('Public Spot V1 Websocket Client', () => { it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.spotPublic, - }); - - await Promise.all([wsOpenPromise]); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotPublic, + }); + } catch (e) { + expect(e).toBeFalsy(); + } }); it('should subscribe to public orderbook events', async () => { diff --git a/test/spot/ws.public.v3.test.ts b/test/spot/ws.public.v3.test.ts index 0cd86f1..0f2d662 100644 --- a/test/spot/ws.public.v3.test.ts +++ b/test/spot/ws.public.v3.test.ts @@ -34,12 +34,14 @@ describe('Public Spot V3 Websocket Client', () => { it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.spotV3Public, - }); - - await Promise.all([wsOpenPromise]); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotV3Public, + }); + } catch (e) { + expect(e).toBeFalsy(); + } }); it('should subscribe to public orderbook events', async () => { diff --git a/test/usdc/options/ws.private.test.ts b/test/usdc/options/ws.private.test.ts index 4f94136..ae93be4 100644 --- a/test/usdc/options/ws.private.test.ts +++ b/test/usdc/options/ws.private.test.ts @@ -77,7 +77,6 @@ describe('Private USDC Option Websocket Client', () => { wsClientOptions, getSilentLogger('expectSuccess') ); - wsClient.connectPrivate(); // logAllEvents(wsClient); }); @@ -86,16 +85,15 @@ describe('Private USDC Option Websocket Client', () => { }); it('should open a private ws connection', async () => { + wsClient.connectPrivate(); const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.usdcOptionPrivate, - }); - try { - await Promise.all([wsOpenPromise]); + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcOptionPrivate, + }); } catch (e) { expect(e).toBeFalsy(); } @@ -113,6 +111,7 @@ describe('Private USDC Option Websocket Client', () => { }); it(`should subscribe to private "${wsTopic}" events`, async () => { + wsClient.connectPrivate(); const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); diff --git a/test/usdc/options/ws.public.test.ts b/test/usdc/options/ws.public.test.ts index 48c4fbf..90b4822 100644 --- a/test/usdc/options/ws.public.test.ts +++ b/test/usdc/options/ws.public.test.ts @@ -31,13 +31,14 @@ describe('Public USDC Option Websocket Client', () => { it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.usdcOptionPublic, - }); - - await Promise.all([wsOpenPromise]); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcOptionPublic, + }); + } catch (e) { + expect(e).toBeFalsy(); + } }); it('should subscribe to public trade events', async () => { diff --git a/test/usdc/perpetual/ws.private.test.ts b/test/usdc/perpetual/ws.private.test.ts index 84d0f4b..7508292 100644 --- a/test/usdc/perpetual/ws.private.test.ts +++ b/test/usdc/perpetual/ws.private.test.ts @@ -89,13 +89,11 @@ describe('Private USDC Perp Websocket Client', () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); - expect(wsOpenPromise).resolves.toMatchObject({ - event: WS_OPEN_EVENT_PARTIAL, - wsKey: WS_KEY_MAP.usdcPerpPrivate, - }); - try { - await Promise.all([wsOpenPromise]); + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.usdcPerpPrivate, + }); } catch (e) { expect(e).toBeFalsy(); } diff --git a/test/usdc/perpetual/ws.public.test.ts b/test/usdc/perpetual/ws.public.test.ts index 5b67c0f..d5bcb87 100644 --- a/test/usdc/perpetual/ws.public.test.ts +++ b/test/usdc/perpetual/ws.public.test.ts @@ -33,12 +33,10 @@ describe('Public USDC Perp Websocket Client', () => { it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); - expect(wsOpenPromise).resolves.toMatchObject({ + expect(await wsOpenPromise).toMatchObject({ event: WS_OPEN_EVENT_PARTIAL, wsKey: WS_KEY_MAP.usdcPerpPublic, }); - - await Promise.all([wsOpenPromise]); }); it('should subscribe to public trade events', async () => { From 6fbaf7c80af709719152339a36e711d3feba177f Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 17:06:04 +0100 Subject: [PATCH 48/74] version bump for beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d051f5..badb8e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "2.4.0-beta.1", + "version": "2.4.0-beta.2", "description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.", "main": "lib/index.js", "types": "lib/index.d.ts", From 350ed53a65ebd7234faf038d2a8582b0386fa76d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 17:06:27 +0100 Subject: [PATCH 49/74] unified margin support for ws --- README.md | 10 +- doc/websocket-client.md | 223 ------------------------------------- src/types/shared.ts | 4 +- src/types/websockets.ts | 4 +- src/util/websocket-util.ts | 69 +++++++++++- src/websocket-client.ts | 59 ++++++++-- 6 files changed, 128 insertions(+), 241 deletions(-) delete mode 100644 doc/websocket-client.md diff --git a/README.md b/README.md index 8641a9b..4594f5a 100644 --- a/README.md +++ b/README.md @@ -152,15 +152,16 @@ All API groups can be used via a shared `WebsocketClient`. However, make sure to The WebsocketClient can be configured to a specific API group using the market parameter. These are the currently available API groups: | API Category | Market | Description | |:----------------------------: |:-------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Unified Margin | TBC | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. | +| Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | +| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | | Futures v2 - Inverse Perps | `market: 'inverse'` | The [inverse v2 perps](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-websocket) category. | | Futures v2 - USDT Perps | `market: 'linear'` | The [USDT/linear v2 perps](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-websocket) category. | | Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | | Spot v3 | `market: 'spotv3'` | The [spot v3](https://bybit-exchange.github.io/docs/spot/v3/#t-websocket) category. | | Spot v1 | `market: 'spot'` | The older [spot v1](https://bybit-exchange.github.io/docs/spot/v1/#t-websocket) category. Use the `spotv3` market if possible, as the v1 category does not have automatic re-subscribe if reconnected. | -| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. | -| USDC Perps | TBC | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | -| USDC Options | TBC | The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | +| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. | +| USDC Perps | `market: 'usdcPerp` | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | +| USDC Options | `market: 'usdcOption'`| The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | ```javascript const { WebsocketClient } = require('bybit-api'); @@ -181,7 +182,6 @@ const wsConfig = { // NOTE: to listen to multiple markets (spot vs inverse vs linear vs linearfutures) at once, make one WebsocketClient instance per market - // defaults to inverse: // market: 'inverse' // market: 'linear' // market: 'spot' diff --git a/doc/websocket-client.md b/doc/websocket-client.md deleted file mode 100644 index 23ecfbd..0000000 --- a/doc/websocket-client.md +++ /dev/null @@ -1,223 +0,0 @@ -# Websocket API -## Class: WebsocketClient - -The `WebsocketClient` inherits from `EventEmitter`. After establishing a -connection, the client sends heartbeats in regular intervalls, and reconnects -to the server once connection has been lost. - -### new WebsocketClient([options][, logger]) -- `options` {Object} Configuration options - - `key` {String} Bybit API Key. Only needed if private topics are subscribed - - `secret` {String} Bybit private Key. Only needed if private topics are - subscribed - - `livenet` {Bool} Weather to connect to livenet (`true`). Default `false`. - - `pingInterval` {Integer} Interval in ms for heartbeat ping. Default: `10000`, - - `pongTimeout` {Integer} Timeout in ms waiting for heartbeat pong response - from server. Default: `1000`, - - `reconnectTimeout` {Integer} Timeout in ms the client waits before trying - to reconnect after a lost connection. Default: 500 -- `logger` {Object} Optional custom logger - -Custom logger must contain the following methods: -```js -const logger = { - silly: function(message, data) {}, - debug: function(message, data) {}, - notice: function(message, data) {}, - info: function(message, data) {}, - warning: function(message, data) {}, - error: function(message, data) {}, -} -``` - -### ws.subscribe(topics) -- `topics` {String|Array} Single topic as string or multiple topics as array of strings. -Subscribe to one or multiple topics. See [available topics](#available-topics) - -### ws.unsubscribe(topics) -- `topics` {String|Array} Single topic as string or multiple topics as array of strings. -Unsubscribe from one or multiple topics. - -### ws.close() -Close the connection to the server. - -### Event: 'open' -Emmited when the connection has been opened for the first time. - -### Event: 'reconnected' -Emmited when the client has been opened after a reconnect. - -### Event: 'update' -- `message` {Object} - - `topic` {String} the topic for which the update occured - - `data` {Array|Object} updated data (see docs for each [topic](#available-topics)). - - `type` {String} Some topics might have different update types (see docs for each [topic](#available-topics)). - -Emmited whenever an update to a subscribed topic occurs. - -### Event: 'response' -- `response` {Object} - - `success` {Bool} - - `ret_msg` {String} empty if operation was successfull, otherwise error message. - - `conn_id` {String} connection id - - `request` {Object} Original request, to which the response belongs - - `op` {String} operation - - `args` {Array} Request Arguments - -Emited when the server responds to an operation sent by the client (usually after subscribing to a topic). - -### Event: 'close' -Emitted when the connection has been finally closed, after a call to `ws.close()` - -### Event: 'reconnect' -Emitted when the connection has been closed, but the client will try to reconnect. - -### Event: 'error' -- `error` {Error} - -Emitted when an error occurs. - -## Available Topics -Generaly all [public](https://bybit-exchange.github.io/docs/inverse/#t-publictopics) and [private](https://bybit-exchange.github.io/docs/inverse/#t-privatetopics) - topics are available. - -### Private topics -#### Positions of your account -All positions of your account. -Topic: `position` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketposition) - -#### Execution message -Execution message, whenever an order has been (partially) filled. -Topic: `execution` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketexecution) - -#### Update for your orders -Updates for your active orders -Topic: `order` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketorder) - -#### Update for your conditional orders -Updates for your active conditional orders -Topic: `stop_order` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketstoporder) - -### Public topics -#### Candlestick chart -Candlestick OHLC "candles" for selected symbol and interval. -Example topic: `klineV2.BTCUSD.1m` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketklinev2) - -#### Real-time trading information -All trades as they occur. -Topic: `trade` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websockettrade) - -#### Daily insurance fund update -Topic: `insurance` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketinsurance) - -#### OrderBook of 25 depth per side -OrderBook for selected symbol -Example topic: `orderBookL2_25.BTCUSD` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketorderbook25) - -#### OrderBook of 200 depth per side -OrderBook for selected symbol -Example topic: `orderBook_200.100ms.BTCUS` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketorderbook200) - -#### Latest information for symbol -Latest information for selected symbol -Example topic: `instrument_info.100ms.BTCUSD` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketinstrumentinfo) - -## Examples -### Klines -```javascript -const {WebsocketClient} = require('bybit-api'); - -const API_KEY = 'xxx'; -const PRIVATE_KEY = 'yyy'; - -const ws = new WebsocketClient({key: API_KEY, secret: PRIVATE_KEY}); - -ws.subscribe(['position', 'execution', 'trade']); -ws.subscribe('kline.BTCUSD.1m'); - -ws.on('open', function() { - console.log('connection open'); -}); - -ws.on('update', function(message) { - console.log('update', message); -}); - -ws.on('response', function(response) { - console.log('response', response); -}); - -ws.on('close', function() { - console.log('connection closed'); -}); - -ws.on('error', function(err) { - console.error('ERR', err); -}); -``` - -### OrderBook Events -```javascript -const { WebsocketClient, DefaultLogger } = require('bybit-api'); -const { OrderBooksStore, OrderBookLevel } = require('orderbooks'); - -const OrderBooks = new OrderBooksStore({ traceLog: true, checkTimestamps: false }); - -// connect to a websocket and relay orderbook events to handlers -DefaultLogger.silly = () => {}; -const ws = new WebsocketClient({ livenet: true }); -ws.on('update', message => { - if (message.topic.toLowerCase().startsWith('orderbook')) { - return handleOrderbookUpdate(message); - } -}); -ws.subscribe('orderBookL2_25.BTCUSD'); - -// parse orderbook messages, detect snapshot vs delta, and format properties using OrderBookLevel -const handleOrderbookUpdate = message => { - const { topic, type, data, timestamp_e6 } = message; - const [ topicKey, symbol ] = topic.split('.'); - - if (type == 'snapshot') { - return OrderBooks.handleSnapshot(symbol, data.map(mapBybitBookSlice), timestamp_e6 / 1000, message).print(); - } - - if (type == 'delta') { - const deleteLevels = data.delete.map(mapBybitBookSlice); - const updateLevels = data.update.map(mapBybitBookSlice); - const insertLevels = data.insert.map(mapBybitBookSlice); - return OrderBooks.handleDelta( - symbol, - deleteLevels, - updateLevels, - insertLevels, - timestamp_e6 / 1000 - ).print(); - } -} - -// Low level map of exchange properties to expected local properties -const mapBybitBookSlice = level => { - return OrderBookLevel(level.symbol, +level.price, level.side, level.size); -}; -``` diff --git a/src/types/shared.ts b/src/types/shared.ts index 10aa83d..901e225 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -2,6 +2,7 @@ import { InverseClient } from '../inverse-client'; import { LinearClient } from '../linear-client'; import { SpotClient } from '../spot-client'; import { SpotClientV3 } from '../spot-client-v3'; +import { UnifiedMarginClient } from '../unified-margin-client'; import { USDCOptionClient } from '../usdc-option-client'; import { USDCPerpetualClient } from '../usdc-perpetual-client'; @@ -11,7 +12,8 @@ export type RESTClient = | SpotClient | SpotClientV3 | USDCOptionClient - | USDCPerpetualClient; + | USDCPerpetualClient + | UnifiedMarginClient; export type numberInString = string; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index a44a737..68b100f 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -7,7 +7,9 @@ export type APIMarket = | 'spot' | 'spotv3' | 'usdcOption' - | 'usdcPerp'; + | 'usdcPerp' + | 'unifiedPerp' + | 'unifiedOption'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index dbc743e..f4f2219 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -10,7 +10,7 @@ interface NetworkMapV3 { type PublicPrivateNetwork = 'public' | 'private'; export const WS_BASE_URL_MAP: Record< - APIMarket, + APIMarket | 'unifiedPerpUSDT' | 'unifiedPerpUSDC', Record > = { inverse: { @@ -81,6 +81,46 @@ export const WS_BASE_URL_MAP: Record< testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/private/v1', }, }, + unifiedOption: { + public: { + livenet: 'wss://stream.bybit.com/option/usdc/public/v3', + testnet: 'wss://stream-testnet.bybit.com/option/usdc/public/v3', + }, + private: { + livenet: 'wss://stream.bybit.com/unified/private/v3', + testnet: 'wss://stream-testnet.bybit.com/unified/private/v3', + }, + }, + unifiedPerp: { + public: { + livenet: 'useBaseSpecificEndpoint', + testnet: 'useBaseSpecificEndpoint', + }, + private: { + livenet: 'wss://stream.bybit.com/unified/private/v3', + testnet: 'wss://stream-testnet.bybit.com/unified/private/v3', + }, + }, + unifiedPerpUSDT: { + public: { + livenet: 'wss://stream.bybit.com/contract/usdt/public/v3', + testnet: 'wss://stream-testnet.bybit.com/contract/usdt/public/v3', + }, + private: { + livenet: 'useUnifiedEndpoint', + testnet: 'useUnifiedEndpoint', + }, + }, + unifiedPerpUSDC: { + public: { + livenet: 'wss://stream.bybit.com/contract/usdc/public/v3', + testnet: 'wss://stream-testnet.bybit.com/contract/usdc/public/v3', + }, + private: { + livenet: 'useUnifiedEndpoint', + testnet: 'useUnifiedEndpoint', + }, + }, }; export const WS_KEY_MAP = { @@ -95,6 +135,10 @@ export const WS_KEY_MAP = { usdcOptionPublic: 'usdcOptionPublic', usdcPerpPrivate: 'usdcPerpPrivate', usdcPerpPublic: 'usdcPerpPublic', + unifiedPrivate: 'unifiedPrivate', + unifiedOptionPublic: 'unifiedOptionPublic', + unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic', + unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic', } as const; export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ @@ -180,6 +224,29 @@ export function getWsKeyForTopic( ? WS_KEY_MAP.usdcPerpPrivate : WS_KEY_MAP.usdcPerpPublic; } + case 'unifiedOption': { + return isPrivateTopic + ? WS_KEY_MAP.unifiedPrivate + : WS_KEY_MAP.unifiedOptionPublic; + } + case 'unifiedPerp': { + if (isPrivateTopic) { + return WS_KEY_MAP.unifiedPrivate; + } + + const upperTopic = topic.toUpperCase(); + if (upperTopic.indexOf('USDT') !== -1) { + return WS_KEY_MAP.unifiedPerpUSDTPublic; + } + + if (upperTopic.indexOf('USDC') !== -1) { + return WS_KEY_MAP.unifiedPerpUSDCPublic; + } + + throw new Error( + `Failed to determine wskey for unified perps topic: "${topic}` + ); + } default: { throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`); } diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 1dc88c7..04d8323 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -33,6 +33,7 @@ import { } from './util'; import { USDCOptionClient } from './usdc-option-client'; import { USDCPerpetualClient } from './usdc-perpetual-client'; +import { UnifiedMarginClient } from './unified-margin-client'; const loggerCategory = { category: 'bybit-ws' }; @@ -163,6 +164,17 @@ export class WebsocketClient extends EventEmitter { ); break; } + case 'unifiedOption': + case 'unifiedPerp': { + this.restClient = new UnifiedMarginClient( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + break; + } default: { throw neverGuard( this.options.market, @@ -198,15 +210,17 @@ export class WebsocketClient extends EventEmitter { switch (this.options.market) { case 'inverse': { // only one for inverse - return [this.connectPublic()]; + return [...this.connectPublic()]; } // these all have separate public & private ws endpoints case 'linear': case 'spot': case 'spotv3': case 'usdcOption': - case 'usdcPerp': { - return [this.connectPublic(), this.connectPrivate()]; + case 'usdcPerp': + case 'unifiedPerp': + case 'unifiedOption': { + return [...this.connectPublic(), this.connectPrivate()]; } default: { throw neverGuard(this.options.market, `connectAll(): Unhandled market`); @@ -214,25 +228,34 @@ export class WebsocketClient extends EventEmitter { } } - public connectPublic(): Promise { + public connectPublic(): Promise[] { switch (this.options.market) { case 'inverse': { - return this.connect(WS_KEY_MAP.inverse); + return [this.connect(WS_KEY_MAP.inverse)]; } case 'linear': { - return this.connect(WS_KEY_MAP.linearPublic); + return [this.connect(WS_KEY_MAP.linearPublic)]; } case 'spot': { - return this.connect(WS_KEY_MAP.spotPublic); + return [this.connect(WS_KEY_MAP.spotPublic)]; } case 'spotv3': { - return this.connect(WS_KEY_MAP.spotV3Public); + return [this.connect(WS_KEY_MAP.spotV3Public)]; } case 'usdcOption': { - return this.connect(WS_KEY_MAP.usdcOptionPublic); + return [this.connect(WS_KEY_MAP.usdcOptionPublic)]; } case 'usdcPerp': { - return this.connect(WS_KEY_MAP.usdcPerpPublic); + return [this.connect(WS_KEY_MAP.usdcPerpPublic)]; + } + case 'unifiedOption': { + return [this.connect(WS_KEY_MAP.unifiedOptionPublic)]; + } + case 'unifiedPerp': { + return [ + this.connect(WS_KEY_MAP.unifiedPerpUSDTPublic), + this.connect(WS_KEY_MAP.unifiedPerpUSDCPublic), + ]; } default: { throw neverGuard( @@ -263,6 +286,10 @@ export class WebsocketClient extends EventEmitter { case 'usdcPerp': { return this.connect(WS_KEY_MAP.usdcPerpPrivate); } + case 'unifiedPerp': + case 'unifiedOption': { + return this.connect(WS_KEY_MAP.unifiedPrivate); + } default: { throw neverGuard( this.options.market, @@ -719,6 +746,18 @@ export class WebsocketClient extends EventEmitter { case WS_KEY_MAP.usdcPerpPrivate: { return WS_BASE_URL_MAP.usdcPerp.private[networkKey]; } + case WS_KEY_MAP.unifiedOptionPublic: { + return WS_BASE_URL_MAP.unifiedOption.public[networkKey]; + } + case WS_KEY_MAP.unifiedPerpUSDTPublic: { + return WS_BASE_URL_MAP.unifiedPerpUSDT.public[networkKey]; + } + case WS_KEY_MAP.unifiedPerpUSDCPublic: { + return WS_BASE_URL_MAP.unifiedPerpUSDC.public[networkKey]; + } + case WS_KEY_MAP.unifiedPrivate: { + return WS_BASE_URL_MAP.unifiedPerp.private[networkKey]; + } default: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { ...loggerCategory, From 2766a17fe89ec44281c43fd314b674a2925e82ba Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 19:39:33 +0100 Subject: [PATCH 50/74] fix bug with mixing topics and wskeys --- src/websocket-client.ts | 18 ++-- test/unified-margin/ws.public.option.test.ts | 67 +++++++++++++++ .../ws.public.perp.usdt.test.ts | 83 +++++++++++++++++++ test/ws.util.ts | 2 +- 4 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 test/unified-margin/ws.public.option.test.ts create mode 100644 test/unified-margin/ws.public.perp.usdt.test.ts diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 04d8323..6049e23 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -508,6 +508,7 @@ export class WebsocketClient extends EventEmitter { if (!topics.length) { return; } + const wsMessage = JSON.stringify({ req_id: topics.join(','), op: 'subscribe', @@ -631,16 +632,16 @@ export class WebsocketClient extends EventEmitter { if (isWsPong(msg)) { this.logger.silly('Received pong', { ...loggerCategory, wsKey }); } else { - this.emit('response', msg); + this.emit('response', { ...msg, wsKey }); } return; } if (msg['finalFragment']) { - return this.emit('response', msg); + return this.emit('response', { ...msg, wsKey }); } if (msg?.topic) { - return this.emit('update', msg); + return this.emit('update', { ...msg, wsKey }); } if ( @@ -651,7 +652,7 @@ export class WebsocketClient extends EventEmitter { // usdc options msg?.success === false ) { - return this.emit('errorEvent', msg); + return this.emit('errorEvent', { ...msg, wsKey }); } this.logger.warning('Unhandled/unrecognised ws event message', { @@ -781,6 +782,7 @@ export class WebsocketClient extends EventEmitter { */ public subscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + topics.forEach((topic) => this.wsStore.addTopic( getWsKeyForTopic(this.options.market, topic, isPrivateTopic), @@ -794,7 +796,9 @@ export class WebsocketClient extends EventEmitter { if ( this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) ) { - return this.requestSubscribeTopics(wsKey, topics); + return this.requestSubscribeTopics(wsKey, [ + ...this.wsStore.getTopics(wsKey), + ]); } // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect @@ -832,7 +836,9 @@ export class WebsocketClient extends EventEmitter { if ( this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) ) { - this.requestUnsubscribeTopics(wsKey, topics); + this.requestUnsubscribeTopics(wsKey, [ + ...this.wsStore.getTopics(wsKey), + ]); } }); } diff --git a/test/unified-margin/ws.public.option.test.ts b/test/unified-margin/ws.public.option.test.ts new file mode 100644 index 0000000..4c179c1 --- /dev/null +++ b/test/unified-margin/ws.public.option.test.ts @@ -0,0 +1,67 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + getSilentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Unified Margin Websocket Client (Options)', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'unifiedOption', + }; + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccessNoAuth') + ); + wsClient.connectPublic(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.unifiedOptionPublic, + }); + } catch (e) { + expect(e).toBeFalsy(); + } + }); + + it('should subscribe to public trade events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + wsClient.subscribe('orderbook.25.BTCUSDT'); + + try { + expect(await wsResponsePromise).toMatchObject({ + success: true, + type: 'COMMAND_RESP', + }); + } catch (e) { + // sub failed + expect(e).toBeFalsy(); + } + + // try { + // expect(await wsUpdatePromise).toStrictEqual('asdfasdf'); + // } catch (e) { + // // no data + // expect(e).toBeFalsy(); + // } + }); +}); diff --git a/test/unified-margin/ws.public.perp.usdt.test.ts b/test/unified-margin/ws.public.perp.usdt.test.ts new file mode 100644 index 0000000..98215d4 --- /dev/null +++ b/test/unified-margin/ws.public.perp.usdt.test.ts @@ -0,0 +1,83 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + getSilentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, + fullLogger, +} from '../ws.util'; + +describe('Public Unified Margin Websocket Client (Perps - USDT)', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'unifiedPerp', + }; + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccessNoAuth') + // fullLogger + ); + // logAllEvents(wsClient); + wsClient.connectPublic(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: expect.stringContaining('unifiedPerpUSD'), + }); + } catch (e) { + expect(e).toBeFalsy(); + } + }); + + it('should subscribe to public trade events through USDT topic', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + wsClient.subscribe('orderbook.25.BTCUSDT'); + + try { + expect(await wsResponsePromise).toMatchObject({ + op: 'subscribe', + req_id: 'orderbook.25.BTCUSDT', + success: true, + wsKey: WS_KEY_MAP.unifiedPerpUSDTPublic, + }); + } catch (e) { + // sub failed + expect(e).toBeFalsy(); + } + + try { + expect(await wsUpdatePromise).toMatchObject({ + data: { + a: expect.any(Array), + b: expect.any(Array), + s: 'BTCUSDT', + u: expect.any(Number), + }, + topic: 'orderbook.25.BTCUSDT', + ts: expect.any(Number), + type: 'snapshot', + wsKey: WS_KEY_MAP.unifiedPerpUSDTPublic, + }); + } catch (e) { + // no data + expect(e).toBeFalsy(); + } + }); +}); diff --git a/test/ws.util.ts b/test/ws.util.ts index 1fa49da..7d1423e 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -105,7 +105,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { export function logAllEvents(wsClient: WebsocketClient) { wsClient.on('update', (data) => { - console.log('wsUpdate: ', JSON.stringify(data, null, 2)); + // console.log('wsUpdate: ', JSON.stringify(data, null, 2)); }); wsClient.on('open', (data) => { From 4eca5fb1804585f4f5e7ccda515fa84a2060078b Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 19:56:24 +0100 Subject: [PATCH 51/74] tests for unified margin WS --- test/linear/ws.public.test.ts | 28 ------ test/unified-margin/ws.private.test.ts | 77 +++++++++++++++++ test/unified-margin/ws.public.option.test.ts | 2 +- .../ws.public.perp.usdc.test.ts | 85 +++++++++++++++++++ .../ws.public.perp.usdt.test.ts | 10 ++- 5 files changed, 169 insertions(+), 33 deletions(-) create mode 100644 test/unified-margin/ws.private.test.ts create mode 100644 test/unified-margin/ws.public.perp.usdc.test.ts diff --git a/test/linear/ws.public.test.ts b/test/linear/ws.public.test.ts index 6281c07..a29f159 100644 --- a/test/linear/ws.public.test.ts +++ b/test/linear/ws.public.test.ts @@ -23,7 +23,6 @@ describe('Public Linear Perps Websocket Client', () => { afterAll(() => { wsClient.closeAll(); - wsClient.removeAllListeners(); }); it('should open a public ws connection', async () => { @@ -65,31 +64,4 @@ describe('Public Linear Perps Websocket Client', () => { console.error(`Wait for "${wsTopic}" event exception: `, e); } }); - - it('should fail to subscribe to private events (no keys)', async () => { - const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); - // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); - - const wsTopic = 'wallet'; - - // No easy way to trigger a private event (other than executing trades) - // expect(wsUpdatePromise).resolves.toMatchObject({ - // topic: wsTopic, - // data: expect.any(Array), - // }); - - wsClient.subscribe(wsTopic); - - try { - expect(await wsResponsePromise).toBeFalsy(); - } catch (e) { - expect(e).toMatchObject({ - request: { - args: [wsTopic], - op: 'subscribe', - }, - success: false, - }); - } - }); }); diff --git a/test/unified-margin/ws.private.test.ts b/test/unified-margin/ws.private.test.ts new file mode 100644 index 0000000..72c16a1 --- /dev/null +++ b/test/unified-margin/ws.private.test.ts @@ -0,0 +1,77 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + getSilentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, + fullLogger, +} from '../ws.util'; + +describe('Private Unified Margin Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'unifiedPerp', + }; + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccessNoAuth') + // fullLogger + ); + // logAllEvents(wsClient); + wsClient.connectPrivate(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.unifiedPrivate, + }); + } catch (e) { + expect(e).toBeFalsy(); + } + }); + + // Should work, but don't have unified margin activated - needs extra testing + // {"conn_id": "064443fffef10442-0000798d-0000a9f6-ba32aeee49712540-1752c4f4", "ret_msg": "3303001", "success": false, "type": "COMMAND_RESP", "wsKey": "unifiedPrivate"} + + it.skip('should subscribe to public private unified account events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + // USDT should be detected and automatically routed through the USDT connection + const topic = 'user.position.unifiedAccount'; + wsClient.subscribe(topic); + + try { + expect(await wsResponsePromise).toMatchObject({ + op: 'subscribe', + req_id: topic, + success: true, + wsKey: WS_KEY_MAP.unifiedPrivate, + }); + } catch (e) { + // sub failed + expect(e).toBeFalsy(); + } + + try { + expect(await wsUpdatePromise).toStrictEqual(''); + } catch (e) { + // no data + expect(e).toBeFalsy(); + } + }); +}); diff --git a/test/unified-margin/ws.public.option.test.ts b/test/unified-margin/ws.public.option.test.ts index 4c179c1..cd05b9b 100644 --- a/test/unified-margin/ws.public.option.test.ts +++ b/test/unified-margin/ws.public.option.test.ts @@ -41,7 +41,7 @@ describe('Public Unified Margin Websocket Client (Options)', () => { } }); - it('should subscribe to public trade events', async () => { + it('should subscribe to public orderbook events', async () => { const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); diff --git a/test/unified-margin/ws.public.perp.usdc.test.ts b/test/unified-margin/ws.public.perp.usdc.test.ts new file mode 100644 index 0000000..1b526a3 --- /dev/null +++ b/test/unified-margin/ws.public.perp.usdc.test.ts @@ -0,0 +1,85 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + getSilentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, + fullLogger, +} from '../ws.util'; + +describe('Public Unified Margin Websocket Client (Perps - USDC)', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'unifiedPerp', + }; + + beforeAll(() => { + wsClient = new WebsocketClient( + wsClientOptions, + getSilentLogger('expectSuccessNoAuth') + // fullLogger + ); + // logAllEvents(wsClient); + wsClient.connectPublic(); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + try { + expect(await wsOpenPromise).toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: expect.stringContaining('unifiedPerpUSD'), + }); + } catch (e) { + expect(e).toBeFalsy(); + } + }); + + // TODO: are there USDC topics? This doesn't seem to work + it.skip('should subscribe to public orderbook events through USDC connection', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + // USDT should be detected and automatically routed through the USDT connection + wsClient.subscribe('orderbook.25.BTCUSDC'); + + try { + expect(await wsResponsePromise).toMatchObject({ + op: 'subscribe', + req_id: 'orderbook.25.BTCUSDC', + success: true, + wsKey: WS_KEY_MAP.unifiedPerpUSDTPublic, + }); + } catch (e) { + // sub failed + expect(e).toBeFalsy(); + } + + try { + expect(await wsUpdatePromise).toMatchObject({ + data: { + a: expect.any(Array), + b: expect.any(Array), + s: 'BTCUSDT', + u: expect.any(Number), + }, + topic: 'orderbook.25.BTCUSDC', + ts: expect.any(Number), + type: 'snapshot', + wsKey: WS_KEY_MAP.unifiedPerpUSDTPublic, + }); + } catch (e) { + // no data + expect(e).toBeFalsy(); + } + }); +}); diff --git a/test/unified-margin/ws.public.perp.usdt.test.ts b/test/unified-margin/ws.public.perp.usdt.test.ts index 98215d4..800c961 100644 --- a/test/unified-margin/ws.public.perp.usdt.test.ts +++ b/test/unified-margin/ws.public.perp.usdt.test.ts @@ -44,16 +44,18 @@ describe('Public Unified Margin Websocket Client (Perps - USDT)', () => { } }); - it('should subscribe to public trade events through USDT topic', async () => { + it('should subscribe to public orderbook events through USDT connection', async () => { const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); - wsClient.subscribe('orderbook.25.BTCUSDT'); + // USDT should be detected and automatically routed through the USDT connection + const topic = 'orderbook.25.BTCUSDT'; + wsClient.subscribe(topic); try { expect(await wsResponsePromise).toMatchObject({ op: 'subscribe', - req_id: 'orderbook.25.BTCUSDT', + req_id: topic, success: true, wsKey: WS_KEY_MAP.unifiedPerpUSDTPublic, }); @@ -70,7 +72,7 @@ describe('Public Unified Margin Websocket Client (Perps - USDT)', () => { s: 'BTCUSDT', u: expect.any(Number), }, - topic: 'orderbook.25.BTCUSDT', + topic: topic, ts: expect.any(Number), type: 'snapshot', wsKey: WS_KEY_MAP.unifiedPerpUSDTPublic, From 4ccaf853b6cf999b6e2b40ed6ed73df47ed88b9b Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 10:56:39 +0100 Subject: [PATCH 52/74] cleaning around reconnect workflow --- examples/ws-public.ts | 40 ++++++++++++++++++++++++++-------------- src/websocket-client.ts | 23 ++++++++++++++++------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/examples/ws-public.ts b/examples/ws-public.ts index 1313cd9..bfe8528 100644 --- a/examples/ws-public.ts +++ b/examples/ws-public.ts @@ -6,35 +6,41 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; (async () => { const logger = { ...DefaultLogger, - // silly: () => {}, + silly: (...params) => console.log('silly', ...params), }; const wsClient = new WebsocketClient( { // key: key, // secret: secret, - // market: 'linear', // market: 'inverse', + market: 'linear', // market: 'spot', - market: 'usdcOption', + // market: 'spotv3', + // market: 'usdcOption', + // market: 'usdcPerp', + // market: 'unifiedPerp', + // market: 'unifiedOption', }, logger ); wsClient.on('update', (data) => { - console.log('raw message received ', JSON.stringify(data, null, 2)); + console.log('raw message received ', JSON.stringify(data)); + // console.log('raw message received ', JSON.stringify(data, null, 2)); }); wsClient.on('open', (data) => { console.log('connection opened open:', data.wsKey); - if (data.wsKey === WS_KEY_MAP.spotPublic) { - // Spot public. - // wsClient.subscribePublicSpotTrades('BTCUSDT'); - // wsClient.subscribePublicSpotTradingPair('BTCUSDT'); - // wsClient.subscribePublicSpotV1Kline('BTCUSDT', '1m'); - // wsClient.subscribePublicSpotOrderbook('BTCUSDT', 'full'); - } + // if (data.wsKey === WS_KEY_MAP.spotPublic) { + // // Spot public, but not recommended - use spotv3 client instead + // // The old spot websockets dont automatically resubscribe if they disconnect + // // wsClient.subscribePublicSpotTrades('BTCUSDT'); + // // wsClient.subscribePublicSpotTradingPair('BTCUSDT'); + // // wsClient.subscribePublicSpotV1Kline('BTCUSDT', '1m'); + // // wsClient.subscribePublicSpotOrderbook('BTCUSDT', 'full'); + // } }); wsClient.on('response', (data) => { console.log('log response: ', JSON.stringify(data, null, 2)); @@ -52,10 +58,16 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; // Linear wsClient.subscribe('trade.BTCUSDT'); + // Spot V3 + // usdc options - wsClient.subscribe(`recenttrades.BTC`); - wsClient.subscribe(`recenttrades.ETH`); - wsClient.subscribe(`recenttrades.SOL`); + // wsClient.subscribe(`recenttrades.BTC`); + // wsClient.subscribe(`recenttrades.ETH`); + // wsClient.subscribe(`recenttrades.SOL`); + + // usdc perps + + // unified perps // setTimeout(() => { // console.log('unsubscribing'); diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 6049e23..80cba8a 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -188,18 +188,22 @@ export class WebsocketClient extends EventEmitter { return this.options.testnet === true; } - public close(wsKey: WsKey) { + public close(wsKey: WsKey, force?: boolean) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); this.clearTimers(wsKey); - this.getWs(wsKey)?.close(); + const ws = this.getWs(wsKey); + ws?.close(); + if (force) { + ws?.terminate(); + } } - public closeAll() { + public closeAll(force?: boolean) { const keys = this.wsStore.getKeys(); keys.forEach((key) => { - this.close(key); + this.close(key, force); }); } @@ -460,6 +464,10 @@ export class WebsocketClient extends EventEmitter { } private ping(wsKey: WsKey) { + if (this.wsStore.get(wsKey, true).activePongTimer) { + return; + } + this.clearPongTimer(wsKey); this.logger.silly('Sending ping', { ...loggerCategory, wsKey }); @@ -470,7 +478,8 @@ export class WebsocketClient extends EventEmitter { ...loggerCategory, wsKey, }); - this.getWs(wsKey)?.close(); + this.getWs(wsKey)?.terminate(); + delete this.wsStore.get(wsKey, true).activePongTimer; }, this.options.pongTimeout); } @@ -622,9 +631,9 @@ export class WebsocketClient extends EventEmitter { const msg = JSON.parse((event && event.data) || event); this.logger.silly('Received event', { - ...this.logger, + ...loggerCategory, wsKey, - msg: JSON.stringify(msg, null, 2), + msg: JSON.stringify(msg), }); // TODO: cleanme From 08674ab2fa01ab46712525d022da9f30a201fdd5 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:05:20 +0100 Subject: [PATCH 53/74] fix error log when reconnect cycle fails --- examples/ws-public.ts | 16 ++++++++++------ src/websocket-client.ts | 6 ++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/ws-public.ts b/examples/ws-public.ts index bfe8528..6f2ae90 100644 --- a/examples/ws-public.ts +++ b/examples/ws-public.ts @@ -13,12 +13,12 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; { // key: key, // secret: secret, + // market: 'linear', // market: 'inverse', - market: 'linear', // market: 'spot', // market: 'spotv3', // market: 'usdcOption', - // market: 'usdcPerp', + market: 'usdcPerp', // market: 'unifiedPerp', // market: 'unifiedOption', }, @@ -56,16 +56,20 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; // wsClient.subscribe('trade'); // Linear - wsClient.subscribe('trade.BTCUSDT'); + // wsClient.subscribe('trade.BTCUSDT'); // Spot V3 + // wsClient.subscribe('trade.BTCUSDT'); // usdc options - // wsClient.subscribe(`recenttrades.BTC`); - // wsClient.subscribe(`recenttrades.ETH`); - // wsClient.subscribe(`recenttrades.SOL`); + // wsClient.subscribe([ + // `recenttrades.BTC`, + // `recenttrades.ETH`, + // `recenttrades.SOL`, + // ]); // usdc perps + wsClient.subscribe('trade.BTCPERP'); // unified perps diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 80cba8a..4a0e52c 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -358,8 +358,10 @@ export class WebsocketClient extends EventEmitter { default: this.logger.error( - `{context} due to unexpected response error: ${error.msg}`, - { ...loggerCategory, wsKey } + `${context} due to unexpected response error: "${ + error?.msg || error?.message || error + }"`, + { ...loggerCategory, wsKey, error } ); break; } From 34884b236fb00227252c1d7bf777ad8dff38d9a5 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:13:48 +0100 Subject: [PATCH 54/74] detect pong for unified ws --- README.md | 13 +++++++++---- examples/ws-public.ts | 7 ++++--- src/util/requestUtils.ts | 4 ++++ src/util/websocket-util.ts | 3 +++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4594f5a..a834bb2 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ client.getOrderBook({ symbol: 'BTCUSD' }) ``` ## WebSockets -All API groups can be used via a shared `WebsocketClient`. However, make sure to make one instance of the WebsocketClient per API group (spot vs inverse vs linear vs linearfutures etc): +All API groups can be used via a shared `WebsocketClient`. However, to listen to multiple API groups at once, you will need to make one WebsocketClient instance per API group. The WebsocketClient can be configured to a specific API group using the market parameter. These are the currently available API groups: | API Category | Market | Description | @@ -182,9 +182,14 @@ const wsConfig = { // NOTE: to listen to multiple markets (spot vs inverse vs linear vs linearfutures) at once, make one WebsocketClient instance per market - // market: 'inverse' - // market: 'linear' - // market: 'spot' + market: 'linear', + // market: 'inverse', + // market: 'spot', + // market: 'spotv3', + // market: 'usdcOption', + // market: 'usdcPerp', + // market: 'unifiedPerp', + // market: 'unifiedOption', // how long to wait (in ms) before deciding the connection should be terminated & reconnected // pongTimeout: 1000, diff --git a/examples/ws-public.ts b/examples/ws-public.ts index 6f2ae90..8834d5a 100644 --- a/examples/ws-public.ts +++ b/examples/ws-public.ts @@ -18,8 +18,8 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; // market: 'spot', // market: 'spotv3', // market: 'usdcOption', - market: 'usdcPerp', - // market: 'unifiedPerp', + // market: 'usdcPerp', + market: 'unifiedPerp', // market: 'unifiedOption', }, logger @@ -69,9 +69,10 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src'; // ]); // usdc perps - wsClient.subscribe('trade.BTCPERP'); + // wsClient.subscribe('trade.BTCPERP'); // unified perps + wsClient.subscribe('publicTrade.BTCUSDT'); // setTimeout(() => { // console.log('unsubscribing'); diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index a17f752..fa52e5b 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -73,6 +73,10 @@ export function isWsPong(msg: any): boolean { return true; } + if (msg['ret_msg'] === 'pong') { + return true; + } + return ( msg.request && msg.request.op === 'ping' && diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index f4f2219..714aa6d 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -153,6 +153,9 @@ export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.spotV3Public, WS_KEY_MAP.usdcOptionPublic, WS_KEY_MAP.usdcPerpPublic, + WS_KEY_MAP.unifiedOptionPublic, + WS_KEY_MAP.unifiedPerpUSDTPublic, + WS_KEY_MAP.unifiedPerpUSDCPublic, ] as string[]; /** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */ From 8ec07c25525f61e0d5fcbe80a260c3a34aa5a80a Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:22:10 +0100 Subject: [PATCH 55/74] extract/clean inverse request types --- src/inverse-client.ts | 192 ++++++++++++----------------------- src/types/request/index.ts | 1 + src/types/request/inverse.ts | 135 ++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 125 deletions(-) create mode 100644 src/types/request/inverse.ts diff --git a/src/inverse-client.ts b/src/inverse-client.ts index 1d3354f..1f9ebee 100644 --- a/src/inverse-client.ts +++ b/src/inverse-client.ts @@ -3,6 +3,22 @@ import { APIResponseWithTime, AssetExchangeRecordsReq, CoinParam, + InverseActiveConditionalOrderRequest, + InverseActiveOrdersRequest, + InverseCancelConditionalOrderRequest, + InverseCancelOrderRequest, + InverseChangePositionMarginRequest, + InverseConditionalOrderRequest, + InverseGetClosedPnlRequest, + InverseGetOrderRequest, + InverseGetTradeRecordsRequest, + InverseOrderRequest, + InverseReplaceConditionalOrderRequest, + InverseReplaceOrderRequest, + InverseSetLeverageRequest, + InverseSetMarginTypeRequest, + InverseSetSlTpPositionModeRequest, + InverseSetTradingStopRequest, SymbolInfo, SymbolIntervalFromLimitParam, SymbolLimitParam, @@ -165,39 +181,21 @@ export class InverseClient extends BaseRestClient { * Active orders */ - placeActiveOrder(orderRequest: { - side: string; - symbol: string; - order_type: string; - qty: number; - price?: number; - time_in_force: string; - take_profit?: number; - stop_loss?: number; - reduce_only?: boolean; - tp_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice'; - sl_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice'; - close_on_trigger?: boolean; - order_link_id?: string; - }): Promise> { + placeActiveOrder( + orderRequest: InverseOrderRequest + ): Promise> { return this.postPrivate('v2/private/order/create', orderRequest); } - getActiveOrderList(params: { - symbol: string; - order_status?: string; - direction?: string; - limit?: number; - cursor?: string; - }): Promise> { + getActiveOrderList( + params: InverseActiveOrdersRequest + ): Promise> { return this.getPrivate('v2/private/order/list', params); } - cancelActiveOrder(params: { - symbol: string; - order_id?: string; - order_link_id?: string; - }): Promise> { + cancelActiveOrder( + params: InverseCancelOrderRequest + ): Promise> { return this.postPrivate('v2/private/order/cancel', params); } @@ -207,25 +205,15 @@ export class InverseClient extends BaseRestClient { return this.postPrivate('v2/private/order/cancelAll', params); } - replaceActiveOrder(params: { - order_id?: string; - order_link_id?: string; - symbol: string; - p_r_qty?: number; - p_r_price?: string; - take_profit?: number; - stop_loss?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - }): Promise> { + replaceActiveOrder( + params: InverseReplaceOrderRequest + ): Promise> { return this.postPrivate('v2/private/order/replace', params); } - queryActiveOrder(params: { - order_id?: string; - order_link_id?: string; - symbol: string; - }): Promise> { + queryActiveOrder( + params: InverseGetOrderRequest + ): Promise> { return this.getPrivate('v2/private/order', params); } @@ -233,38 +221,22 @@ export class InverseClient extends BaseRestClient { * Conditional orders */ - placeConditionalOrder(params: { - side: string; - symbol: string; - order_type: string; - qty: string; - price?: string; - base_price: string; - stop_px: string; - time_in_force: string; - trigger_by?: string; - close_on_trigger?: boolean; - order_link_id?: string; - }): Promise> { + placeConditionalOrder( + params: InverseConditionalOrderRequest + ): Promise> { return this.postPrivate('v2/private/stop-order/create', params); } /** get conditional order list. This may see delays, use queryConditionalOrder() for real-time queries */ - getConditionalOrder(params: { - symbol: string; - stop_order_status?: string; - direction?: string; - limit?: number; - cursor?: string; - }): Promise> { + getConditionalOrder( + params: InverseActiveConditionalOrderRequest + ): Promise> { return this.getPrivate('v2/private/stop-order/list', params); } - cancelConditionalOrder(params: { - symbol: string; - stop_order_id?: string; - order_link_id?: string; - }): Promise> { + cancelConditionalOrder( + params: InverseCancelConditionalOrderRequest + ): Promise> { return this.postPrivate('v2/private/stop-order/cancel', params); } @@ -274,22 +246,15 @@ export class InverseClient extends BaseRestClient { return this.postPrivate('v2/private/stop-order/cancelAll', params); } - replaceConditionalOrder(params: { - stop_order_id?: string; - order_link_id?: string; - symbol: string; - p_r_qty?: number; - p_r_price?: string; - p_r_trigger_price?: string; - }): Promise> { + replaceConditionalOrder( + params: InverseReplaceConditionalOrderRequest + ): Promise> { return this.postPrivate('v2/private/stop-order/replace', params); } - queryConditionalOrder(params: { - symbol: string; - stop_order_id?: string; - order_link_id?: string; - }): Promise> { + queryConditionalOrder( + params: InverseGetOrderRequest + ): Promise> { return this.getPrivate('v2/private/stop-order', params); } @@ -303,68 +268,45 @@ export class InverseClient extends BaseRestClient { return this.getPrivate('v2/private/position/list', params); } - changePositionMargin(params: { - symbol: string; - margin: string; - }): Promise> { + changePositionMargin( + params: InverseChangePositionMarginRequest + ): Promise> { return this.postPrivate('position/change-position-margin', params); } - setTradingStop(params: { - symbol: string; - take_profit?: number; - stop_loss?: number; - trailing_stop?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - new_trailing_active?: number; - }): Promise> { + setTradingStop( + params: InverseSetTradingStopRequest + ): Promise> { return this.postPrivate('v2/private/position/trading-stop', params); } - setUserLeverage(params: { - symbol: string; - leverage: number; - leverage_only?: boolean; - }): Promise> { + setUserLeverage( + params: InverseSetLeverageRequest + ): Promise> { return this.postPrivate('v2/private/position/leverage/save', params); } - getTradeRecords(params: { - order_id?: string; - symbol: string; - start_time?: number; - page?: number; - limit?: number; - order?: string; - }): Promise> { + getTradeRecords( + params: InverseGetTradeRecordsRequest + ): Promise> { return this.getPrivate('v2/private/execution/list', params); } - getClosedPnl(params: { - symbol: string; - start_time?: number; - end_time?: number; - exec_type?: string; - page?: number; - limit?: number; - }): Promise> { + getClosedPnl( + params: InverseGetClosedPnlRequest + ): Promise> { return this.getPrivate('v2/private/trade/closed-pnl/list', params); } - setSlTpPositionMode(params: { - symbol: string; - tp_sl_mode: 'Full' | 'Partial'; - }): Promise> { + setSlTpPositionMode( + params: InverseSetSlTpPositionModeRequest + ): Promise> { return this.postPrivate('v2/private/tpsl/switch-mode', params); } - setMarginType(params: { - symbol: string; - is_isolated: boolean; - buy_leverage: number; - sell_leverage: number; - }): Promise> { + setMarginType( + params: InverseSetMarginTypeRequest + ): Promise> { return this.postPrivate('v2/private/position/switch-isolated', params); } diff --git a/src/types/request/index.ts b/src/types/request/index.ts index 8ee5bfd..88721cd 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,5 +1,6 @@ export * from './account-asset'; export * from './copy-trading'; +export * from './inverse'; export * from './spot'; export * from './usdt-perp'; export * from './usdc-perp'; diff --git a/src/types/request/inverse.ts b/src/types/request/inverse.ts new file mode 100644 index 0000000..c3e453c --- /dev/null +++ b/src/types/request/inverse.ts @@ -0,0 +1,135 @@ +export interface InverseOrderRequest { + side: string; + symbol: string; + order_type: string; + qty: number; + price?: number; + time_in_force: string; + take_profit?: number; + stop_loss?: number; + reduce_only?: boolean; + tp_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice'; + sl_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice'; + close_on_trigger?: boolean; + order_link_id?: string; +} + +export interface InverseActiveOrdersRequest { + symbol: string; + order_status?: string; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface InverseCancelOrderRequest { + symbol: string; + order_id?: string; + order_link_id?: string; +} + +export interface InverseReplaceOrderRequest { + order_id?: string; + order_link_id?: string; + symbol: string; + p_r_qty?: number; + p_r_price?: string; + take_profit?: number; + stop_loss?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; +} + +export interface InverseGetOrderRequest { + order_id?: string; + order_link_id?: string; + symbol: string; +} + +export interface InverseConditionalOrderRequest { + side: string; + symbol: string; + order_type: string; + qty: string; + price?: string; + base_price: string; + stop_px: string; + time_in_force: string; + trigger_by?: string; + close_on_trigger?: boolean; + order_link_id?: string; +} + +export interface InverseActiveConditionalOrderRequest { + symbol: string; + stop_order_status?: string; + direction?: string; + limit?: number; + cursor?: string; +} + +export interface InverseCancelConditionalOrderRequest { + symbol: string; + stop_order_id?: string; + order_link_id?: string; +} + +export interface InverseReplaceConditionalOrderRequest { + stop_order_id?: string; + order_link_id?: string; + symbol: string; + p_r_qty?: number; + p_r_price?: string; + p_r_trigger_price?: string; +} + +export interface InverseChangePositionMarginRequest { + symbol: string; + margin: string; +} + +export interface InverseSetTradingStopRequest { + symbol: string; + take_profit?: number; + stop_loss?: number; + trailing_stop?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; + new_trailing_active?: number; +} + +export interface InverseSetLeverageRequest { + symbol: string; + leverage: number; + leverage_only?: boolean; +} + +export interface InverseGetTradeRecordsRequest { + order_id?: string; + symbol: string; + start_time?: number; + page?: number; + limit?: number; + order?: string; +} + +export interface InverseGetClosedPnlRequest { + symbol: string; + start_time?: number; + end_time?: number; + exec_type?: string; + page?: number; + limit?: number; +} + +export interface InverseSetSlTpPositionModeRequest { + symbol: string; + tp_sl_mode: 'Full' | 'Partial'; +} + +export interface InverseSetMarginTypeRequest { + symbol: string; + is_isolated: boolean; + buy_leverage: number; + sell_leverage: number; +} From c02a2f0c759882664dbea545e3d7fa451b1abfb7 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:33:49 +0100 Subject: [PATCH 56/74] clean extract linear req types --- src/linear-client.ts | 216 ++++++++++++--------------------- src/types/request/index.ts | 1 + src/types/request/linear.ts | 190 +++++++++++++++++++++++++++++ src/types/request/usdt-perp.ts | 27 ----- 4 files changed, 267 insertions(+), 167 deletions(-) create mode 100644 src/types/request/linear.ts delete mode 100644 src/types/request/usdt-perp.ts diff --git a/src/linear-client.ts b/src/linear-client.ts index 32b6754..ca8e52c 100644 --- a/src/linear-client.ts +++ b/src/linear-client.ts @@ -4,7 +4,26 @@ import { APIResponseWithTime, AssetExchangeRecordsReq, CoinParam, + LinearCancelConditionalOrderRequest, + LinearCancelOrderRequest, + LinearConditionalOrderRequest, + LinearGetClosedPnlRequest, + LinearGetConditionalOrderRequest, + LinearGetOrderRequest, + LinearGetOrdersRequest, + LinearGetTradeRecordsRequest, LinearOrder, + LinearQueryConditionalOrderRequest, + LinearReplaceConditionalOrderRequest, + LinearReplaceOrderRequest, + LinearSetAddReduceMarginRequest, + LinearSetAutoAddMarginRequest, + LinearSetMarginSwitchRequest, + LinearSetPositionModeRequest, + LinearSetPositionTpSlModeRequest, + LinearSetRiskLimitRequest, + LinearSetTradingStopRequest, + LinearSetUserLeverageRequest, NewLinearOrder, PerpPosition, PerpPositionRoot, @@ -178,23 +197,15 @@ export class LinearClient extends BaseRestClient { return this.postPrivate('private/linear/order/create', params); } - getActiveOrderList(params: { - order_id?: string; - order_link_id?: string; - symbol: string; - order?: string; - page?: number; - limit?: number; - order_status?: string; - }): Promise> { + getActiveOrderList( + params: LinearGetOrdersRequest + ): Promise> { return this.getPrivate('private/linear/order/list', params); } - cancelActiveOrder(params: { - symbol: string; - order_id?: string; - order_link_id?: string; - }): Promise> { + cancelActiveOrder( + params: LinearCancelOrderRequest + ): Promise> { return this.postPrivate('private/linear/order/cancel', params); } @@ -204,25 +215,15 @@ export class LinearClient extends BaseRestClient { return this.postPrivate('private/linear/order/cancel-all', params); } - replaceActiveOrder(params: { - order_id?: string; - order_link_id?: string; - symbol: string; - p_r_qty?: number; - p_r_price?: number; - take_profit?: number; - stop_loss?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - }): Promise> { + replaceActiveOrder( + params: LinearReplaceOrderRequest + ): Promise> { return this.postPrivate('private/linear/order/replace', params); } - queryActiveOrder(params: { - order_id?: string; - order_link_id?: string; - symbol: string; - }): Promise> { + queryActiveOrder( + params: LinearGetOrderRequest + ): Promise> { return this.getPrivate('private/linear/order/search', params); } @@ -230,44 +231,21 @@ export class LinearClient extends BaseRestClient { * Conditional orders */ - placeConditionalOrder(params: { - side: string; - symbol: string; - order_type: string; - qty: number; - price?: number; - base_price: number; - stop_px: number; - time_in_force: string; - trigger_by?: string; - close_on_trigger?: boolean; - order_link_id?: string; - reduce_only: boolean; - take_profit?: number; - stop_loss?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - }): Promise> { + placeConditionalOrder( + params: LinearConditionalOrderRequest + ): Promise> { return this.postPrivate('private/linear/stop-order/create', params); } - getConditionalOrder(params: { - stop_order_id?: string; - order_link_id?: string; - symbol: string; - stop_order_status?: string; - order?: string; - page?: number; - limit?: number; - }): Promise> { + getConditionalOrder( + params: LinearGetConditionalOrderRequest + ): Promise> { return this.getPrivate('private/linear/stop-order/list', params); } - cancelConditionalOrder(params: { - symbol: string; - stop_order_id?: string; - order_link_id?: string; - }): Promise> { + cancelConditionalOrder( + params: LinearCancelConditionalOrderRequest + ): Promise> { return this.postPrivate('private/linear/stop-order/cancel', params); } @@ -277,26 +255,15 @@ export class LinearClient extends BaseRestClient { return this.postPrivate('private/linear/stop-order/cancel-all', params); } - replaceConditionalOrder(params: { - stop_order_id?: string; - order_link_id?: string; - symbol: string; - p_r_qty?: number; - p_r_price?: number; - p_r_trigger_price?: number; - take_profit?: number; - stop_loss?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - }): Promise> { + replaceConditionalOrder( + params: LinearReplaceConditionalOrderRequest + ): Promise> { return this.postPrivate('private/linear/stop-order/replace', params); } - queryConditionalOrder(params: { - symbol: string; - stop_order_id?: string; - order_link_id?: string; - }): Promise> { + queryConditionalOrder( + params: LinearQueryConditionalOrderRequest + ): Promise> { return this.getPrivate('private/linear/stop-order/search', params); } @@ -315,33 +282,27 @@ export class LinearClient extends BaseRestClient { return this.getPrivate('private/linear/position/list', params); } - setAutoAddMargin(params?: { - symbol: string; - side: string; - auto_add_margin: boolean; - }): Promise> { + setAutoAddMargin( + params?: LinearSetAutoAddMarginRequest + ): Promise> { return this.postPrivate( 'private/linear/position/set-auto-add-margin', params ); } - setMarginSwitch(params?: { - symbol: string; - is_isolated: boolean; - buy_leverage: number; - sell_leverage: number; - }): Promise> { + setMarginSwitch( + params?: LinearSetMarginSwitchRequest + ): Promise> { return this.postPrivate('private/linear/position/switch-isolated', params); } /** * Switch between one-way vs hedge mode. Use `linearPositionModeEnum` for the mode parameter. */ - setPositionMode(params: { - symbol: string; - mode: typeof linearPositionModeEnum[keyof typeof linearPositionModeEnum]; - }): Promise> { + setPositionMode( + params: LinearSetPositionModeRequest + ): Promise> { return this.postPrivate('private/linear/position/switch-mode', params); } @@ -349,62 +310,39 @@ export class LinearClient extends BaseRestClient { * Switch TP/SL mode between full or partial. When set to Partial, TP/SL orders may have a quantity less than the position size. * This is set with the setTradingStop() method. Use `positionTpSlModeEnum` for the tp_sl_mode parameter. */ - setPositionTpSlMode(params: { - symbol: string; - tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum]; - }): Promise> { + setPositionTpSlMode( + params: LinearSetPositionTpSlModeRequest + ): Promise> { return this.postPrivate('private/linear/tpsl/switch-mode', params); } - setAddReduceMargin(params?: { - symbol: string; - side: string; - margin: number; - }): Promise> { + setAddReduceMargin( + params?: LinearSetAddReduceMarginRequest + ): Promise> { return this.postPrivate('private/linear/position/add-margin', params); } - setUserLeverage(params: { - symbol: string; - buy_leverage: number; - sell_leverage: number; - }): Promise> { + setUserLeverage( + params: LinearSetUserLeverageRequest + ): Promise> { return this.postPrivate('private/linear/position/set-leverage', params); } - setTradingStop(params: { - symbol: string; - side: string; - take_profit?: number; - stop_loss?: number; - trailing_stop?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - sl_size?: number; - tp_size?: number; - }): Promise> { + setTradingStop( + params: LinearSetTradingStopRequest + ): Promise> { return this.postPrivate('private/linear/position/trading-stop', params); } - getTradeRecords(params: { - symbol: string; - start_time?: number; - end_time?: number; - exec_type?: string; - page?: number; - limit?: number; - }): Promise> { + getTradeRecords( + params: LinearGetTradeRecordsRequest + ): Promise> { return this.getPrivate('private/linear/trade/execution/list', params); } - getClosedPnl(params: { - symbol: string; - start_time?: number; - end_time?: number; - exec_type?: string; - page?: number; - limit?: number; - }): Promise> { + getClosedPnl( + params: LinearGetClosedPnlRequest + ): Promise> { return this.getPrivate('private/linear/trade/closed-pnl/list', params); } @@ -416,11 +354,9 @@ export class LinearClient extends BaseRestClient { return this.getPrivate('public/linear/risk-limit', params); } - setRiskLimit(params: { - symbol: string; - side: string; - risk_id: number; - }): Promise> { + setRiskLimit( + params: LinearSetRiskLimitRequest + ): Promise> { return this.postPrivate('private/linear/position/set-risk', params); } diff --git a/src/types/request/index.ts b/src/types/request/index.ts index 88721cd..de89375 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,5 +1,6 @@ export * from './account-asset'; export * from './copy-trading'; +export * from './linear'; export * from './inverse'; export * from './spot'; export * from './usdt-perp'; diff --git a/src/types/request/linear.ts b/src/types/request/linear.ts new file mode 100644 index 0000000..6901a24 --- /dev/null +++ b/src/types/request/linear.ts @@ -0,0 +1,190 @@ +import { + LinearPositionIdx, + linearPositionModeEnum, + positionTpSlModeEnum, +} from '../../constants/enum'; +import { OrderSide } from '../shared'; + +export interface LinearGetOrdersRequest { + order_id?: string; + order_link_id?: string; + symbol: string; + order?: string; + page?: number; + limit?: number; + order_status?: string; +} + +export interface LinearCancelOrderRequest { + symbol: string; + order_id?: string; + order_link_id?: string; +} + +export interface LinearReplaceOrderRequest { + order_id?: string; + order_link_id?: string; + symbol: string; + p_r_qty?: number; + p_r_price?: number; + take_profit?: number; + stop_loss?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; +} + +export interface LinearGetOrderRequest { + order_id?: string; + order_link_id?: string; + symbol: string; +} + +export type LinearOrderType = 'Limit' | 'Market'; + +export type LinearTimeInForce = + | 'GoodTillCancel' + | 'ImmediateOrCancel' + | 'FillOrKill' + | 'PostOnly'; + +export interface NewLinearOrder { + side: OrderSide; + symbol: string; + order_type: LinearOrderType; + qty: number; + price?: number; + time_in_force: LinearTimeInForce; + take_profit?: number; + stop_loss?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; + reduce_only: boolean; + close_on_trigger: boolean; + order_link_id?: string; + position_idx?: LinearPositionIdx; +} + +export interface LinearConditionalOrderRequest { + side: string; + symbol: string; + order_type: string; + qty: number; + price?: number; + base_price: number; + stop_px: number; + time_in_force: string; + trigger_by?: string; + close_on_trigger?: boolean; + order_link_id?: string; + reduce_only: boolean; + take_profit?: number; + stop_loss?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; +} + +export interface LinearGetConditionalOrderRequest { + stop_order_id?: string; + order_link_id?: string; + symbol: string; + stop_order_status?: string; + order?: string; + page?: number; + limit?: number; +} + +export interface LinearCancelConditionalOrderRequest { + symbol: string; + stop_order_id?: string; + order_link_id?: string; +} + +export interface LinearReplaceConditionalOrderRequest { + stop_order_id?: string; + order_link_id?: string; + symbol: string; + p_r_qty?: number; + p_r_price?: number; + p_r_trigger_price?: number; + take_profit?: number; + stop_loss?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; +} + +export interface LinearQueryConditionalOrderRequest { + symbol: string; + stop_order_id?: string; + order_link_id?: string; +} + +export interface LinearSetAutoAddMarginRequest { + symbol: string; + side: string; + auto_add_margin: boolean; +} + +export interface LinearSetMarginSwitchRequest { + symbol: string; + is_isolated: boolean; + buy_leverage: number; + sell_leverage: number; +} + +export interface LinearSetPositionModeRequest { + symbol: string; + mode: typeof linearPositionModeEnum[keyof typeof linearPositionModeEnum]; +} + +export interface LinearSetPositionTpSlModeRequest { + symbol: string; + tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum]; +} + +export interface LinearSetAddReduceMarginRequest { + symbol: string; + side: string; + margin: number; +} + +export interface LinearSetUserLeverageRequest { + symbol: string; + buy_leverage: number; + sell_leverage: number; +} + +export interface LinearSetTradingStopRequest { + symbol: string; + side: string; + take_profit?: number; + stop_loss?: number; + trailing_stop?: number; + tp_trigger_by?: string; + sl_trigger_by?: string; + sl_size?: number; + tp_size?: number; +} + +export interface LinearGetTradeRecordsRequest { + symbol: string; + start_time?: number; + end_time?: number; + exec_type?: string; + page?: number; + limit?: number; +} + +export interface LinearGetClosedPnlRequest { + symbol: string; + start_time?: number; + end_time?: number; + exec_type?: string; + page?: number; + limit?: number; +} + +export interface LinearSetRiskLimitRequest { + symbol: string; + side: string; + risk_id: number; +} diff --git a/src/types/request/usdt-perp.ts b/src/types/request/usdt-perp.ts deleted file mode 100644 index f9bf6e7..0000000 --- a/src/types/request/usdt-perp.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { LinearPositionIdx } from '../../constants/enum'; -import { OrderSide } from '../shared'; - -export type LinearOrderType = 'Limit' | 'Market'; - -export type LinearTimeInForce = - | 'GoodTillCancel' - | 'ImmediateOrCancel' - | 'FillOrKill' - | 'PostOnly'; - -export interface NewLinearOrder { - side: OrderSide; - symbol: string; - order_type: LinearOrderType; - qty: number; - price?: number; - time_in_force: LinearTimeInForce; - take_profit?: number; - stop_loss?: number; - tp_trigger_by?: string; - sl_trigger_by?: string; - reduce_only: boolean; - close_on_trigger: boolean; - order_link_id?: string; - position_idx?: LinearPositionIdx; -} From 679e54eb42a2e0673e3a8444ae9a3e1e3050d623 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:36:57 +0100 Subject: [PATCH 57/74] cleaning in spot client --- src/spot-client-v3.ts | 9 ++++----- src/types/request/spot.ts | 6 ++++++ src/util/BaseRestClient.ts | 4 ++-- src/util/requestUtils.ts | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/spot-client-v3.ts b/src/spot-client-v3.ts index 33719a9..7624204 100644 --- a/src/spot-client-v3.ts +++ b/src/spot-client-v3.ts @@ -11,6 +11,7 @@ import { SpotLeveragedTokenPRHistoryRequest, SpotCrossMarginBorrowingInfoRequest, SpotCrossMarginRepaymentHistoryRequest, + SpotCancelOrderBatchRequest, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -119,11 +120,9 @@ export class SpotClientV3 extends BaseRestClient { } /** Batch cancel orders */ - cancelOrderBatch(params: { - symbol: string; - side?: OrderSide; - orderTypes: OrderTypeSpot[]; - }): Promise> { + cancelOrderBatch( + params: SpotCancelOrderBatchRequest + ): Promise> { const orderTypes = params.orderTypes ? params.orderTypes.join(',') : undefined; diff --git a/src/types/request/spot.ts b/src/types/request/spot.ts index fc589ea..3c0c20f 100644 --- a/src/types/request/spot.ts +++ b/src/types/request/spot.ts @@ -25,6 +25,12 @@ export interface NewSpotOrderV3 { triggerPrice?: string; } +export interface SpotCancelOrderBatchRequest { + symbol: string; + side?: OrderSide; + orderTypes: OrderTypeSpot[]; +} + export interface SpotOrderQueryById { orderId?: string; orderLinkId?: string; diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index b5a7a13..9141dab 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -6,7 +6,7 @@ import { serializeParams, RestClientType, REST_CLIENT_TYPE_ENUM, - agentSource, + APIID, getRestBaseUrl, } from './requestUtils'; @@ -98,7 +98,7 @@ export default abstract class BaseRestClient { // custom request options based on axios specs - see: https://github.com/axios/axios#request-config ...requestOptions, headers: { - 'x-referer': agentSource, + 'x-referer': APIID, }, }; diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index fa52e5b..fde95a0 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -85,7 +85,7 @@ export function isWsPong(msg: any): boolean { ); } -export const agentSource = 'bybitapinode'; +export const APIID = 'bybitapinode'; /** * Used to switch how authentication/requests work under the hood (primarily for SPOT since it's different there) From 63547848c971ad7f247297ceb151e1f67fa66ce7 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:40:05 +0100 Subject: [PATCH 58/74] readme phrasing --- README.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a834bb2..de793b1 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ Most methods accept JS objects. These can be populated using parameters specifie - [Bybit API Docs (choose API category from the tabs at the top)](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-introduction). ## Structure -The connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bybit-api). +This connector is fully compatible with both TypeScript and pure JavaScript projects, while the connector is written in TypeScript. A pure JavaScript version can be built using `npm run build`, which is also the version published to [npm](https://www.npmjs.com/package/bybit-api). -This connector is fully compatible with both TypeScript and pure JavaScript projects. The version on npm is the output from the `build` command and can be used in projects without TypeScript (although TypeScript is definitely recommended). +The version on npm is the output from the `build` command and can be used in projects without TypeScript (although TypeScript is definitely recommended). - [src](./src) - the whole connector written in TypeScript - [lib](./lib) - the JavaScript version of the project (built from TypeScript). This should not be edited directly, as it will be overwritten with each release. diff --git a/package.json b/package.json index badb8e5..8eaf14a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "2.4.0-beta.2", + "version": "3.0.0", "description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.", "main": "lib/index.js", "types": "lib/index.d.ts", From 2d0bc66b51e7b9bbb424cffbb5bbaddd75481c73 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:41:05 +0100 Subject: [PATCH 59/74] readme tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de793b1..484a2df 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The version on npm is the output from the `build` command and can be used in pro - [src](./src) - the whole connector written in TypeScript - [lib](./lib) - the JavaScript version of the project (built from TypeScript). This should not be edited directly, as it will be overwritten with each release. -- [dist](./dist) - the web-packed bundle of the project for use in browser environments. +- [dist](./dist) - the webpack bundle of the project for use in browser environments (see guidance on webpack below). - [examples](./examples) - some implementation examples & demonstrations. Contributions are welcome! --- From c7d3885fe2056d714bcc79ba619279f3c2355802 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:45:18 +0100 Subject: [PATCH 60/74] readme cleaning --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 484a2df..c232dda 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Each REST API group has a dedicated REST client. To avoid confusion, here are th | [LinearClient](src/linear-client.ts) | [USDT Perpetual Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-introduction) | | [InverseFuturesClient](src/inverse-futures-client.ts) | [Inverse Futures (v2) APIs](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-introduction) | | [USDCPerpetualClient](src/usdc-perpetual-client.ts) | [USDC Perpetual APIs](https://bybit-exchange.github.io/docs/usdc/option/?console#t-querydeliverylog) | -| [UnifiedMarginClient](src/unified-margin-client.ts) | [Derivatives (v3) unified margin APIs](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-introduction) | | [USDCOptionClient](src/usdc-option-client.ts) | [USDC Option APIs](https://bybit-exchange.github.io/docs/usdc/option/#t-introduction) | +| [UnifiedMarginClient](src/unified-margin-client.ts) | [Derivatives (v3) unified margin APIs](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-introduction) | | [SpotClientV3](src/spot-client-v3.ts) | [Spot Market (v3) APIs](https://bybit-exchange.github.io/docs/spot/v3/#t-introduction) | | [~SpotClient~](src/spot-client.ts) (deprecated, v3 client recommended)| [Spot Market (v1) APIs](https://bybit-exchange.github.io/docs/spot/v1/#t-introduction) | | [AccountAssetClient](src/account-asset-client.ts) | [Account Asset APIs](https://bybit-exchange.github.io/docs/account_asset/#t-introduction) | @@ -86,25 +86,23 @@ const { LinearClient, InverseFuturesClient, SpotClient, - SpotClient3, + SpotClientV3, + UnifiedMarginClient, USDCOptionClient, USDCPerpetualClient, - CopyTradingClient, AccountAssetClient, + CopyTradingClient, } = require('bybit-api'); const restClientOptions = { // override the max size of the request window (in ms) recv_window?: number; - // how often to sync time drift with bybit servers - sync_interval_ms?: number; - - // Default: false. Disable above sync mechanism if true. + // Disabled by default, not recommended unless you know what you're doing enable_time_sync?: boolean; - // Default: false. If true, we'll throw errors if any params are undefined - strict_param_validation?: boolean; + // how often to sync time drift with bybit servers + sync_interval_ms?: number; // Optionally override API protocol + domain // e.g 'https://api.bytick.com' @@ -119,16 +117,20 @@ const PRIVATE_KEY = 'yyy'; const useLivenet = false; const client = new InverseClient( + // Optional, unless you plan on making private API calls API_KEY, PRIVATE_KEY, - // optional, uses testnet by default. Set to 'true' to use livenet. + // Optional, uses testnet by default. Set to 'true' to use livenet. useLivenet, // restClientOptions, // requestLibraryOptions ); +// For public-only API calls, simply set key and secret to "undefined": +// const client = new InverseClient(undefined, undefined, useLivenet); + client.getApiKeyInfo() .then(result => { console.log("getApiKeyInfo result: ", result); @@ -152,8 +154,8 @@ All API groups can be used via a shared `WebsocketClient`. However, to listen to The WebsocketClient can be configured to a specific API group using the market parameter. These are the currently available API groups: | API Category | Market | Description | |:----------------------------: |:-------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | -| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | +| Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. \nNote: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | +| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. \nNote: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | | Futures v2 - Inverse Perps | `market: 'inverse'` | The [inverse v2 perps](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-websocket) category. | | Futures v2 - USDT Perps | `market: 'linear'` | The [USDT/linear v2 perps](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-websocket) category. | | Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | From 5297eaa913fb63b6cc843463ac1c8b568e2f63e2 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:46:13 +0100 Subject: [PATCH 61/74] remove nl --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c232dda..3288ac1 100644 --- a/README.md +++ b/README.md @@ -154,8 +154,8 @@ All API groups can be used via a shared `WebsocketClient`. However, to listen to The WebsocketClient can be configured to a specific API group using the market parameter. These are the currently available API groups: | API Category | Market | Description | |:----------------------------: |:-------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. \nNote: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | -| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. \nNote: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | +| Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | +| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | | Futures v2 - Inverse Perps | `market: 'inverse'` | The [inverse v2 perps](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-websocket) category. | | Futures v2 - USDT Perps | `market: 'linear'` | The [USDT/linear v2 perps](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-websocket) category. | | Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | From f8273181233ec6404b5e20594173002888a83113 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:48:41 +0100 Subject: [PATCH 62/74] readme fix --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3288ac1..8727a4d 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The WebsocketClient can be configured to a specific API group using the market p | API Category | Market | Description | |:----------------------------: |:-------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | -| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | +| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only support USDT/USDC perpetual topics - use `unifiedOption` if you need public options topics. | | Futures v2 - Inverse Perps | `market: 'inverse'` | The [inverse v2 perps](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-websocket) category. | | Futures v2 - USDT Perps | `market: 'linear'` | The [USDT/linear v2 perps](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-websocket) category. | | Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | @@ -258,11 +258,14 @@ Pass a custom logger which supports the log methods `silly`, `debug`, `notice`, const { WebsocketClient, DefaultLogger } = require('bybit-api'); // Disable all logging on the silly level -DefaultLogger.silly = () => {}; +const customLogger = { + ...DefaultLogger, + silly: () => {}, +}; const ws = new WebsocketClient( { key: 'xxx', secret: 'yyy' }, - DefaultLogger + customLogger ); ``` From 296262f4fb0a47029590b89c76cdd2cf2c379b65 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sat, 17 Sep 2022 11:50:39 +0100 Subject: [PATCH 63/74] ws private clean --- examples/ws-private.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/ws-private.ts b/examples/ws-private.ts index 59623f3..14bd950 100644 --- a/examples/ws-private.ts +++ b/examples/ws-private.ts @@ -1,13 +1,12 @@ -import { DefaultLogger } from '../src'; -import { WebsocketClient } from '../src/websocket-client'; +import { WebsocketClient, WS_KEY_MAP, DefaultLogger } from '../src'; // or -// import { DefaultLogger, WebsocketClient } from 'bybit-api'; +// import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from 'bybit-api'; (async () => { const logger = { ...DefaultLogger, - // silly: () => {}, + silly: () => {}, }; const key = process.env.API_KEY; @@ -25,7 +24,7 @@ import { WebsocketClient } from '../src/websocket-client'; key: key, secret: secret, market: market, - livenet: true, + // testnet: true, restOptions: { // enable_time_sync: true, }, @@ -33,8 +32,6 @@ import { WebsocketClient } from '../src/websocket-client'; logger ); - // wsClient.subscribePublicSpotOrderbook('test', 'full'); - wsClient.on('update', (data) => { console.log('raw message received ', JSON.stringify(data, null, 2)); }); From 5894e45393b7f419bbf31468d4eb13480d5d2e6c Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Sun, 18 Sep 2022 10:57:15 +0100 Subject: [PATCH 64/74] wsstore note --- src/util/WsStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index 9d68743..e5daa2e 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -16,7 +16,7 @@ type WsTopic = string; /** * A "Set" is used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to) - * TODO: do any WS topics allow parameters? If so, we need a way to track those (see FTX implementation) + * Note: Accurate duplicate tracking only works for plaintext topics. E.g. JSON objects may not be seen as duplicates if keys are in different orders. If that's needed, check the FTX implementation. */ type WsTopicList = Set; From 2d221aac14825090f9c081f48b71c11533ec572a Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 00:13:11 +0100 Subject: [PATCH 65/74] error event from ws client --- src/websocket-client.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 4a0e52c..062dae2 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -41,7 +41,7 @@ export type WsClientEvent = | 'open' | 'update' | 'close' - | 'errorEvent' + | 'error' | 'reconnect' | 'reconnected' | 'response'; @@ -53,7 +53,7 @@ interface WebsocketClientEvents { close: (evt: { wsKey: WsKey; event: any }) => void; response: (response: any) => void; update: (response: any) => void; - errorEvent: (response: any) => void; + error: (response: any) => void; } // Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837 @@ -344,7 +344,7 @@ export class WebsocketClient extends EventEmitter { private parseWsError(context: string, error: any, wsKey: WsKey) { if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); - this.emit('errorEvent', error); + this.emit('error', error); return; } @@ -365,7 +365,7 @@ export class WebsocketClient extends EventEmitter { ); break; } - this.emit('errorEvent', error); + this.emit('error', error); } /** @@ -663,7 +663,7 @@ export class WebsocketClient extends EventEmitter { // usdc options msg?.success === false ) { - return this.emit('errorEvent', { ...msg, wsKey }); + return this.emit('error', { ...msg, wsKey }); } this.logger.warning('Unhandled/unrecognised ws event message', { @@ -684,11 +684,6 @@ export class WebsocketClient extends EventEmitter { private onWsError(error: any, wsKey: WsKey) { this.parseWsError('Websocket error', error, wsKey); - if ( - this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) - ) { - this.emit('errorEvent', error); - } } private onWsClose(event, wsKey: WsKey) { From 36743773d34dcc30c4c337316e56f9ff2cd1e9d4 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 00:15:03 +0100 Subject: [PATCH 66/74] remove unused var --- src/spot-client.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/spot-client.ts b/src/spot-client.ts index e87bb6a..b4a8871 100644 --- a/src/spot-client.ts +++ b/src/spot-client.ts @@ -10,7 +10,7 @@ import { SpotSymbolInfo, } from './types'; import BaseRestClient from './util/BaseRestClient'; -import { agentSource, REST_CLIENT_TYPE_ENUM } from './util/requestUtils'; +import { REST_CLIENT_TYPE_ENUM } from './util/requestUtils'; /** * @deprecated Use SpotV3Client instead, which leverages the newer v3 APIs @@ -103,10 +103,7 @@ export class SpotClient extends BaseRestClient { */ submitOrder(params: NewSpotOrder): Promise> { - return this.postPrivate('/spot/v1/order', { - ...params, - agentSource, - }); + return this.postPrivate('/spot/v1/order', params); } getOrder(params: SpotOrderQueryById): Promise> { From 250174a7b2b67d1e05220e037f352cd775c81c0e Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 00:16:07 +0100 Subject: [PATCH 67/74] remove unused ref --- src/types/request/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/request/index.ts b/src/types/request/index.ts index de89375..fb11d51 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -3,7 +3,6 @@ export * from './copy-trading'; export * from './linear'; export * from './inverse'; export * from './spot'; -export * from './usdt-perp'; export * from './usdc-perp'; export * from './usdc-options'; export * from './usdc-shared'; From f7d59b48c1ee9a07515b032b60fd0388095d4dfb Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 00:18:56 +0100 Subject: [PATCH 68/74] fix mistake --- test/ws.util.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/ws.util.ts b/test/ws.util.ts index 7d1423e..a20793b 100644 --- a/test/ws.util.ts +++ b/test/ws.util.ts @@ -59,7 +59,7 @@ export function waitForSocketEvent( } wsClient.on(event, (e) => resolver(e)); - wsClient.on('errorEvent', (e) => rejector(e)); + wsClient.on('error', (e) => rejector(e)); // if (event !== 'close') { // wsClient.on('close', (event) => { @@ -89,7 +89,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { wsClient.on('response', retVal.response); wsClient.on('update', retVal.update); wsClient.on('close', retVal.close); - wsClient.on('errorEvent', retVal.error); + wsClient.on('error', retVal.error); return { ...retVal, @@ -98,7 +98,7 @@ export function listenToSocketEvents(wsClient: WebsocketClient) { wsClient.removeListener('response', retVal.response); wsClient.removeListener('update', retVal.update); wsClient.removeListener('close', retVal.close); - wsClient.removeListener('errorEvent', retVal.error); + wsClient.removeListener('error', retVal.error); }, }; } From d3fa937cdf7f98de7fa097ab3db9a5622273efa8 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 23:48:35 +0100 Subject: [PATCH 69/74] refactor tests for new constructor pattern --- README.md | 5 ++-- examples/rest-spot-public.ts | 6 ++-- src/util/BaseRestClient.ts | 34 +++++++++------------- src/util/requestUtils.ts | 18 +++++++++--- src/websocket-client.ts | 32 +++++++------------- test/account-asset/private.read.test.ts | 7 +++-- test/account-asset/public.read.test.ts | 7 +++-- test/copy-trading/private.read.test.ts | 7 +++-- test/copy-trading/public.read.test.ts | 7 +++-- test/inverse-futures/private.read.test.ts | 7 +++-- test/inverse-futures/private.write.test.ts | 7 +++-- test/inverse-futures/public.test.ts | 3 +- test/inverse/private.read.test.ts | 7 +++-- test/inverse/private.write.test.ts | 7 +++-- test/inverse/public.test.ts | 3 +- test/linear/private.read.test.ts | 7 +++-- test/linear/private.write.test.ts | 7 +++-- test/spot/private.v1.read.test.ts | 7 +++-- test/spot/private.v1.write.test.ts | 7 +++-- test/spot/private.v3.read.test.ts | 7 +++-- test/spot/private.v3.write.test.ts | 7 +++-- test/unified-margin/private.read.test.ts | 7 +++-- test/unified-margin/private.write.test.ts | 7 +++-- test/unified-margin/public.read.test.ts | 7 +++-- test/usdc/options/private.read.test.ts | 7 +++-- test/usdc/options/private.write.test.ts | 7 +++-- test/usdc/options/public.read.test.ts | 7 +++-- test/usdc/perpetual/private.read.test.ts | 7 +++-- test/usdc/perpetual/private.write.test.ts | 7 +++-- test/usdc/perpetual/public.read.test.ts | 7 +++-- 30 files changed, 160 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 8727a4d..b6102f6 100644 --- a/README.md +++ b/README.md @@ -240,10 +240,9 @@ ws.on('close', () => { console.log('connection closed'); }); -// Optional: Listen to raw error events. -// Note: responses to invalid topics are currently only sent in the "response" event. +// Optional: Listen to raw error events. Recommended. ws.on('error', err => { - console.error('ERR', err); + console.error('error', err); }); ``` diff --git a/examples/rest-spot-public.ts b/examples/rest-spot-public.ts index 151d71d..cfbe950 100644 --- a/examples/rest-spot-public.ts +++ b/examples/rest-spot-public.ts @@ -1,9 +1,9 @@ -import { SpotClient } from '../src/index'; +import { SpotClientV3 } from '../src/index'; // or -// import { SpotClient } from 'bybit-api'; +// import { SpotClientV3 } from 'bybit-api'; -const client = new SpotClient(); +const client = new SpotClientV3(); const symbol = 'BTCUSDT'; diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 9141dab..c9a7328 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -24,7 +24,7 @@ interface SignedRequestContext { timestamp?: number; api_key?: string; recv_window?: number; - // spot is diff from the rest... + // spot v1 is diff from the rest... recvWindow?: number; } @@ -45,8 +45,8 @@ interface UnsignedRequest { type SignMethod = 'keyInBody' | 'usdc'; export default abstract class BaseRestClient { - private timeOffset: number | null; - private syncTimePromise: null | Promise; + private timeOffset: number | null = null; + private syncTimePromise: null | Promise = null; private options: RestClientOptions; private baseUrl: string; private globalRequestOptions: AxiosRequestConfig; @@ -66,19 +66,12 @@ export default abstract class BaseRestClient { * @param {string} secret - your API secret * @param {boolean} [useLivenet=false] * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity - * @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios + * @param {AxiosRequestConfig} [networkOptions={}] HTTP networking options for axios */ constructor( - key?: string | undefined, - secret?: string | undefined, - useLivenet: boolean = false, - options: RestClientOptions = {}, - requestOptions: AxiosRequestConfig = {} + restOptions: RestClientOptions = {}, + networkOptions: AxiosRequestConfig = {} ) { - const baseUrl = getRestBaseUrl(useLivenet, options); - this.timeOffset = null; - this.syncTimePromise = null; - this.clientType = this.getClientType(); this.options = { @@ -89,24 +82,26 @@ 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, - ...options, + ...restOptions, }; this.globalRequestOptions = { // in ms == 5 minutes by default timeout: 1000 * 60 * 5, // custom request options based on axios specs - see: https://github.com/axios/axios#request-config - ...requestOptions, + ...networkOptions, headers: { 'x-referer': APIID, }, }; - this.baseUrl = baseUrl; + this.baseUrl = getRestBaseUrl(!!this.options.testnet, restOptions); + this.key = this.options.key; + this.secret = this.options.secret; - if (key && !secret) { + if (this.key && !this.secret) { throw new Error( - 'API Key & Secret are both required for private enpoints' + 'API Key & Secret are both required for private endpoints' ); } @@ -114,9 +109,6 @@ export default abstract class BaseRestClient { this.syncTime(); setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms!); } - - this.key = key; - this.secret = secret; } private isSpotClient() { diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index fde95a0..aa26be4 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -1,4 +1,13 @@ export interface RestClientOptions { + /** Your API key */ + key?: string; + + /** Your API secret */ + secret?: string; + + /** Set to `true` to connect to testnet. Uses the live environment by default. */ + testnet?: boolean; + /** Override the max size of the request window (in ms) */ recv_window?: number; @@ -43,7 +52,7 @@ export function serializeParams( } export function getRestBaseUrl( - useLivenet: boolean, + useTestnet: boolean, restInverseOptions: RestClientOptions ): string { const exchangeBaseUrls = { @@ -55,10 +64,11 @@ export function getRestBaseUrl( return restInverseOptions.baseUrl; } - if (useLivenet === true) { - return exchangeBaseUrls.livenet; + if (useTestnet) { + return exchangeBaseUrls.testnet; } - return exchangeBaseUrls.testnet; + + return exchangeBaseUrls.livenet; } export function isWsPong(msg: any): boolean { diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 062dae2..3905a21 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -47,12 +47,19 @@ export type WsClientEvent = | 'response'; interface WebsocketClientEvents { + /** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */ open: (evt: { wsKey: WsKey; event: any }) => void; + /** Reconnecting a dropped connection */ reconnect: (evt: { wsKey: WsKey; event: any }) => void; + /** Successfully reconnected a connection that dropped */ reconnected: (evt: { wsKey: WsKey; event: any }) => void; + /** Connection closed */ close: (evt: { wsKey: WsKey; event: any }) => void; + /** Received reply to websocket command (e.g. after subscribing to topics) */ response: (response: any) => void; + /** Received data for topic */ update: (response: any) => void; + /** Exception from ws client OR custom listeners */ error: (response: any) => void; } @@ -92,6 +99,10 @@ export class WebsocketClient extends EventEmitter { fetchTimeOffsetBeforeAuth: false, ...options, }; + this.options.restOptions = { + ...this.options.restOptions, + testnet: this.options.testnet, + }; this.prepareRESTClient(); } @@ -105,9 +116,6 @@ export class WebsocketClient extends EventEmitter { switch (this.options.market) { case 'inverse': { this.restClient = new InverseClient( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -115,9 +123,6 @@ export class WebsocketClient extends EventEmitter { } case 'linear': { this.restClient = new LinearClient( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -125,9 +130,6 @@ export class WebsocketClient extends EventEmitter { } case 'spot': { this.restClient = new SpotClient( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -136,9 +138,6 @@ export class WebsocketClient extends EventEmitter { } case 'spotv3': { this.restClient = new SpotClientV3( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -146,9 +145,6 @@ export class WebsocketClient extends EventEmitter { } case 'usdcOption': { this.restClient = new USDCOptionClient( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -156,9 +152,6 @@ export class WebsocketClient extends EventEmitter { } case 'usdcPerp': { this.restClient = new USDCPerpetualClient( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); @@ -167,9 +160,6 @@ export class WebsocketClient extends EventEmitter { case 'unifiedOption': case 'unifiedPerp': { this.restClient = new UnifiedMarginClient( - undefined, - undefined, - !this.isTestnet(), this.options.restOptions, this.options.requestOptions ); diff --git a/test/account-asset/private.read.test.ts b/test/account-asset/private.read.test.ts index 36f1458..16d97cd 100644 --- a/test/account-asset/private.read.test.ts +++ b/test/account-asset/private.read.test.ts @@ -2,7 +2,6 @@ import { AccountAssetClient } from '../../src/'; import { successResponseObject } from '../response.util'; describe('Private Account Asset REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Account Asset REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new AccountAssetClient(API_KEY, API_SECRET, useLivenet); + const api = new AccountAssetClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); it('getInternalTransfers()', async () => { expect(await api.getInternalTransfers()).toMatchObject( diff --git a/test/account-asset/public.read.test.ts b/test/account-asset/public.read.test.ts index e6e9f0b..d98cd9a 100644 --- a/test/account-asset/public.read.test.ts +++ b/test/account-asset/public.read.test.ts @@ -2,11 +2,14 @@ import { AccountAssetClient } from '../../src'; import { successResponseObject } from '../response.util'; describe('Public Account Asset REST API Endpoints', () => { - const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; - const api = new AccountAssetClient(API_KEY, API_SECRET, useLivenet); + const api = new AccountAssetClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); it('getSupportedDepositList()', async () => { expect(await api.getSupportedDepositList()).toMatchObject( diff --git a/test/copy-trading/private.read.test.ts b/test/copy-trading/private.read.test.ts index d0637ea..3579ddb 100644 --- a/test/copy-trading/private.read.test.ts +++ b/test/copy-trading/private.read.test.ts @@ -1,7 +1,6 @@ import { API_ERROR_CODE, CopyTradingClient } from '../../src'; describe('Private Copy Trading REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -10,7 +9,11 @@ describe('Private Copy Trading REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new CopyTradingClient(API_KEY, API_SECRET, useLivenet); + const api = new CopyTradingClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); // Don't have copy trading properly enabled on the test account, so testing is very light // (just make sure auth works and endpoint doesn't throw) diff --git a/test/copy-trading/public.read.test.ts b/test/copy-trading/public.read.test.ts index 8e5abc4..36b7f4b 100644 --- a/test/copy-trading/public.read.test.ts +++ b/test/copy-trading/public.read.test.ts @@ -2,11 +2,14 @@ import { CopyTradingClient } from '../../src'; import { successResponseObject } from '../response.util'; describe('Public Copy Trading REST API Endpoints', () => { - const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; - const api = new CopyTradingClient(API_KEY, API_SECRET, useLivenet); + const api = new CopyTradingClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); it('getSymbols()', async () => { expect(await api.getSymbols()).toMatchObject({ diff --git a/test/inverse-futures/private.read.test.ts b/test/inverse-futures/private.read.test.ts index 821d82b..7df1ec0 100644 --- a/test/inverse-futures/private.read.test.ts +++ b/test/inverse-futures/private.read.test.ts @@ -2,11 +2,14 @@ import { InverseFuturesClient } from '../../src/inverse-futures-client'; import { successResponseList, successResponseObject } from '../response.util'; describe('Private Inverse-Futures REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; - const api = new InverseFuturesClient(API_KEY, API_SECRET, useLivenet); + const api = new InverseFuturesClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); // Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol! const symbol = 'BTCUSDU22'; diff --git a/test/inverse-futures/private.write.test.ts b/test/inverse-futures/private.write.test.ts index d3c1cab..074d8f9 100644 --- a/test/inverse-futures/private.write.test.ts +++ b/test/inverse-futures/private.write.test.ts @@ -2,7 +2,6 @@ import { API_ERROR_CODE, InverseFuturesClient } from '../../src'; import { successResponseObject } from '../response.util'; describe('Private Inverse-Futures REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Inverse-Futures REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new InverseFuturesClient(API_KEY, API_SECRET, useLivenet); + const api = new InverseFuturesClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); // Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol! const symbol = 'BTCUSDU22'; diff --git a/test/inverse-futures/public.test.ts b/test/inverse-futures/public.test.ts index fd34263..bb60ea9 100644 --- a/test/inverse-futures/public.test.ts +++ b/test/inverse-futures/public.test.ts @@ -6,8 +6,7 @@ import { } from '../response.util'; describe('Public Inverse-Futures REST API Endpoints', () => { - const useLivenet = true; - const api = new InverseFuturesClient(undefined, undefined, useLivenet); + const api = new InverseFuturesClient(); const symbol = 'BTCUSD'; const interval = '15'; diff --git a/test/inverse/private.read.test.ts b/test/inverse/private.read.test.ts index d0b9ee9..db167b8 100644 --- a/test/inverse/private.read.test.ts +++ b/test/inverse/private.read.test.ts @@ -2,7 +2,6 @@ import { InverseClient } from '../../src/'; import { successResponseList, successResponseObject } from '../response.util'; describe('Private Inverse REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Inverse REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new InverseClient(API_KEY, API_SECRET, useLivenet); + const api = new InverseClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSD'; diff --git a/test/inverse/private.write.test.ts b/test/inverse/private.write.test.ts index f667045..6b106ef 100644 --- a/test/inverse/private.write.test.ts +++ b/test/inverse/private.write.test.ts @@ -3,7 +3,6 @@ import { InverseClient } from '../../src/inverse-client'; import { successResponseObject } from '../response.util'; describe('Private Inverse REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -12,7 +11,11 @@ describe('Private Inverse REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new InverseClient(API_KEY, API_SECRET, useLivenet); + const api = new InverseClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSD'; diff --git a/test/inverse/public.test.ts b/test/inverse/public.test.ts index 310641a..fb47bb9 100644 --- a/test/inverse/public.test.ts +++ b/test/inverse/public.test.ts @@ -6,8 +6,7 @@ import { } from '../response.util'; describe('Public Inverse REST API Endpoints', () => { - const useLivenet = true; - const api = new InverseClient(undefined, undefined, useLivenet); + const api = new InverseClient(); const symbol = 'BTCUSD'; const interval = '15'; diff --git a/test/linear/private.read.test.ts b/test/linear/private.read.test.ts index 9eef850..af403ab 100644 --- a/test/linear/private.read.test.ts +++ b/test/linear/private.read.test.ts @@ -2,7 +2,6 @@ import { LinearClient } from '../../src/linear-client'; import { successResponseList, successResponseObject } from '../response.util'; describe('Private Linear REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Linear REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new LinearClient(API_KEY, API_SECRET, useLivenet); + const api = new LinearClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSDT'; diff --git a/test/linear/private.write.test.ts b/test/linear/private.write.test.ts index dd920f1..32a3919 100644 --- a/test/linear/private.write.test.ts +++ b/test/linear/private.write.test.ts @@ -2,7 +2,6 @@ import { API_ERROR_CODE, LinearClient } from '../../src'; import { successResponseObject } from '../response.util'; describe('Private Linear REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Linear REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new LinearClient(API_KEY, API_SECRET, useLivenet); + const api = new LinearClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); // Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol! const symbol = 'BTCUSDT'; diff --git a/test/spot/private.v1.read.test.ts b/test/spot/private.v1.read.test.ts index 518e853..cbb6950 100644 --- a/test/spot/private.v1.read.test.ts +++ b/test/spot/private.v1.read.test.ts @@ -2,7 +2,6 @@ import { SpotClient } from '../../src'; import { errorResponseObject, successResponseList } from '../response.util'; describe('Private Spot REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Spot REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new SpotClient(API_KEY, API_SECRET, useLivenet); + const api = new SpotClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); it('getOrder()', async () => { // No auth error == test pass diff --git a/test/spot/private.v1.write.test.ts b/test/spot/private.v1.write.test.ts index 4b678de..fb59c7a 100644 --- a/test/spot/private.v1.write.test.ts +++ b/test/spot/private.v1.write.test.ts @@ -2,7 +2,6 @@ import { API_ERROR_CODE, SpotClient } from '../../src'; import { successResponseObject } from '../response.util'; describe('Private Spot REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Spot REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new SpotClient(API_KEY, API_SECRET, useLivenet); + const api = new SpotClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); // Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol! const symbol = 'BTCUSDT'; diff --git a/test/spot/private.v3.read.test.ts b/test/spot/private.v3.read.test.ts index 949bf0f..6db17ae 100644 --- a/test/spot/private.v3.read.test.ts +++ b/test/spot/private.v3.read.test.ts @@ -6,7 +6,6 @@ import { } from '../response.util'; describe('Private Spot REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -15,7 +14,11 @@ describe('Private Spot REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new SpotClientV3(API_KEY, API_SECRET, useLivenet); + const api = new SpotClientV3({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSDT'; const interval = '15m'; diff --git a/test/spot/private.v3.write.test.ts b/test/spot/private.v3.write.test.ts index 95ad6e7..6e9694f 100644 --- a/test/spot/private.v3.write.test.ts +++ b/test/spot/private.v3.write.test.ts @@ -2,7 +2,6 @@ import { API_ERROR_CODE, SpotClientV3 } from '../../src'; import { successResponseObjectV3 } from '../response.util'; describe('Private Spot REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Spot REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new SpotClientV3(API_KEY, API_SECRET, useLivenet); + const api = new SpotClientV3({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSDT'; const ltCode = 'BTC3S'; diff --git a/test/unified-margin/private.read.test.ts b/test/unified-margin/private.read.test.ts index c5093b9..9deeced 100644 --- a/test/unified-margin/private.read.test.ts +++ b/test/unified-margin/private.read.test.ts @@ -2,7 +2,6 @@ import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; import { successResponseObjectV3 } from '../response.util'; describe('Private Unified Margin REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private Unified Margin REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new UnifiedMarginClient(API_KEY, API_SECRET, useLivenet); + const api = new UnifiedMarginClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSDT'; const category = 'linear'; diff --git a/test/unified-margin/private.write.test.ts b/test/unified-margin/private.write.test.ts index 3e8d297..f0cb47f 100644 --- a/test/unified-margin/private.write.test.ts +++ b/test/unified-margin/private.write.test.ts @@ -1,7 +1,6 @@ import { API_ERROR_CODE, UnifiedMarginClient } from '../../src'; describe('Private Unified Margin REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -10,7 +9,11 @@ describe('Private Unified Margin REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new UnifiedMarginClient(API_KEY, API_SECRET, useLivenet); + const api = new UnifiedMarginClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSDT'; const category = 'linear'; diff --git a/test/unified-margin/public.read.test.ts b/test/unified-margin/public.read.test.ts index 9f7843d..406e8a9 100644 --- a/test/unified-margin/public.read.test.ts +++ b/test/unified-margin/public.read.test.ts @@ -5,11 +5,14 @@ import { } from '../response.util'; describe('Public Unified Margin REST API Endpoints', () => { - const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; - const api = new UnifiedMarginClient(API_KEY, API_SECRET, useLivenet); + const api = new UnifiedMarginClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCUSDT'; const category = 'linear'; diff --git a/test/usdc/options/private.read.test.ts b/test/usdc/options/private.read.test.ts index cd31815..abdc79b 100644 --- a/test/usdc/options/private.read.test.ts +++ b/test/usdc/options/private.read.test.ts @@ -2,7 +2,6 @@ import { USDCOptionClient } from '../../../src'; import { successResponseObjectV3 } from '../../response.util'; describe('Private USDC Options REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; const symbol = 'BTC-30SEP22-400000-C'; @@ -12,7 +11,11 @@ describe('Private USDC Options REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCOptionClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCOptionClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const category = 'OPTION'; it('getActiveRealtimeOrders()', async () => { diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts index 5bd48a7..442aa20 100644 --- a/test/usdc/options/private.write.test.ts +++ b/test/usdc/options/private.write.test.ts @@ -2,7 +2,6 @@ import { API_ERROR_CODE, USDCOptionClient } from '../../../src'; import { successResponseObjectV3 } from '../../response.util'; describe('Private USDC Options REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private USDC Options REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCOptionClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCOptionClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const currency = 'USDC'; const symbol = 'BTC-30SEP22-400000-C'; diff --git a/test/usdc/options/public.read.test.ts b/test/usdc/options/public.read.test.ts index 891082c..46536cc 100644 --- a/test/usdc/options/public.read.test.ts +++ b/test/usdc/options/public.read.test.ts @@ -5,11 +5,14 @@ import { } from '../../response.util'; describe('Public USDC Options REST API Endpoints', () => { - const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; - const api = new USDCOptionClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCOptionClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTC-30SEP22-400000-C'; it('getOrderBook()', async () => { diff --git a/test/usdc/perpetual/private.read.test.ts b/test/usdc/perpetual/private.read.test.ts index 876aeac..ea565f2 100644 --- a/test/usdc/perpetual/private.read.test.ts +++ b/test/usdc/perpetual/private.read.test.ts @@ -2,7 +2,6 @@ import { USDCPerpetualClient } from '../../../src'; import { successResponseObjectV3 } from '../../response.util'; describe('Private USDC Perp REST API GET Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -11,7 +10,11 @@ describe('Private USDC Perp REST API GET Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCPerpetualClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCPERP'; const category = 'PERPETUAL'; diff --git a/test/usdc/perpetual/private.write.test.ts b/test/usdc/perpetual/private.write.test.ts index 9401a37..ef13efc 100644 --- a/test/usdc/perpetual/private.write.test.ts +++ b/test/usdc/perpetual/private.write.test.ts @@ -5,7 +5,6 @@ import { } from '../../response.util'; describe('Private USDC Perp REST API POST Endpoints', () => { - const useLivenet = true; const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; @@ -14,7 +13,11 @@ describe('Private USDC Perp REST API POST Endpoints', () => { expect(API_SECRET).toStrictEqual(expect.any(String)); }); - const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCPerpetualClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCPERP'; diff --git a/test/usdc/perpetual/public.read.test.ts b/test/usdc/perpetual/public.read.test.ts index 161c580..0eb6865 100644 --- a/test/usdc/perpetual/public.read.test.ts +++ b/test/usdc/perpetual/public.read.test.ts @@ -5,11 +5,14 @@ import { } from '../../response.util'; describe('Public USDC Perp REST API Endpoints', () => { - const useLivenet = true; const API_KEY = undefined; const API_SECRET = undefined; - const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + const api = new USDCPerpetualClient({ + key: API_KEY, + secret: API_SECRET, + testnet: false, + }); const symbol = 'BTCPERP'; const category = 'PERPETUAL'; From 6e2ba00730aa703da10b9f08a6b2af1626f89361 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 23:53:01 +0100 Subject: [PATCH 70/74] readme cleaning --- README.md | 56 +++++++++++++++++++++++----------------- src/util/requestUtils.ts | 3 --- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b6102f6..9b9fef3 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ All REST clients have can be used in a similar way. However, method names, param Not sure which function to call or which parameters to use? Click the class name in the table above to look at all the function names (they are in the same order as the official API docs), and check the API docs for a list of endpoints/paramters/responses. -```javascript +```typescript const { InverseClient, LinearClient, @@ -95,41 +95,51 @@ const { } = require('bybit-api'); const restClientOptions = { - // override the max size of the request window (in ms) + /** Your API key. Optional, if you plan on making private api calls */ + key?: string; + + /** Your API secret. Optional, if you plan on making private api calls */ + secret?: string; + + /** Set to `true` to connect to testnet. Uses the live environment by default. */ + testnet?: boolean; + + /** Override the max size of the request window (in ms) */ recv_window?: number; - // Disabled by default, not recommended unless you know what you're doing + /** Disabled by default. This can help on machines with consistent latency problems. */ enable_time_sync?: boolean; - // how often to sync time drift with bybit servers - sync_interval_ms?: number; + /** How often to sync time drift with bybit servers */ + sync_interval_ms?: number | string; - // Optionally override API protocol + domain - // e.g 'https://api.bytick.com' + /** Default: false. If true, we'll throw errors if any params are undefined */ + strict_param_validation?: boolean; + + /** + * Optionally override API protocol + domain + * e.g baseUrl: 'https://api.bytick.com' + **/ baseUrl?: string; - // Default: true. whether to try and post-process request exceptions. + /** Default: true. whether to try and post-process request exceptions. */ parse_exceptions?: boolean; }; const API_KEY = 'xxx'; -const PRIVATE_KEY = 'yyy'; -const useLivenet = false; +const API_SECRET = 'yyy'; +const useTestnet = false; -const client = new InverseClient( - // Optional, unless you plan on making private API calls - API_KEY, - PRIVATE_KEY, - - // Optional, uses testnet by default. Set to 'true' to use livenet. - useLivenet, - - // restClientOptions, +const client = new InverseClient({ + key: API_KEY, + secret: API_SECRET, + testnet: useTestnet +}, // requestLibraryOptions ); -// For public-only API calls, simply set key and secret to "undefined": -// const client = new InverseClient(undefined, undefined, useLivenet); +// For public-only API calls, simply don't provide a key & secret or set them to undefined +// const client = new InverseClient({}); client.getApiKeyInfo() .then(result => { @@ -179,8 +189,8 @@ const wsConfig = { The following parameters are optional: */ - // defaults to false == testnet. Set to true for livenet. - // livenet: true + // defaults to true == livenet + // testnet: false // NOTE: to listen to multiple markets (spot vs inverse vs linear vs linearfutures) at once, make one WebsocketClient instance per market diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index aa26be4..5c0bf61 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -11,9 +11,6 @@ export interface RestClientOptions { /** Override the max size of the request window (in ms) */ recv_window?: number; - /** @deprecated Time sync is now disabled by default. To re-enable it, use enable_time_sync instead. */ - disable_time_sync?: boolean; - /** Disabled by default. This can help on machines with consistent latency problems. */ enable_time_sync?: boolean; From 0ad221fe5a1549c2f6edbd95d5e71626a82b3343 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Mon, 19 Sep 2022 23:58:02 +0100 Subject: [PATCH 71/74] cleaning in the base rest client --- src/util/BaseRestClient.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index c9a7328..dc87f1a 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -115,22 +115,18 @@ export default abstract class BaseRestClient { return this.clientType === REST_CLIENT_TYPE_ENUM.spot; } - private isUSDCClient() { - return this.clientType === REST_CLIENT_TYPE_ENUM.v3; - } - get(endpoint: string, params?: any) { return this._call('GET', endpoint, params, true); } - post(endpoint: string, params?: any) { - return this._call('POST', endpoint, params, true); - } - getPrivate(endpoint: string, params?: any) { return this._call('GET', endpoint, params, false); } + post(endpoint: string, params?: any) { + return this._call('POST', endpoint, params, true); + } + postPrivate(endpoint: string, params?: any) { return this._call('POST', endpoint, params, false); } @@ -201,8 +197,8 @@ export default abstract class BaseRestClient { }; } - // USDC Options uses a different way of authenticating requests (headers instead of params) - if (this.isUSDCClient()) { + // USDC endpoints, unified margin and a few others use a different way of authenticating requests (headers instead of params) + if (this.clientType === REST_CLIENT_TYPE_ENUM.v3) { if (!options.headers) { options.headers = {}; } From 22c0de50b03ac05623e980e1b5c83afb71a70fe4 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 20 Sep 2022 00:01:59 +0100 Subject: [PATCH 72/74] fix test and jsdoc --- src/util/BaseRestClient.ts | 5 +---- test/linear/public.test.ts | 3 +-- test/spot/public.v1.test.ts | 3 +-- test/spot/public.v3.test.ts | 3 +-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index dc87f1a..8e47583 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -61,10 +61,7 @@ export default abstract class BaseRestClient { abstract getClientType(): RestClientType; /** - * Create an instance of the REST client - * @param {string} key - your API key - * @param {string} secret - your API secret - * @param {boolean} [useLivenet=false] + * Create an instance of the REST client. Pass API credentials in the object in the first parameter. * @param {RestClientOptions} [restClientOptions={}] options to configure REST API connectivity * @param {AxiosRequestConfig} [networkOptions={}] HTTP networking options for axios */ diff --git a/test/linear/public.test.ts b/test/linear/public.test.ts index 169f93f..73f495d 100644 --- a/test/linear/public.test.ts +++ b/test/linear/public.test.ts @@ -6,8 +6,7 @@ import { } from '../response.util'; describe('Public Linear REST API Endpoints', () => { - const useLivenet = true; - const api = new LinearClient(undefined, undefined, useLivenet); + const api = new LinearClient(); const symbol = 'BTCUSDT'; const interval = '15'; diff --git a/test/spot/public.v1.test.ts b/test/spot/public.v1.test.ts index f0d261c..e89afbc 100644 --- a/test/spot/public.v1.test.ts +++ b/test/spot/public.v1.test.ts @@ -6,8 +6,7 @@ import { } from '../response.util'; describe('Public Spot REST API Endpoints', () => { - const useLivenet = true; - const api = new SpotClient(undefined, undefined, useLivenet); + const api = new SpotClient(); const symbol = 'BTCUSDT'; const interval = '15m'; diff --git a/test/spot/public.v3.test.ts b/test/spot/public.v3.test.ts index e18f260..58d92cb 100644 --- a/test/spot/public.v3.test.ts +++ b/test/spot/public.v3.test.ts @@ -5,8 +5,7 @@ import { } from '../response.util'; describe('Public Spot REST API Endpoints', () => { - const useLivenet = true; - const api = new SpotClientV3(undefined, undefined, useLivenet); + const api = new SpotClientV3(); const symbol = 'BTCUSDT'; const interval = '15m'; From c766b331fa3a7da10d5d5d94c29ec11bfc7a85bf Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 20 Sep 2022 00:06:38 +0100 Subject: [PATCH 73/74] readme tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b9fef3..1ea50fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# bybit-api +# Node.js & Typescript Bybit API SDK [![Tests](https://circleci.com/gh/tiagosiebler/bybit-api.svg?style=shield)](https://circleci.com/gh/tiagosiebler/bybit-api) [![npm version](https://img.shields.io/npm/v/bybit-api)][1] [![npm size](https://img.shields.io/bundlephobia/min/bybit-api/latest)][1] [![npm downloads](https://img.shields.io/npm/dt/bybit-api)][1] [![last commit](https://img.shields.io/github/last-commit/tiagosiebler/bybit-api)][1] From bc3ce2e948cd6d22451d5055d6cde5294cdcedee Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Tue, 20 Sep 2022 00:07:48 +0100 Subject: [PATCH 74/74] update desc --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eaf14a..b85d8eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bybit-api", "version": "3.0.0", - "description": "Node.js connector 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", "types": "lib/index.d.ts", "files": [