From 63201b465ce2b1ef9aac57a1605dabc71e569ede Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 13:39:07 +0100 Subject: [PATCH] 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()); + }); +});