diff --git a/docs/endpointFunctionList.md b/docs/endpointFunctionList.md index 71e456a..d9c9e49 100644 --- a/docs/endpointFunctionList.md +++ b/docs/endpointFunctionList.md @@ -14,7 +14,7 @@ Each REST client is a JavaScript class, which provides functions individually ma The following table shows all methods available in each REST client, whether the method requires authentication (automatically handled if API keys are provided), as well as the exact endpoint each method is connected to. -This can be used to easily find which method to call, once you have [found which endpoint you're looking to use](https://github.com/tiagosiebler/awesome-crypto-examples/wiki/How-to-find-SDK-functions-that-match-API-docs-endpoint). +This can be used to easily find which method to call, once you have [found which endpoint you're looking to use](https://github.com/sieblyio/awesome-crypto-examples/wiki/How-to-find-SDK-functions-that-match-API-docs-endpoint). All REST clients are in the [src](/src) folder. For usage examples, make sure to check the [examples](/examples) folder. @@ -50,116 +50,118 @@ This table includes all endpoints from the official Exchange API docs and corres | Function | AUTH | HTTP Method | Endpoint | | -------- | :------: | :------: | -------- | -| [getMarketStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L181) | | GET | `/v2/market-status` | -| [getTimestamp()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L190) | | GET | `/v1/common/timestamp` | -| [getTradingSymbols()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L199) | | GET | `/v2/settings/common/symbols` | -| [getCurrencies()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L210) | | GET | `/v2/settings/common/currencies` | -| [getCurrencysSettings()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L221) | | GET | `/v1/settings/common/currencys` | -| [getSymbolsSettings()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L232) | | GET | `/v1/settings/common/symbols` | -| [getMarketSymbolsSettings()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L243) | | GET | `/v1/settings/common/market-symbols` | -| [getChainsInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L255) | | GET | `/v1/settings/common/chains` | -| [getReferenceCurrencies()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L266) | | GET | `/v2/reference/currencies` | -| [getKlines()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L284) | | GET | `/market/history/kline` | -| [getTicker()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L295) | | GET | `/market/detail/merged` | -| [getTickers()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L306) | | GET | `/market/tickers` | -| [getMarketDepth()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L315) | | GET | `/market/depth` | -| [getLastTrade()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L326) | | GET | `/market/trade` | -| [getHistoryTrades()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L337) | | GET | `/market/history/trade` | -| [get24hMarketSummary()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L349) | | GET | `/market/detail` | -| [getFullOrderBook()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L360) | | GET | `/market/fullMbp` | -| [getAccounts()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L377) | :closed_lock_with_key: | GET | `/v1/account/accounts` | -| [getAccountBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L386) | :closed_lock_with_key: | GET | `/v1/account/accounts/{accountId}/balance` | -| [getAccountValuation()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L397) | :closed_lock_with_key: | GET | `/v2/account/valuation` | -| [getAssetValuation()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L409) | :closed_lock_with_key: | GET | `/v2/account/asset-valuation` | -| [submitTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L420) | :closed_lock_with_key: | POST | `/v1/account/transfer` | -| [getAccountHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L431) | :closed_lock_with_key: | GET | `/v1/account/history` | -| [getAccountLedger()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L444) | :closed_lock_with_key: | GET | `/v2/account/ledger` | -| [submitV2AccountTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L457) | :closed_lock_with_key: | POST | `/v2/account/transfer` | -| [submitFuturesTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L468) | :closed_lock_with_key: | POST | `/v1/futures/transfer` | -| [getPointBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L479) | :closed_lock_with_key: | GET | `/v2/point/account` | -| [submitPointTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L490) | :closed_lock_with_key: | POST | `/v2/point/transfer` | -| [getAccountSwitchUserInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L501) | :closed_lock_with_key: | GET | `/v1/account/switch/user/info` | -| [getAccountOverviewInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L512) | :closed_lock_with_key: | GET | `/v1/account/overview/info` | -| [updateFeeDeductionMethod()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L523) | :closed_lock_with_key: | POST | `/v1/account/fee/switch` | -| [submitOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L541) | :closed_lock_with_key: | POST | `/v1/order/orders/place` | -| [submitBatchOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L553) | :closed_lock_with_key: | POST | `/v1/order/batch-orders` | -| [submitMarginOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L567) | :closed_lock_with_key: | POST | `/v1/order/auto/place` | -| [cancelOrderById()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L579) | :closed_lock_with_key: | POST | `/v1/order/orders/{orderId}/submitcancel` | -| [cancelOrderByClientId()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L594) | :closed_lock_with_key: | POST | `/v1/order/orders/submitCancelClientOrder` | -| [cancelAllOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L607) | :closed_lock_with_key: | GET | `/v1/order/cancelAllOrders` | -| [getOpenOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L618) | :closed_lock_with_key: | GET | `/v1/order/openOrders` | -| [batchCancelOpenOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L629) | :closed_lock_with_key: | POST | `/v1/order/orders/batchCancelOpenOrders` | -| [batchCancelOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L642) | :closed_lock_with_key: | POST | `/v1/order/orders/batchcancel` | -| [setCancelAllAfter()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L654) | :closed_lock_with_key: | POST | `/v2/algo-orders/cancel-all-after` | -| [getOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L667) | :closed_lock_with_key: | GET | `/v1/order/orders/{orderId}` | -| [getOrderByClientId()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L678) | :closed_lock_with_key: | GET | `/v1/order/orders/getClientOrder` | -| [getOrderMatch()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L689) | :closed_lock_with_key: | GET | `/v1/order/orders/{orderId}/matchresults` | -| [getOrderHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L700) | :closed_lock_with_key: | GET | `/v1/order/orders` | -| [getOrderHistory48h()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L711) | :closed_lock_with_key: | GET | `/v1/order/history` | -| [getMatchResults()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L722) | :closed_lock_with_key: | GET | `/v1/order/matchresults` | -| [getFeeRate()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L733) | :closed_lock_with_key: | GET | `/v2/reference/transact-fee-rate` | -| [placeConditionalOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L750) | :closed_lock_with_key: | POST | `/v2/algo-orders` | -| [cancelConditionalOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L762) | :closed_lock_with_key: | POST | `/v2/algo-orders/cancellation` | -| [getOpenConditionalOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L773) | :closed_lock_with_key: | GET | `/v2/algo-orders/opening` | -| [getConditionalOrderHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L784) | :closed_lock_with_key: | GET | `/v2/algo-orders/history` | -| [getConditionalOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L795) | :closed_lock_with_key: | GET | `/v2/algo-orders/specific` | -| [getRepaymentRecords()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L812) | :closed_lock_with_key: | GET | `/v2/account/repayment` | -| [repayMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L825) | :closed_lock_with_key: | POST | `/v2/account/repayment` | -| [transferSpotToIsolatedMargin()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L836) | :closed_lock_with_key: | POST | `/v1/dw/transfer-in/margin` | -| [transferIsolatedMarginToSpot()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L847) | :closed_lock_with_key: | POST | `/v1/dw/transfer-out/margin` | -| [getMarginLoanInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L858) | :closed_lock_with_key: | GET | `/v1/margin/loan-info` | -| [requestMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L869) | :closed_lock_with_key: | POST | `/v1/margin/orders` | -| [repayMarginLoanIsolated()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L880) | :closed_lock_with_key: | POST | `/v1/margin/orders/{orderId}/repay` | -| [getMarginLoanOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L895) | :closed_lock_with_key: | GET | `/v1/margin/loan-orders` | -| [getMarginAccountBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L906) | :closed_lock_with_key: | GET | `/v1/margin/accounts/balance` | -| [transferSpotToCrossMargin()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L918) | :closed_lock_with_key: | POST | `/v1/cross-margin/transfer-in` | -| [transferCrossMarginToSpot()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L930) | :closed_lock_with_key: | POST | `/v1/cross-margin/transfer-out` | -| [getCrossMarginLoanInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L942) | :closed_lock_with_key: | GET | `/v1/cross-margin/loan-info` | -| [requestCrossMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L953) | :closed_lock_with_key: | POST | `/v1/cross-margin/orders` | -| [repayCrossMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L965) | :closed_lock_with_key: | POST | `/v1/cross-margin/orders/{orderId}/repay` | -| [getCrossMarginLoanOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L980) | :closed_lock_with_key: | GET | `/v1/cross-margin/loan-orders` | -| [getCrossMarginBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L991) | :closed_lock_with_key: | GET | `/v1/cross-margin/accounts/balance` | -| [getCrossMarginLimit()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1002) | :closed_lock_with_key: | GET | `/v2/margin/limit` | -| [getDepositAddress()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1019) | :closed_lock_with_key: | GET | `/v2/account/deposit/address` | -| [getWithdrawQuota()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1030) | :closed_lock_with_key: | GET | `/v2/account/withdraw/quota` | -| [getWithdrawAddress()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1041) | :closed_lock_with_key: | GET | `/v2/account/withdraw/address` | -| [submitWithdraw()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1054) | :closed_lock_with_key: | POST | `/v1/dw/withdraw/api/create` | -| [getWithdrawByClientId()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1065) | :closed_lock_with_key: | GET | `/v1/query/withdraw/client-order-id` | -| [cancelWithdraw()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1076) | :closed_lock_with_key: | POST | `/v1/dw/withdraw-virtual/{withdrawId}/cancel` | -| [getDepositWithdrawHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1090) | :closed_lock_with_key: | GET | `/v1/query/deposit-withdraw` | -| [getVaspList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1101) | | GET | `/v1/query/vasp-list` | -| [getBrokerUserRebateStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1116) | :closed_lock_with_key: | GET | `/broker/v1/user_rebate_status` | -| [setBrokerSubUserFeeRate()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1142) | :closed_lock_with_key: | POST | `/broker/v1/sub-user/fee_rate/add` | -| [getBrokerAccountCapitalSnapshot()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1155) | :closed_lock_with_key: | POST | `/broker/v1/account_capital_snapshot_everyday` | -| [updateSubUserDeductMode()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1174) | :closed_lock_with_key: | POST | `/v2/sub-user/deduct-mode` | -| [getSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1186) | :closed_lock_with_key: | GET | `/v2/user/api-key` | -| [getUserUid()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1198) | :closed_lock_with_key: | GET | `/v2/user/uid` | -| [getSubUserList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1221) | :closed_lock_with_key: | GET | `/v2/sub-user/user-list` | -| [updateSubUserLockStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1232) | :closed_lock_with_key: | POST | `/v2/sub-user/management` | -| [getSubUserStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1244) | :closed_lock_with_key: | GET | `/v2/sub-user/user-state` | -| [setSubUserTradableMarket()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1255) | :closed_lock_with_key: | POST | `/v2/sub-user/tradable-market` | -| [setSubUserTransferPermissions()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1266) | :closed_lock_with_key: | POST | `/v2/sub-user/transferability` | -| [getSubUserAccounts()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1277) | :closed_lock_with_key: | GET | `/v2/sub-user/account-list` | -| [createSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1288) | :closed_lock_with_key: | POST | `/v2/sub-user/api-key-generation` | -| [updateSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1301) | :closed_lock_with_key: | POST | `/v2/sub-user/api-key-modification` | -| [deleteSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1314) | :closed_lock_with_key: | POST | `/v2/sub-user/api-key-deletion` | -| [submitSubUserTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1328) | :closed_lock_with_key: | POST | `/v1/subuser/transfer` | -| [getSubUserDepositAddress()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1339) | :closed_lock_with_key: | GET | `/v2/sub-user/deposit-address` | -| [getSubUserDepositHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1351) | :closed_lock_with_key: | GET | `/v2/sub-user/query-deposit` | -| [getSubUsersAggregatedBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1364) | :closed_lock_with_key: | GET | `/v1/subuser/aggregate-balance` | -| [getSubUserBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1375) | :closed_lock_with_key: | GET | `/v1/account/accounts/{subUid}` | -| [getSubUserEntrustUserList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1386) | :closed_lock_with_key: | GET | `/v2/sub-user/entrust-user-list` | -| [getSubUserManagedTransferHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1402) | :closed_lock_with_key: | GET | `/v2/sub-user/managed-transfer-history` | -| [getReferralRebateDetail()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1418) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/detail` | -| [getReferralRebateHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1429) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/history` | -| [getReferralAllRebateDetail()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1440) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/all_rebate/detail` | -| [getReferralMultipleRebateDetail()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1453) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/batcher_rebate/detail` | -| [getReferralInvitedUserList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1464) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/referrals` | -| [getP2POrderHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1483) | | GET | `/v1/api/c2c/order/history` | -| [getEarnProjectList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1503) | :closed_lock_with_key: | GET | `/v1/earn/project/queryEarnProjectList` | -| [earnSubscribe()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1516) | :closed_lock_with_key: | POST | `/v1/earn/order/demand/add` | -| [earnRedeem()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1527) | :closed_lock_with_key: | POST | `/v1/earn/order/demand/redeem-order` | -| [getEarnUserAssets()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1540) | :closed_lock_with_key: | GET | `/v1/earn/order/user/assets/list` | +| [getMarketStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L185) | | GET | `/v2/market-status` | +| [getTimestamp()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L194) | | GET | `/v1/common/timestamp` | +| [getTradingSymbols()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L203) | | GET | `/v2/settings/common/symbols` | +| [getCurrencies()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L214) | | GET | `/v2/settings/common/currencies` | +| [getCurrencysSettings()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L225) | | GET | `/v1/settings/common/currencys` | +| [getSymbolsSettings()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L236) | | GET | `/v1/settings/common/symbols` | +| [getMarketSymbolsSettings()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L247) | | GET | `/v1/settings/common/market-symbols` | +| [getChainsInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L259) | | GET | `/v1/settings/common/chains` | +| [getReferenceCurrencies()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L270) | | GET | `/v2/reference/currencies` | +| [getKlines()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L288) | | GET | `/market/history/kline` | +| [getTicker()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L299) | | GET | `/market/detail/merged` | +| [getTickers()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L310) | | GET | `/market/tickers` | +| [getMarketDepth()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L319) | | GET | `/market/depth` | +| [getLastTrade()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L330) | | GET | `/market/trade` | +| [getHistoryTrades()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L341) | | GET | `/market/history/trade` | +| [get24hMarketSummary()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L353) | | GET | `/market/detail` | +| [getFullOrderBook()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L364) | | GET | `/market/fullMbp` | +| [getAccounts()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L381) | :closed_lock_with_key: | GET | `/v1/account/accounts` | +| [getAccountBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L390) | :closed_lock_with_key: | GET | `/v1/account/accounts/{accountId}/balance` | +| [getAccountValuation()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L401) | :closed_lock_with_key: | GET | `/v2/account/valuation` | +| [getAssetValuation()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L413) | :closed_lock_with_key: | GET | `/v2/account/asset-valuation` | +| [submitTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L424) | :closed_lock_with_key: | POST | `/v1/account/transfer` | +| [getAccountHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L435) | :closed_lock_with_key: | GET | `/v1/account/history` | +| [getAccountLedger()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L448) | :closed_lock_with_key: | GET | `/v2/account/ledger` | +| [submitV2AccountTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L461) | :closed_lock_with_key: | POST | `/v2/account/transfer` | +| [submitUniversalTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L472) | :closed_lock_with_key: | POST | `/v5/account/universal_transfer` | +| [getUniversalTransferRecords()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L483) | :closed_lock_with_key: | GET | `/v5/account/universal_transfer_records` | +| [submitFuturesTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L494) | :closed_lock_with_key: | POST | `/v1/futures/transfer` | +| [getPointBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L505) | :closed_lock_with_key: | GET | `/v2/point/account` | +| [submitPointTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L516) | :closed_lock_with_key: | POST | `/v2/point/transfer` | +| [getAccountSwitchUserInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L527) | :closed_lock_with_key: | GET | `/v1/account/switch/user/info` | +| [getAccountOverviewInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L538) | :closed_lock_with_key: | GET | `/v1/account/overview/info` | +| [updateFeeDeductionMethod()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L549) | :closed_lock_with_key: | POST | `/v1/account/fee/switch` | +| [submitOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L567) | :closed_lock_with_key: | POST | `/v1/order/orders/place` | +| [submitBatchOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L579) | :closed_lock_with_key: | POST | `/v1/order/batch-orders` | +| [submitMarginOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L593) | :closed_lock_with_key: | POST | `/v1/order/auto/place` | +| [cancelOrderById()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L605) | :closed_lock_with_key: | POST | `/v1/order/orders/{orderId}/submitcancel` | +| [cancelOrderByClientId()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L620) | :closed_lock_with_key: | POST | `/v1/order/orders/submitCancelClientOrder` | +| [cancelAllOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L633) | :closed_lock_with_key: | GET | `/v1/order/cancelAllOrders` | +| [getOpenOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L644) | :closed_lock_with_key: | GET | `/v1/order/openOrders` | +| [batchCancelOpenOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L655) | :closed_lock_with_key: | POST | `/v1/order/orders/batchCancelOpenOrders` | +| [batchCancelOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L668) | :closed_lock_with_key: | POST | `/v1/order/orders/batchcancel` | +| [setCancelAllAfter()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L680) | :closed_lock_with_key: | POST | `/v2/algo-orders/cancel-all-after` | +| [getOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L693) | :closed_lock_with_key: | GET | `/v1/order/orders/{orderId}` | +| [getOrderByClientId()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L704) | :closed_lock_with_key: | GET | `/v1/order/orders/getClientOrder` | +| [getOrderMatch()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L715) | :closed_lock_with_key: | GET | `/v1/order/orders/{orderId}/matchresults` | +| [getOrderHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L726) | :closed_lock_with_key: | GET | `/v1/order/orders` | +| [getOrderHistory48h()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L737) | :closed_lock_with_key: | GET | `/v1/order/history` | +| [getMatchResults()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L748) | :closed_lock_with_key: | GET | `/v1/order/matchresults` | +| [getFeeRate()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L759) | :closed_lock_with_key: | GET | `/v2/reference/transact-fee-rate` | +| [placeConditionalOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L776) | :closed_lock_with_key: | POST | `/v2/algo-orders` | +| [cancelConditionalOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L788) | :closed_lock_with_key: | POST | `/v2/algo-orders/cancellation` | +| [getOpenConditionalOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L799) | :closed_lock_with_key: | GET | `/v2/algo-orders/opening` | +| [getConditionalOrderHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L810) | :closed_lock_with_key: | GET | `/v2/algo-orders/history` | +| [getConditionalOrder()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L821) | :closed_lock_with_key: | GET | `/v2/algo-orders/specific` | +| [getRepaymentRecords()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L838) | :closed_lock_with_key: | GET | `/v2/account/repayment` | +| [repayMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L851) | :closed_lock_with_key: | POST | `/v2/account/repayment` | +| [transferSpotToIsolatedMargin()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L862) | :closed_lock_with_key: | POST | `/v1/dw/transfer-in/margin` | +| [transferIsolatedMarginToSpot()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L873) | :closed_lock_with_key: | POST | `/v1/dw/transfer-out/margin` | +| [getMarginLoanInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L884) | :closed_lock_with_key: | GET | `/v1/margin/loan-info` | +| [requestMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L895) | :closed_lock_with_key: | POST | `/v1/margin/orders` | +| [repayMarginLoanIsolated()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L906) | :closed_lock_with_key: | POST | `/v1/margin/orders/{orderId}/repay` | +| [getMarginLoanOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L921) | :closed_lock_with_key: | GET | `/v1/margin/loan-orders` | +| [getMarginAccountBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L932) | :closed_lock_with_key: | GET | `/v1/margin/accounts/balance` | +| [transferSpotToCrossMargin()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L944) | :closed_lock_with_key: | POST | `/v1/cross-margin/transfer-in` | +| [transferCrossMarginToSpot()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L956) | :closed_lock_with_key: | POST | `/v1/cross-margin/transfer-out` | +| [getCrossMarginLoanInfo()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L968) | :closed_lock_with_key: | GET | `/v1/cross-margin/loan-info` | +| [requestCrossMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L979) | :closed_lock_with_key: | POST | `/v1/cross-margin/orders` | +| [repayCrossMarginLoan()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L991) | :closed_lock_with_key: | POST | `/v1/cross-margin/orders/{orderId}/repay` | +| [getCrossMarginLoanOrders()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1006) | :closed_lock_with_key: | GET | `/v1/cross-margin/loan-orders` | +| [getCrossMarginBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1017) | :closed_lock_with_key: | GET | `/v1/cross-margin/accounts/balance` | +| [getCrossMarginLimit()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1028) | :closed_lock_with_key: | GET | `/v2/margin/limit` | +| [getDepositAddress()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1045) | :closed_lock_with_key: | GET | `/v2/account/deposit/address` | +| [getWithdrawQuota()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1056) | :closed_lock_with_key: | GET | `/v2/account/withdraw/quota` | +| [getWithdrawAddress()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1067) | :closed_lock_with_key: | GET | `/v2/account/withdraw/address` | +| [submitWithdraw()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1080) | :closed_lock_with_key: | POST | `/v1/dw/withdraw/api/create` | +| [getWithdrawByClientId()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1091) | :closed_lock_with_key: | GET | `/v1/query/withdraw/client-order-id` | +| [cancelWithdraw()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1102) | :closed_lock_with_key: | POST | `/v1/dw/withdraw-virtual/{withdrawId}/cancel` | +| [getDepositWithdrawHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1116) | :closed_lock_with_key: | GET | `/v1/query/deposit-withdraw` | +| [getVaspList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1127) | | GET | `/v1/query/vasp-list` | +| [getBrokerUserRebateStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1142) | :closed_lock_with_key: | GET | `/broker/v1/user_rebate_status` | +| [setBrokerSubUserFeeRate()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1168) | :closed_lock_with_key: | POST | `/broker/v1/sub-user/fee_rate/add` | +| [getBrokerAccountCapitalSnapshot()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1181) | :closed_lock_with_key: | POST | `/broker/v1/account_capital_snapshot_everyday` | +| [updateSubUserDeductMode()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1200) | :closed_lock_with_key: | POST | `/v2/sub-user/deduct-mode` | +| [getSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1212) | :closed_lock_with_key: | GET | `/v2/user/api-key` | +| [getUserUid()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1224) | :closed_lock_with_key: | GET | `/v2/user/uid` | +| [getSubUserList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1247) | :closed_lock_with_key: | GET | `/v2/sub-user/user-list` | +| [updateSubUserLockStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1258) | :closed_lock_with_key: | POST | `/v2/sub-user/management` | +| [getSubUserStatus()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1270) | :closed_lock_with_key: | GET | `/v2/sub-user/user-state` | +| [setSubUserTradableMarket()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1281) | :closed_lock_with_key: | POST | `/v2/sub-user/tradable-market` | +| [setSubUserTransferPermissions()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1292) | :closed_lock_with_key: | POST | `/v2/sub-user/transferability` | +| [getSubUserAccounts()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1303) | :closed_lock_with_key: | GET | `/v2/sub-user/account-list` | +| [createSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1314) | :closed_lock_with_key: | POST | `/v2/sub-user/api-key-generation` | +| [updateSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1327) | :closed_lock_with_key: | POST | `/v2/sub-user/api-key-modification` | +| [deleteSubUserApiKey()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1340) | :closed_lock_with_key: | POST | `/v2/sub-user/api-key-deletion` | +| [submitSubUserTransfer()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1354) | :closed_lock_with_key: | POST | `/v1/subuser/transfer` | +| [getSubUserDepositAddress()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1365) | :closed_lock_with_key: | GET | `/v2/sub-user/deposit-address` | +| [getSubUserDepositHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1377) | :closed_lock_with_key: | GET | `/v2/sub-user/query-deposit` | +| [getSubUsersAggregatedBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1390) | :closed_lock_with_key: | GET | `/v1/subuser/aggregate-balance` | +| [getSubUserBalance()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1401) | :closed_lock_with_key: | GET | `/v1/account/accounts/{subUid}` | +| [getSubUserEntrustUserList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1412) | :closed_lock_with_key: | GET | `/v2/sub-user/entrust-user-list` | +| [getSubUserManagedTransferHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1428) | :closed_lock_with_key: | GET | `/v2/sub-user/managed-transfer-history` | +| [getReferralRebateDetail()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1444) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/detail` | +| [getReferralRebateHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1455) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/history` | +| [getReferralAllRebateDetail()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1466) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/all_rebate/detail` | +| [getReferralMultipleRebateDetail()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1479) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/batcher_rebate/detail` | +| [getReferralInvitedUserList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1490) | :closed_lock_with_key: | GET | `/v2/invitee/rebate/referrals` | +| [getP2POrderHistory()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1509) | | GET | `/v1/api/c2c/order/history` | +| [getEarnProjectList()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1529) | :closed_lock_with_key: | GET | `/v1/earn/project/queryEarnProjectList` | +| [earnSubscribe()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1542) | :closed_lock_with_key: | POST | `/v1/earn/order/demand/add` | +| [earnRedeem()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1553) | :closed_lock_with_key: | POST | `/v1/earn/order/demand/redeem-order` | +| [getEarnUserAssets()](https://github.com/sieblyio/htx-api/blob/main/src/SpotClient.ts#L1566) | :closed_lock_with_key: | GET | `/v1/earn/order/user/assets/list` | # FuturesClient.ts diff --git a/examples/apidoc/SpotClient/getUniversalTransferRecords.js b/examples/apidoc/SpotClient/getUniversalTransferRecords.js new file mode 100644 index 0000000..45250d6 --- /dev/null +++ b/examples/apidoc/SpotClient/getUniversalTransferRecords.js @@ -0,0 +1,22 @@ +import { SpotClient } from '@siebly/htx-api'; +// or, if require is preferred: +// const { SpotClient } = require('@siebly/htx-api'); + +// This example shows how to call this HTX API endpoint with either node.js, javascript (js) or typescript (ts) with the npm module "@siebly/htx-api" for HTX exchange +// This HTX API SDK is available on npm via "npm install @siebly/htx-api" +// ENDPOINT: /v5/account/universal_transfer_records +// METHOD: GET +// PUBLIC: NO + +const client = new SpotClient({ + apiKey: 'insert_api_key_here', + apiSecret: 'insert_api_secret_here', +}); + +client.getUniversalTransferRecords(params) + .then((response) => { + console.log(response); + }) + .catch((error) => { + console.error(error); + }); diff --git a/examples/apidoc/SpotClient/submitUniversalTransfer.js b/examples/apidoc/SpotClient/submitUniversalTransfer.js new file mode 100644 index 0000000..7104dab --- /dev/null +++ b/examples/apidoc/SpotClient/submitUniversalTransfer.js @@ -0,0 +1,22 @@ +import { SpotClient } from '@siebly/htx-api'; +// or, if require is preferred: +// const { SpotClient } = require('@siebly/htx-api'); + +// This example shows how to call this HTX API endpoint with either node.js, javascript (js) or typescript (ts) with the npm module "@siebly/htx-api" for HTX exchange +// This HTX API SDK is available on npm via "npm install @siebly/htx-api" +// ENDPOINT: /v5/account/universal_transfer +// METHOD: POST +// PUBLIC: NO + +const client = new SpotClient({ + apiKey: 'insert_api_key_here', + apiSecret: 'insert_api_secret_here', +}); + +client.submitUniversalTransfer(params) + .then((response) => { + console.log(response); + }) + .catch((error) => { + console.error(error); + }); diff --git a/llms.txt b/llms.txt index e222040..c2efdc7 100644 --- a/llms.txt +++ b/llms.txt @@ -48,6 +48,8 @@ Notes: ================================================================ Directory Structure ================================================================ +docs/ + HTX_SDK_QUICKSTART_GUIDE.md examples/ Derivatives/ Private/ @@ -164,763 +166,1921 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================ -File: scripts/clean.mjs +File: docs/HTX_SDK_QUICKSTART_GUIDE.md ================ + +# HTX API JavaScript Tutorial for Node.js -================ -File: scripts/postbuild.mjs -================ -async function copySourceDeclarations(directory) + -================ -File: src/lib/websocket/logger.ts -================ -export type LogParams = unknown[]; -⋮---- -// eslint-disable-next-line @typescript-eslint/no-unused-vars -⋮---- -// console.log(_params); -⋮---- -export type DefaultLogger = typeof DefaultLogger; +> [!TIP] +> Read this guide in tutorial format on the Siebly website: [HTX JavaScript REST API and WebSocket Tutorial](https://siebly.io/sdk/htx/javascript/tutorial) -================ -File: src/lib/websocket/rest-client-cache.ts -================ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { AxiosRequestConfig } from 'axios'; -⋮---- -import { FuturesClient } from '../../FuturesClient.js'; -import { SpotClient } from '../../SpotClient.js'; -import { RestClientOptions } from '../requestUtils.js'; -import { DefaultLogger } from './logger.js'; -⋮---- -interface RestClientStore { - spot: SpotClient; - futures: FuturesClient; -} -⋮---- -interface WebSocketTokenCache { - spot?: { token: string; expiresAtMs: number }; - derivatives?: { token: string; expiresAtMs: number }; -} -⋮---- -/** - * Caches REST clients and WebSocket tokens to avoid redundant requests. - */ -export class RestClientCache -⋮---- -public setLogger(logger: DefaultLogger, loggerCategory: object): void -⋮---- -public getSpotRESTClient( - restOptions: RestClientOptions, - requestOptions?: AxiosRequestConfig, -): SpotClient -⋮---- -public async fetchSpotWebSocketToken( - _restOptions: RestClientOptions, - _requestOptions?: AxiosRequestConfig, -): Promise< -⋮---- -// still valid for at least 10s -⋮---- -// const client = this.getSpotRESTClient(restOptions, requestOptions); -const tokenResult = ''; //await client.getWebSocketsToken(); -⋮---- -const token = ''; //tokenResult?.result?.token; -⋮---- -public getDerivativesRESTClient( - restOptions: RestClientOptions, - requestOptions?: AxiosRequestConfig, -): FuturesClient + -================ -File: src/lib/event-emitter.browser.ts -================ -// The browser entry intentionally has no bundled declarations. Its API is the -// same long-standing EventEmitter implementation used by browser bundlers. -// @ts-expect-error -- typed through the SDK-owned constructor below -import BrowserEventEmitter from 'events/events.js'; -⋮---- -import { - EventEmitterConstructor, - EventListener, - EventName, -} from '../types/websockets/ws-portable.js'; -⋮---- -/** Browser adapter backed by the established `events` package. */ -export class EventEmitter extends RuntimeEventEmitter -⋮---- -listenerCount(eventName: EventName, listener?: EventListener): number +This tutorial uses [`@siebly/htx-api`](https://www.npmjs.com/package/@siebly/htx-api), Siebly's Node.js and JavaScript SDK for HTX. It covers Spot and Futures REST API calls, public and private WebSocket streams, production order workflows, WebSocket API commands, API hosts, and proxies. -================ -File: src/lib/event-emitter.ts -================ -import { EventEmitter as NodeEventEmitter } from 'node:events'; -⋮---- -import { EventEmitterConstructor } from '../types/websockets/ws-portable.js'; -⋮---- -/** Node.js adapter. The cast keeps Node declarations out of the public API. */ -export class EventEmitter extends RuntimeEventEmitter +The SDK handles REST API request signing, private WebSocket authentication, compressed HTX messages, heartbeats, reconnects, resubscriptions, and WebSocket API response matching. TypeScript declarations are included for projects that use them. -================ -File: src/lib/https-agent.browser.ts -================ -import type { HttpsRequestOptionsLike } from './https-agent.js'; -⋮---- -/** Browser requests must not force Node-only or preflight-triggering headers. */ -⋮---- -/** Browsers manage connection pooling; HTTPS agent settings do not apply. */ -export function configureHttpsKeepAlive( - _requestOptions: HttpsRequestOptionsLike, - _keepAliveMsecs?: number, -): void +**Key links** -================ -File: src/lib/https-agent.ts -================ -import https from 'node:https'; -⋮---- -export interface HttpsRequestOptionsLike { - httpsAgent?: unknown; -} -⋮---- -/** Configure Node's HTTPS keep-alive agent while preserving caller options. */ -export function configureHttpsKeepAlive( - requestOptions: HttpsRequestOptionsLike, - keepAliveMsecs?: number, -): void +- HTX JavaScript SDK by Siebly: [`@siebly/htx-api`](https://siebly.io/sdk/htx/javascript) +- npm package: [`@siebly/htx-api`](https://www.npmjs.com/package/@siebly/htx-api) +- GitHub repository: [`sieblyio/htx-api`](https://github.com/sieblyio/htx-api) +- SDK examples: [HTX SDK examples](https://siebly.io/examples/HTX) +- SDK endpoint map: [HTX JavaScript endpoint reference](./endpointFunctionList.md) +- HTX API documentation: [HTX API docs](https://www.htx.com/en-us/opend/newApiPages/) +- Trading-system terms: [Siebly glossary](https://siebly.io/reference/glossary) +- More JavaScript and TypeScript SDKs: [Siebly.io](https://siebly.io) -================ -File: src/types/request/wsapi.types.ts -================ -export type HTXSpotOrderType = - | 'buy-market' - | 'sell-market' - | 'buy-limit' - | 'sell-limit' - | 'buy-ioc' - | 'sell-ioc' - | 'buy-limit-maker' - | 'sell-limit-maker' - | 'buy-stop-limit' - | 'sell-stop-limit' - | 'buy-limit-fok' - | 'sell-limit-fok' - | string; -⋮---- -export interface WSAPISpotOrderParams { - 'account-id': number | string; - symbol: string; - type: HTXSpotOrderType; - amount?: string | number; - price?: string | number; - 'market-amount'?: string | number; - source?: string; - 'client-order-id'?: string; - 'self-match-prevent'?: number; - self_match_prevent?: number; - self_match_prevent_new?: - | 'cancel_taker' - | 'cancel_maker' - | 'cancel_both' - | string; - 'stop-price'?: string | number; - operator?: 'gte' | 'lte' | string; - 'coupon-id'?: string; - [key: string]: unknown; +## Why use `@siebly/htx-api`? + +HTX has separate Spot and Futures REST API hosts, several WebSocket paths, and different topic formats across product groups. Private requests also need timestamps and signatures. + +`@siebly/htx-api` handles those mechanics through four clients: + +| Client | Use it for | +| -------------------- | ------------------------------------------------------------------------ | +| `SpotClient` | Spot market data, accounts, orders, margin, transfers, Earn, and wallets | +| `FuturesClient` | USDT-M and Coin-M Futures market data, accounts, positions, and orders | +| `WebsocketClient` | Public and private Spot and Futures subscriptions | +| `WebsocketAPIClient` | Promise-based Spot and Futures trading commands over WebSocket | + +`WebsocketClient` also handles HTX's compressed frames, ping and pong messages, reconnects, cached subscriptions, and resubscription. `WebsocketAPIClient` adds authentication, correlation IDs, response matching, and promise resolution. + +## Install the SDK + +```bash +npm install @siebly/htx-api +``` + +Every example below is plain JavaScript using ESM imports. + +## Create HTX API keys + +Public market data does not require credentials. Private REST API calls, private streams, and WebSocket API commands use an Access Key and Secret Key. + +Create and manage credentials from [HTX API Key Management](https://www.htx.com/apikey/). Configure the key for the work performed by the process: + +- Start with read access while building account views and recovery logic. +- Add trading access for order workflows. +- Bind the key to stable outbound IP addresses when available. +- Keep credentials in a server-side environment or secret manager. +- Allow for key and permission changes to propagate before testing them. +- Review HTX's current inactivity rules for keys with sensitive permissions and no bound IP. + +Set these values in your shell, deployment environment, or secret manager: + +- `HTX_API_KEY` +- `HTX_API_SECRET` + +The examples read these variables directly. Do not log them or include them in browser code. + +## HTX products and request vocabulary + +HTX uses different symbol and request conventions across product groups: + +| Field or value | Example | Meaning | +| ----------------- | ----------------------------------- | --------------------------------------------------------------------------------------- | +| Spot symbol | `btcusdt` | A lowercase Spot market without a separator | +| Linear contract | `BTC-USDT` | A USDT-margined perpetual Futures contract | +| Coin-M perpetual | `BTC-USD` | A coin-margined perpetual Futures contract | +| Delivery alias | `BTC_CW` | A current-week coin-margined delivery contract used by market streams | +| `account-id` | `1234567` | The Spot account returned by `getAccounts()` | +| `client-order-id` | Generated by `generateNewOrderID()` | Your [custom order ID](https://siebly.io/reference/glossary#custom-order-id) | +| `margin_mode` | `cross` or `isolated` | The margin mode for a V5 USDT-M Futures request | +| `position_side` | `both`, `long`, or `short` | The position leg affected by a Futures request | +| `wsKey` | `WS_KEY_MAP.derivativesPrivateV5` | The [WebSocket key](https://siebly.io/reference/glossary#ws-key) selecting a connection | + +Spot balances and orders are scoped to an account ID. Fetch accounts first, then select the active account with `type === 'spot'`. + +USDT-M Futures order `volume` is a contract count. The contract's `contract_size` describes the underlying value represented by one contract. Apply the returned `price_tick` when choosing an order price. + +REST API response bodies are endpoint-specific. Common fields include: + +- `status`, `code`, or `success` for request status +- `data` for account records, symbols, candles, or order results +- `tick` for a ticker or order-book snapshot +- `ts` for a response timestamp + +Read the method's response shape rather than assuming every result uses the same field. + + + +## Start building: first calls and streams + +Run each example on its own. Start with public Spot data, then add private Spot state, Futures data, and live streams. + +### 1. Make public Spot REST API calls + +Public calls need no API key. + + + +```javascript +import { SpotClient } from '@siebly/htx-api'; + +const client = new SpotClient(); + +async function main() { + try { + const time = await client.getTimestamp(); + console.log('Server time:', time.data); + } catch (error) { + console.error('Server time request failed:', error); + } + + try { + const symbols = await client.getTradingSymbols(); + const btcUsdt = symbols.data.find((symbol) => symbol.sc === 'btcusdt'); + console.log('BTC/USDT symbol:', btcUsdt); + } catch (error) { + console.error('Symbol request failed:', error); + } + + try { + const ticker = await client.getTicker({ + symbol: 'btcusdt', + }); + console.log('BTC/USDT ticker:', ticker.tick); + } catch (error) { + console.error('Ticker request failed:', error); + } + + try { + const orderBook = await client.getMarketDepth({ + symbol: 'btcusdt', + depth: 5, + type: 'step0', + }); + console.log('Best bid:', orderBook.tick.bids[0]); + console.log('Best ask:', orderBook.tick.asks[0]); + } catch (error) { + console.error('Order-book request failed:', error); + } + + try { + const candles = await client.getKlines({ + symbol: 'btcusdt', + period: '1min', + size: 10, + }); + console.log('Latest one-minute candle:', candles.data[0]); + } catch (error) { + console.error('Candle request failed:', error); + } } -⋮---- -export type WSAPISpotBatchOrderParams = WSAPISpotOrderParams[]; -⋮---- -export interface WSAPISpotMarginOrderParams extends WSAPISpotOrderParams { - 'repay-amount'?: string | number; - 'trade-purpose'?: number; + +main(); +``` + +`getMarketDepth()` returns price levels as `[price, quantity]` tuples under `tick.bids` and `tick.asks`. `getKlines()` returns candle objects under `data`, with fields such as `id`, `open`, `close`, `low`, `high`, `amount`, `vol`, and `count`. + +### 2. Read private Spot account state + +The SDK signs private REST API calls when credentials are supplied. Start by retrieving the Spot account ID because balance and order calls use it for routing. + + + +```javascript +import { SpotClient } from '@siebly/htx-api'; + +const client = new SpotClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +async function main() { + let spotAccountId; + + try { + const accounts = await client.getAccounts(); + const spotAccount = accounts.data.find( + (account) => account.type === 'spot' && account.state === 'working', + ); + spotAccountId = spotAccount?.id; + console.log('Spot account:', spotAccount); + } catch (error) { + console.error('Account request failed:', error); + return; + } + + if (!spotAccountId) { + console.error('No active Spot account was returned.'); + return; + } + + try { + const balances = await client.getAccountBalance({ + accountId: spotAccountId, + }); + const nonZeroBalances = balances.data.list.filter( + (balance) => Number(balance.balance) !== 0, + ); + console.log('Non-zero Spot balances:', nonZeroBalances); + } catch (error) { + console.error('Balance request failed:', error); + } + + try { + const openOrders = await client.getOpenOrders({ + 'account-id': String(spotAccountId), + symbol: 'btcusdt', + }); + console.log('Open BTC/USDT orders:', openOrders.data); + } catch (error) { + console.error('Open-order request failed:', error); + } + + try { + const recentOrders = await client.getOrderHistory48h({ + symbol: 'btcusdt', + size: 20, + }); + console.log('Recent BTC/USDT orders:', recentOrders.data); + } catch (error) { + console.error('Order-history request failed:', error); + } + + try { + const matches = await client.getMatchResults({ + symbol: 'btcusdt', + size: 20, + }); + console.log('Recent BTC/USDT matches:', matches.data); + } catch (error) { + console.error('Match request failed:', error); + } } -⋮---- -export interface WSAPISpotCancelOrdersParams { - 'order-ids'?: string[]; - 'client-order-ids'?: string[]; + +main(); +``` + +Balance rows identify a currency and balance type. A `trade` balance is available for trading, while a `frozen` balance is reserved by an open order or another account operation. + +Private request failures are rejected as JavaScript errors. The SDK includes the parsed HTX error body and request context while redacting credentials. + +### 3. Work with the USDT-M Futures REST API + +Public USDT-M market data uses the linear-swap methods on `FuturesClient`. Current private account, position, and order state uses HTX's V5 methods. See HTX's [USDT-M API upgrade guidance](https://www.htx.com/support/95034086350701) for the current interface requirements. + + + +```javascript +import { FuturesClient } from '@siebly/htx-api'; + +const client = new FuturesClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +async function main() { + try { + const time = await client.getTimestamp(); + console.log('Futures server time:', time.ts); + } catch (error) { + console.error('Futures time request failed:', error); + } + + try { + const contracts = await client.getLinearSwapContractInfo({ + contract_code: 'BTC-USDT', + }); + console.log('BTC-USDT contract:', contracts.data[0]); + } catch (error) { + console.error('Contract request failed:', error); + } + + try { + const ticker = await client.getLinearSwapTicker({ + contract_code: 'BTC-USDT', + }); + console.log('BTC-USDT ticker:', ticker.tick); + } catch (error) { + console.error('Futures ticker request failed:', error); + } + + try { + const orderBook = await client.getLinearSwapMarketDepth({ + contract_code: 'BTC-USDT', + type: 'step0', + }); + console.log('BTC-USDT best bid:', orderBook.tick.bids[0]); + console.log('BTC-USDT best ask:', orderBook.tick.asks[0]); + } catch (error) { + console.error('Futures order-book request failed:', error); + } + + try { + const funding = await client.getLinearSwapFundingRate({ + contract_code: 'BTC-USDT', + }); + console.log('BTC-USDT funding:', funding.data); + } catch (error) { + console.error('Funding-rate request failed:', error); + } + + try { + const assetMode = await client.getMultiAssetMode(); + console.log('Futures asset mode:', assetMode.data); + } catch (error) { + console.error('Asset-mode request failed:', error); + } + + try { + const account = await client.getMultiAssetAccountBalance(); + console.log('Futures account balance:', account.data); + } catch (error) { + console.error('Futures balance request failed:', error); + } + + try { + const positionMode = await client.getMultiAssetPositionMode(); + console.log('Futures position mode:', positionMode.data); + } catch (error) { + console.error('Position-mode request failed:', error); + } + + try { + const positions = await client.getMultiAssetPositions({ + contract_code: 'BTC-USDT', + }); + console.log('BTC-USDT positions:', positions.data); + } catch (error) { + console.error('Position request failed:', error); + } + + try { + const openOrders = await client.getMultiAssetOpenOrders({ + contract_code: 'BTC-USDT', + limit: 20, + }); + console.log('BTC-USDT open orders:', openOrders.data); + } catch (error) { + console.error('Futures open-order request failed:', error); + } } -⋮---- -export interface WSAPISpotCancelAllOrdersParams { - 'account-id': number | string; - symbol?: string; - types?: string; - side?: 'buy' | 'sell' | string; - size?: number; -} -⋮---- -export type HTXDerivativesDirection = 'buy' | 'sell' | string; -export type HTXDerivativesOffset = 'open' | 'close' | 'both' | string; -⋮---- -export interface WSAPIDerivativesOrderParams { - contract_code?: string; - direction?: HTXDerivativesDirection; - offset?: HTXDerivativesOffset; - price?: string | number; - lever_rate?: number; - volume?: string | number; - order_price_type?: string; - client_order_id?: string | number; - reduce_only?: number; - tp_trigger_price?: string | number; - tp_order_price?: string | number; - tp_order_price_type?: string; - sl_trigger_price?: string | number; - sl_order_price?: string | number; - sl_order_price_type?: string; - self_match_prevent?: string | number; - self_match_prevent_new?: string; - [key: string]: unknown; -} -⋮---- -export type WSAPIDerivativesBatchOrderParams = WSAPIDerivativesOrderParams[]; -⋮---- -export interface WSAPIDerivativesCancelOrderParams { - order_id?: string | number; - order_id_str?: string; - client_order_id?: string | number; - contract_code?: string; - [key: string]: unknown; -} -⋮---- -export interface WSAPIDerivativesCancelAllOrdersParams { - contract_code?: string; - direction?: HTXDerivativesDirection; - offset?: HTXDerivativesOffset; - [key: string]: unknown; -} -⋮---- -export interface WSAPIDerivativesPlaceOrderParams { - contract_code: string; - margin_mode: 'cross' | 'isolated' | string; - position_side?: 'long' | 'short' | 'both' | string; - side: HTXDerivativesDirection; - type: 'market' | 'limit' | 'post_only' | string; - price_match?: string | null; - time_in_force?: 'gtc' | 'ioc' | 'fok' | string; - client_order_id?: string; - price?: string; - volume: string; - reduce_only?: number; - tp_trigger_price?: string; - tp_order_price?: string; - tp_type?: string; - tp_trigger_price_type?: 'last' | 'mark' | string; - sl_trigger_price?: string; - sl_order_price?: string; - sl_type?: string; - sl_trigger_price_type?: 'last' | 'mark' | string; - price_protect?: boolean | string; - self_match_prevent?: string; - [key: string]: unknown; + +main(); +``` + +The public contract response supplies the rules needed for order construction: + +- `contract_size` is the underlying value represented by one contract. +- `price_tick` is the allowed price step. +- `contract_status` identifies the current contract state. +- `support_margin_mode` describes the available margin modes. + +The V5 account calls return the configured asset and position modes. In `single_side` mode, orders use `position_side: 'both'`. In `dual_side` mode, use `long` or `short` to select the intended position leg. + + + +## Build with HTX WebSocket streams + +HTX separates Spot, feed, linear Futures, coin-margined Futures, private data, and order commands across different WebSocket paths. The SDK's [WebSocket keys](https://siebly.io/reference/glossary#ws-key) select the correct connection. + +### 4. Subscribe to public WebSocket streams + +The same `WebsocketClient` can manage several HTX connections at once. + + + +```javascript +import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; + +const client = new WebsocketClient(); + +function main() { + client + .on('open', (event) => { + console.log('WebSocket opened:', event.wsKey); + }) + .on('response', (event) => { + console.log('WebSocket response:', event); + }) + .on('message', (event) => { + console.log('WebSocket message:', event); + }) + .on('reconnecting', (event) => { + console.log('WebSocket reconnecting:', event.wsKey); + }) + .on('reconnected', (event) => { + console.log('WebSocket reconnected:', event.wsKey); + }) + .on('close', (event) => { + console.log('WebSocket closed:', event.wsKey); + }) + .on('exception', (event) => { + console.error('WebSocket exception:', event); + }); + + client.subscribe('market.btcusdt.ticker', WS_KEY_MAP.spotPublic); + + client.subscribe('market.btcusdt.mbp.5', WS_KEY_MAP.spotFeed); + + client.subscribe('market.BTC-USDT.detail', WS_KEY_MAP.linearSwapPublic); } -⋮---- -export type WSAPIDerivativesBatchPlaceOrderParams = - WSAPIDerivativesPlaceOrderParams[]; -⋮---- -export interface WSAPIDerivativesV5CancelOrderParams { - contract_code: string; - order_id?: string; - client_order_id?: string; - [key: string]: unknown; + +main(); +``` + +The `response` event carries [subscription acknowledgements](https://siebly.io/reference/glossary#subscription-acknowledgement). Market data arrives through `message`. Every event includes `wsKey`, which lets one handler identify the connection that emitted it. + +The SDK decompresses HTX messages and answers application-level ping messages. Your handlers receive parsed objects. + +### 5. Subscribe to private WebSocket streams + +This example supplies one API key pair to both private connections. The key needs the corresponding Spot and Futures access. The SDK authenticates each connection before sending its subscriptions. + + + +```javascript +import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; + +const client = new WebsocketClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +function main() { + client + .on('open', (event) => { + console.log('WebSocket opened:', event.wsKey); + }) + .on('authenticated', (event) => { + console.log('WebSocket authenticated:', event.wsKey); + }) + .on('response', (event) => { + console.log('WebSocket response:', event); + }) + .on('message', (event) => { + console.log('Private update:', event); + }) + .on('reconnecting', (event) => { + console.log('WebSocket reconnecting:', event.wsKey); + }) + .on('reconnected', (event) => { + console.log('WebSocket reconnected:', event.wsKey); + }) + .on('close', (event) => { + console.log('WebSocket closed:', event.wsKey); + }) + .on('exception', (event) => { + console.error('WebSocket exception:', event); + }); + + client.subscribe( + ['accounts.update#1', 'orders#btcusdt', 'trade.clearing#btcusdt#1'], + WS_KEY_MAP.spotPrivateV2, + ); + + client.subscribe( + [ + 'account', + { + topic: 'trade', + payload: { + contract_code: 'BTC-USDT', + }, + }, + { + topic: 'positions', + payload: { + contract_code: 'BTC-USDT', + }, + }, + ], + WS_KEY_MAP.derivativesPrivateV5, + ); +} + +main(); +``` + +`accounts.update#1` reports changes to both total and available Spot balances. `orders#btcusdt` reports order changes for the selected market. `trade.clearing#btcusdt#1` includes trade and cancellation events. + +The V5 Futures connection uses `account` for account updates and object-form subscriptions for contract-scoped trade and position data. These private messages provide [private stream confirmation](https://siebly.io/reference/glossary#private-stream-confirmation) after an account or order changes. + +Subscriptions are cached and restored after a reconnect. Call `client.closeAll()` when your application explicitly needs to close its active WebSocket connections. + + + +## Place and manage orders + +Order code should begin with current market metadata. That keeps price precision, amount precision, minimum order value, contract size, and price steps aligned with the exchange. + +An order submission response confirms that HTX accepted the request. Query the order or use a private stream to follow its state. A cancellation response also needs a later state check because the order may change while the cancellation is being processed. This is the [pending confirmation](https://siebly.io/reference/glossary#pending-confirmation) period. + +### 6. Run a Spot REST API order lifecycle + +This example creates a post-only BTC/USDT buy order, queries it, cancels it, and queries it again. It derives the price and amount from current HTX data. + + + +```javascript +import { SpotClient } from '@siebly/htx-api'; + +const client = new SpotClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +async function main() { + let marketStatus; + let symbolRules; + let bestBid; + let spotAccountId; + + try { + const statusResponse = await client.getMarketStatus(); + marketStatus = statusResponse.data.marketStatus; + console.log('Spot market status:', statusResponse.data); + } catch (error) { + console.error('Market-status request failed:', error); + return; + } + + if (marketStatus !== 1) { + console.error('HTX Spot is not in normal trading status.'); + return; + } + + try { + const rulesResponse = await client.getMarketSymbolsSettings({ + symbols: 'btcusdt', + }); + symbolRules = rulesResponse.data.find( + (rules) => rules.symbol === 'btcusdt', + ); + console.log('BTC/USDT order rules:', symbolRules); + } catch (error) { + console.error('Symbol-rule request failed:', error); + return; + } + + if (!symbolRules || symbolRules.state !== 'online') { + console.error('BTC/USDT is not available for trading.'); + return; + } + + try { + const orderBook = await client.getMarketDepth({ + symbol: 'btcusdt', + depth: 5, + type: 'step0', + }); + bestBid = Number(orderBook.tick.bids[0][0]); + console.log('Current best bid:', bestBid); + } catch (error) { + console.error('Order-book request failed:', error); + return; + } + + try { + const accounts = await client.getAccounts(); + const spotAccount = accounts.data.find( + (account) => account.type === 'spot' && account.state === 'working', + ); + spotAccountId = spotAccount?.id; + console.log('Spot account:', spotAccount); + } catch (error) { + console.error('Account request failed:', error); + return; + } + + if (!spotAccountId) { + console.error('No active Spot account was returned.'); + return; + } + + const pricePrecision = Number(symbolRules.pp); + const amountPrecision = Number(symbolRules.ap); + const minimumAmount = Number(symbolRules.minoa); + const minimumOrderValue = Number(symbolRules.minov); + const priceScale = 10 ** pricePrecision; + const amountScale = 10 ** amountPrecision; + const orderPriceNumber = Math.floor(bestBid * 0.9 * priceScale) / priceScale; + const requiredAmount = Math.max( + minimumAmount, + minimumOrderValue / orderPriceNumber, + ); + const orderAmountNumber = + Math.ceil(requiredAmount * 1.05 * amountScale) / amountScale; + const orderPrice = orderPriceNumber.toFixed(pricePrecision); + const orderAmount = orderAmountNumber.toFixed(amountPrecision); + const clientOrderId = client.generateNewOrderID(); + + console.log('Order request:', { + symbol: 'btcusdt', + type: 'buy-limit-maker', + price: orderPrice, + amount: orderAmount, + clientOrderId, + }); + + let orderId; + + try { + const submitted = await client.submitOrder({ + 'account-id': spotAccountId, + symbol: 'btcusdt', + type: 'buy-limit-maker', + amount: orderAmount, + price: orderPrice, + source: 'spot-api', + 'client-order-id': clientOrderId, + }); + orderId = submitted.data; + console.log('Submitted Spot order:', submitted); + } catch (error) { + console.error('Spot order submission failed:', error); + return; + } + + try { + const order = await client.getOrder({ + orderId: String(orderId), + }); + console.log('Spot order after submission:', order.data); + } catch (error) { + console.error('Spot order query failed:', error); + } + + try { + const cancellation = await client.cancelOrderById({ + orderId: String(orderId), + symbol: 'btcusdt', + }); + console.log('Spot cancellation response:', cancellation); + } catch (error) { + console.error('Spot cancellation failed:', error); + } + + try { + const orderAfterCancel = await client.getOrder({ + orderId: String(orderId), + }); + console.log( + 'Spot order after cancellation attempt:', + orderAfterCancel.data, + ); + } catch (error) { + console.error('Post-cancel Spot order query failed:', error); + } } -⋮---- -export interface WSAPIDerivativesV5CancelAllOrdersParams { - contract_code?: string; - margin_mode?: 'cross' | 'isolated' | string; - [key: string]: unknown; + +main(); +``` + +`pp` and `ap` are the price and amount decimal places. `minoa` is the minimum order amount, and `minov` is the minimum order value. The example places its post-only price below the current best bid, rounds the price down, and rounds the amount up. + +`generateNewOrderID()` produces an HTX-compatible client order ID with the SDK's channel prefix. Keep that identifier with your local order record. If the submission result is uncertain, call `getOrderByClientId()` before sending the request again. + +### 7. Run a USDT-M Futures REST API order lifecycle + +This example creates a one-contract V5 post-only order on `BTC-USDT`, queries it, cancels it, and queries it again. It uses the account's current position mode to choose `position_side`. + + + +```javascript +import { FuturesClient } from '@siebly/htx-api'; + +const client = new FuturesClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +async function main() { + let contract; + let bestBid; + let positionMode; + + try { + const contracts = await client.getLinearSwapContractInfo({ + contract_code: 'BTC-USDT', + }); + contract = contracts.data.find((item) => item.contract_code === 'BTC-USDT'); + console.log('BTC-USDT contract:', contract); + } catch (error) { + console.error('Contract request failed:', error); + return; + } + + if (!contract) { + console.error('BTC-USDT contract metadata was not returned.'); + return; + } + + if (contract.contract_status !== 1) { + console.error('BTC-USDT is not available for trading.'); + return; + } + + try { + const orderBook = await client.getLinearSwapMarketDepth({ + contract_code: 'BTC-USDT', + type: 'step0', + }); + bestBid = Number(orderBook.tick.bids[0][0]); + console.log('Current BTC-USDT best bid:', bestBid); + } catch (error) { + console.error('Futures order-book request failed:', error); + return; + } + + try { + const assetMode = await client.getMultiAssetMode(); + console.log('Futures asset mode:', assetMode.data); + } catch (error) { + console.error('Asset-mode request failed:', error); + return; + } + + try { + const balance = await client.getMultiAssetAccountBalance(); + console.log('Futures account balance:', balance.data); + } catch (error) { + console.error('Futures balance request failed:', error); + return; + } + + try { + const modeResponse = await client.getMultiAssetPositionMode(); + positionMode = modeResponse.data.position_mode; + console.log('Futures position mode:', positionMode); + } catch (error) { + console.error('Position-mode request failed:', error); + return; + } + + const priceTick = Number(contract.price_tick); + const tickText = String(contract.price_tick); + const priceDecimals = tickText.includes('.') + ? tickText.split('.')[1].length + : 0; + const orderPriceNumber = Math.floor((bestBid * 0.9) / priceTick) * priceTick; + const orderPrice = orderPriceNumber.toFixed(priceDecimals); + const marginMode = + contract.support_margin_mode === 'isolated' ? 'isolated' : 'cross'; + const positionSide = positionMode === 'dual_side' ? 'long' : 'both'; + const clientOrderId = client.generateNewOrderID(); + + console.log('Futures order request:', { + contract_code: 'BTC-USDT', + margin_mode: marginMode, + position_side: positionSide, + side: 'buy', + type: 'post_only', + price: orderPrice, + volume: '1', + client_order_id: clientOrderId, + }); + + let orderId; + + try { + const submitted = await client.submitMultiAssetOrder({ + contract_code: 'BTC-USDT', + margin_mode: marginMode, + position_side: positionSide, + side: 'buy', + type: 'post_only', + time_in_force: 'gtc', + price: orderPrice, + volume: '1', + reduce_only: 0, + client_order_id: clientOrderId, + }); + orderId = submitted.data.order_id; + console.log('Submitted Futures order:', submitted); + } catch (error) { + console.error('Futures order submission failed:', error); + return; + } + + try { + const order = await client.getMultiAssetOrderInfo({ + contract_code: 'BTC-USDT', + margin_mode: marginMode, + order_id: orderId, + }); + console.log('Futures order after submission:', order.data); + } catch (error) { + console.error('Futures order query failed:', error); + } + + try { + const cancellation = await client.cancelMultiAssetOrder({ + contract_code: 'BTC-USDT', + order_id: orderId, + }); + console.log('Futures cancellation response:', cancellation); + } catch (error) { + console.error('Futures cancellation failed:', error); + } + + try { + const orderAfterCancel = await client.getMultiAssetOrderInfo({ + contract_code: 'BTC-USDT', + margin_mode: marginMode, + order_id: orderId, + }); + console.log( + 'Futures order after cancellation attempt:', + orderAfterCancel.data, + ); + } catch (error) { + console.error('Post-cancel Futures order query failed:', error); + } } -================ -File: src/types/response/wsapi.types.ts -================ -export interface WSAPIRateLimit { - limit?: string; - interval?: string; - remaining?: string; - reset?: string; +main(); +``` + +The order's `volume: '1'` means one contract. Use `contract_size` to translate that into underlying exposure. The V5 order uses the account's configured leverage; query `getMultiAssetLeverage()` and update it separately when the strategy needs a specific setting. + +When `support_margin_mode` is `all`, the example uses cross margin. If a contract reports isolated margin only, it uses isolated margin. In single-side mode, `position_side: 'both'` addresses the net position. In dual-side mode, `long` and `short` are separate position legs. Set `reduce_only: 1` when an order must only reduce an existing position. + + + +## Send orders with the WebSocket API + +`WebsocketAPIClient` exposes promise-based wrappers around HTX trading commands. The first command opens and authenticates the required trade connection. The SDK adds a correlation ID, matches the response, and resolves the promise with that response. + +A successful command reply identifies the accepted order request. Confirm the order's current state with a private stream or a REST API query. + +### 8. Run a Spot WebSocket API order lifecycle + +The Spot WebSocket API example uses the same live metadata as the REST API workflow, then submits and cancels the order over `WS_KEY_MAP.spotTrade`. + + + +```javascript +import { SpotClient, WebsocketAPIClient } from '@siebly/htx-api'; + +const restClient = new SpotClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +const wsApiClient = new WebsocketAPIClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +async function main() { + let symbolRules; + let bestBid; + let spotAccountId; + + try { + const rulesResponse = await restClient.getMarketSymbolsSettings({ + symbols: 'btcusdt', + }); + symbolRules = rulesResponse.data.find( + (rules) => rules.symbol === 'btcusdt', + ); + console.log('BTC/USDT order rules:', symbolRules); + } catch (error) { + console.error('Symbol-rule request failed:', error); + return; + } + + if (!symbolRules || symbolRules.state !== 'online') { + console.error('BTC/USDT is not available for trading.'); + return; + } + + try { + const orderBook = await restClient.getMarketDepth({ + symbol: 'btcusdt', + depth: 5, + type: 'step0', + }); + bestBid = Number(orderBook.tick.bids[0][0]); + console.log('Current best bid:', bestBid); + } catch (error) { + console.error('Order-book request failed:', error); + return; + } + + try { + const accounts = await restClient.getAccounts(); + const spotAccount = accounts.data.find( + (account) => account.type === 'spot' && account.state === 'working', + ); + spotAccountId = spotAccount?.id; + console.log('Spot account:', spotAccount); + } catch (error) { + console.error('Account request failed:', error); + return; + } + + if (!spotAccountId) { + console.error('No active Spot account was returned.'); + return; + } + + const pricePrecision = Number(symbolRules.pp); + const amountPrecision = Number(symbolRules.ap); + const minimumAmount = Number(symbolRules.minoa); + const minimumOrderValue = Number(symbolRules.minov); + const priceScale = 10 ** pricePrecision; + const amountScale = 10 ** amountPrecision; + const orderPriceNumber = Math.floor(bestBid * 0.9 * priceScale) / priceScale; + const requiredAmount = Math.max( + minimumAmount, + minimumOrderValue / orderPriceNumber, + ); + const orderAmountNumber = + Math.ceil(requiredAmount * 1.05 * amountScale) / amountScale; + const orderPrice = orderPriceNumber.toFixed(pricePrecision); + const orderAmount = orderAmountNumber.toFixed(amountPrecision); + const clientOrderId = wsApiClient.generateNewOrderID(); + + let orderId; + + try { + const submitted = await wsApiClient.submitSpotOrder({ + 'account-id': spotAccountId, + symbol: 'btcusdt', + type: 'buy-limit-maker', + amount: orderAmount, + price: orderPrice, + source: 'spot-api', + 'client-order-id': clientOrderId, + }); + const result = submitted.data; + orderId = + typeof result === 'string' + ? result + : (result?.['order-id'] ?? result?.orderId); + console.log('Spot WebSocket API response:', submitted); + } catch (error) { + console.error('Spot WebSocket API submission failed:', error); + return; + } + + if (!orderId) { + console.error('The Spot command response did not include an order ID.'); + return; + } + + try { + const order = await restClient.getOrder({ + orderId: String(orderId), + }); + console.log('Spot order after WebSocket command:', order.data); + } catch (error) { + console.error('Spot order query failed:', error); + } + + try { + const cancellation = await wsApiClient.cancelSpotOrders({ + 'order-ids': [String(orderId)], + }); + console.log('Spot WebSocket cancellation response:', cancellation); + } catch (error) { + console.error('Spot WebSocket cancellation failed:', error); + } + + try { + const orderAfterCancel = await restClient.getOrder({ + orderId: String(orderId), + }); + console.log( + 'Spot order after cancellation attempt:', + orderAfterCancel.data, + ); + } catch (error) { + console.error('Post-cancel Spot order query failed:', error); + } + + wsApiClient.getWSClient().closeAll(); } -⋮---- -export interface WSAPIBaseResponse { - wsKey?: string; - cid?: string; - ch?: TOperation; - op?: TOperation; - status?: 'ok' | 'error' | string; - code?: number; - message?: string | null; - success?: boolean; - data?: TData; - ts?: number; - 'err-code'?: number; - 'err-msg'?: string; - rate_limit?: WSAPIRateLimit; - request?: unknown; -} -⋮---- -export interface WSAPISpotOrderResult { - 'order-id'?: number | string; - 'client-order-id'?: string; - orderId?: number | string; -} -⋮---- -export interface WSAPISpotCancelOrdersResult { - success?: string[]; - failed?: { - 'order-id'?: string; - 'client-order-id'?: string; - 'err-code'?: string; - 'err-msg'?: string; - }[]; -} -⋮---- -export interface WSAPISpotCancelAllOrdersResult { - 'success-count': number; - 'failed-count': number; - 'next-id'?: number | string; -} -⋮---- -export interface WSAPIDerivativesOrderResult { - order_id?: number | string; - order_id_str?: string; - client_order_id?: number | string; -} -⋮---- -export interface WSAPIDerivativesV5OrderResult { - order_id?: string; - client_order_id?: string; + +main(); +``` + +The response can place the order ID directly in `data` or in an order-result object. The SDK resolves the command promise after matching its `cid`. The later REST API queries show the exchange's order state. + +### 9. Run a V5 Futures WebSocket API order lifecycle + +This example sends the V5 `place_order` and `cancel_order` commands through the USDT-M trade connection. + + + +```javascript +import { FuturesClient, WebsocketAPIClient } from '@siebly/htx-api'; + +const restClient = new FuturesClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +const wsApiClient = new WebsocketAPIClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +async function main() { + let contract; + let bestBid; + let positionMode; + + try { + const contracts = await restClient.getLinearSwapContractInfo({ + contract_code: 'BTC-USDT', + }); + contract = contracts.data.find((item) => item.contract_code === 'BTC-USDT'); + console.log('BTC-USDT contract:', contract); + } catch (error) { + console.error('Contract request failed:', error); + return; + } + + if (!contract) { + console.error('BTC-USDT contract metadata was not returned.'); + return; + } + + if (contract.contract_status !== 1) { + console.error('BTC-USDT is not available for trading.'); + return; + } + + try { + const orderBook = await restClient.getLinearSwapMarketDepth({ + contract_code: 'BTC-USDT', + type: 'step0', + }); + bestBid = Number(orderBook.tick.bids[0][0]); + console.log('Current BTC-USDT best bid:', bestBid); + } catch (error) { + console.error('Futures order-book request failed:', error); + return; + } + + try { + const modeResponse = await restClient.getMultiAssetPositionMode(); + positionMode = modeResponse.data.position_mode; + console.log('Futures position mode:', positionMode); + } catch (error) { + console.error('Position-mode request failed:', error); + return; + } + + const priceTick = Number(contract.price_tick); + const tickText = String(contract.price_tick); + const priceDecimals = tickText.includes('.') + ? tickText.split('.')[1].length + : 0; + const orderPriceNumber = Math.floor((bestBid * 0.9) / priceTick) * priceTick; + const orderPrice = orderPriceNumber.toFixed(priceDecimals); + const marginMode = + contract.support_margin_mode === 'isolated' ? 'isolated' : 'cross'; + const positionSide = positionMode === 'dual_side' ? 'long' : 'both'; + const clientOrderId = wsApiClient.generateNewOrderID(); + + let orderId; + + try { + const submitted = await wsApiClient.placeLinearSwapOrder({ + contract_code: 'BTC-USDT', + margin_mode: marginMode, + position_side: positionSide, + side: 'buy', + type: 'post_only', + time_in_force: 'gtc', + price: orderPrice, + volume: '1', + reduce_only: 0, + client_order_id: clientOrderId, + }); + orderId = submitted.data?.order_id; + console.log('Futures WebSocket API response:', submitted); + } catch (error) { + console.error('Futures WebSocket API submission failed:', error); + return; + } + + if (!orderId) { + console.error('The Futures command response did not include an order ID.'); + return; + } + + try { + const order = await restClient.getMultiAssetOrderInfo({ + contract_code: 'BTC-USDT', + margin_mode: marginMode, + order_id: orderId, + }); + console.log('Futures order after WebSocket command:', order.data); + } catch (error) { + console.error('Futures order query failed:', error); + } + + try { + const cancellation = await wsApiClient.cancelLinearSwapV5Order({ + contract_code: 'BTC-USDT', + order_id: orderId, + }); + console.log('Futures WebSocket cancellation response:', cancellation); + } catch (error) { + console.error('Futures WebSocket cancellation failed:', error); + } + + try { + const orderAfterCancel = await restClient.getMultiAssetOrderInfo({ + contract_code: 'BTC-USDT', + margin_mode: marginMode, + order_id: orderId, + }); + console.log( + 'Futures order after cancellation attempt:', + orderAfterCancel.data, + ); + } catch (error) { + console.error('Post-cancel Futures order query failed:', error); + } + + wsApiClient.getWSClient().closeAll(); } -================ -File: src/types/websockets/ws-api.ts -================ -import { WS_KEY_MAP } from '../../lib/websocket/websocket-util.js'; -import { - WSAPIDerivativesBatchOrderParams, - WSAPIDerivativesBatchPlaceOrderParams, - WSAPIDerivativesCancelAllOrdersParams, - WSAPIDerivativesCancelOrderParams, - WSAPIDerivativesOrderParams, - WSAPIDerivativesPlaceOrderParams, - WSAPIDerivativesV5CancelAllOrdersParams, - WSAPIDerivativesV5CancelOrderParams, - WSAPISpotBatchOrderParams, - WSAPISpotCancelAllOrdersParams, - WSAPISpotCancelOrdersParams, - WSAPISpotMarginOrderParams, - WSAPISpotOrderParams, -} from '../request/wsapi.types.js'; -import { - WSAPIBaseResponse, - WSAPIDerivativesOrderResult, - WSAPIDerivativesV5OrderResult, - WSAPISpotCancelAllOrdersResult, - WSAPISpotCancelOrdersResult, - WSAPISpotOrderResult, -} from '../response/wsapi.types.js'; -⋮---- -export type Exact = T extends object ? T & Record : T; -⋮---- -export interface WsRequestOperation { - op?: string; - action?: string; - topic?: TWSTopic; - ch?: TWSTopic; - id?: string | number; - cid?: string; -} -⋮---- -export interface WSAPIAuthenticationConfirmedFromServer { - action?: 'req'; - op?: 'auth'; - code?: number; - ch?: 'auth'; - 'err-code'?: number; - data?: Record; -} -⋮---- -export type WSAPIOperation = (typeof WS_API_Operations)[number]; -⋮---- -export type WSAPISpotOperation = - | 'create-order' - | 'create-batchorder' - | 'create-margin-order' - | 'cancelall' - | 'cancel'; -⋮---- -export type WSAPILegacyDerivativesOperation = - | 'create_order' - | 'create_cross_order' - | 'create_batchorder' - | 'create_cross_batchorder' - | 'cancel' - | 'cross_cancel' - | 'cancelall' - | 'cross_cancelall'; -⋮---- -export type WSAPIDerivativesV5Operation = - | 'place_order' - | 'place_batch_orders' - | 'cancel_order' - | 'cancel_batch_orders' - | 'cancel_all_orders'; -⋮---- -export type WSAPIDerivativesOperation = - | WSAPILegacyDerivativesOperation - | WSAPIDerivativesV5Operation; -⋮---- -export interface WSAPIWsKeyTopicMap { - [WS_KEY_MAP.spotTrade]: WSAPISpotOperation; - [WS_KEY_MAP.linearSwapTrade]: WSAPIDerivativesOperation; - [WS_KEY_MAP.coinDeliveryTrade]: WSAPILegacyDerivativesOperation; - [WS_KEY_MAP.coinSwapTrade]: WSAPILegacyDerivativesOperation; -} -⋮---- -export type WSAPIWsKey = keyof WSAPIWsKeyTopicMap; -⋮---- -export interface HTXSpotWSAPIRequest< - TOperation extends WSAPISpotOperation = WSAPISpotOperation, - TParams = unknown, -> { - cid: string; - ch: TOperation; - params?: TParams; -} -⋮---- -export interface HTXDerivativesWSAPIRequest< - TOperation extends WSAPIDerivativesOperation = WSAPIDerivativesOperation, - TParams = unknown, -> { - op: TOperation; - cid: string; - data?: TParams; -} -⋮---- -export type HTXWSAPIRequest = HTXSpotWSAPIRequest | HTXDerivativesWSAPIRequest; -⋮---- -export type WSAPIResponse< - TData = unknown, - TOperation extends string = string, -> = WSAPIBaseResponse; -⋮---- -export interface WSAPITopicRequestParamMap { - 'create-order': WSAPISpotOrderParams; - 'create-batchorder': WSAPISpotBatchOrderParams; - 'create-margin-order': WSAPISpotMarginOrderParams; - cancelall: - | WSAPISpotCancelAllOrdersParams - | WSAPIDerivativesCancelAllOrdersParams; - cancel: WSAPISpotCancelOrdersParams | WSAPIDerivativesCancelOrderParams; +main(); +``` - create_order: WSAPIDerivativesOrderParams; - create_cross_order: WSAPIDerivativesOrderParams; - create_batchorder: WSAPIDerivativesBatchOrderParams; - create_cross_batchorder: WSAPIDerivativesBatchOrderParams; - cross_cancel: WSAPIDerivativesCancelOrderParams; - cross_cancelall: WSAPIDerivativesCancelAllOrdersParams; +`placeLinearSwapOrder()` sends the V5 `place_order` operation. `cancelLinearSwapV5Order()` sends `cancel_order`. The command response includes fields such as `cid`, `op`, `status`, `code`, `message`, `success`, and `data`, depending on the HTX response. - place_order: WSAPIDerivativesPlaceOrderParams; - place_batch_orders: WSAPIDerivativesBatchPlaceOrderParams; - cancel_order: WSAPIDerivativesV5CancelOrderParams; - cancel_batch_orders: WSAPIDerivativesV5CancelOrderParams[]; - cancel_all_orders: WSAPIDerivativesV5CancelAllOrdersParams; -} -⋮---- -export interface WSAPITopicResponseMap { - 'create-order': WSAPIResponse; - 'create-batchorder': WSAPIResponse< - WSAPISpotOrderResult[], - 'create-batchorder' - >; - 'create-margin-order': WSAPIResponse< - WSAPISpotOrderResult, - 'create-margin-order' - >; - cancelall: WSAPIResponse< - WSAPISpotCancelAllOrdersResult | WSAPIDerivativesOrderResult, - 'cancelall' - >; - cancel: WSAPIResponse< - WSAPISpotCancelOrdersResult | WSAPIDerivativesOrderResult, - 'cancel' - >; + - create_order: WSAPIResponse; - create_cross_order: WSAPIResponse< - WSAPIDerivativesOrderResult, - 'create_cross_order' - >; - create_batchorder: WSAPIResponse< - WSAPIDerivativesOrderResult[], - 'create_batchorder' - >; - create_cross_batchorder: WSAPIResponse< - WSAPIDerivativesOrderResult[], - 'create_cross_batchorder' - >; - cross_cancel: WSAPIResponse; - cross_cancelall: WSAPIResponse< - WSAPIDerivativesOrderResult, - 'cross_cancelall' - >; +## Explore more REST API methods - place_order: WSAPIResponse; - place_batch_orders: WSAPIResponse< - WSAPIDerivativesV5OrderResult[], - 'place_batch_orders' - >; - cancel_order: WSAPIResponse; - cancel_batch_orders: WSAPIResponse< - WSAPIDerivativesV5OrderResult[], - 'cancel_batch_orders' - >; - cancel_all_orders: WSAPIResponse< - WSAPIDerivativesV5OrderResult, - 'cancel_all_orders' - >; -} +`SpotClient` and `FuturesClient` map JavaScript methods to HTX endpoints. The [complete endpoint map](./endpointFunctionList.md) lists every method, its authentication requirement, HTTP method, and endpoint. -================ -File: src/types/websockets/ws-portable.ts -================ -/* eslint-disable @typescript-eslint/no-explicit-any */ -⋮---- -/** A binary WebSocket payload supported in both Node.js and browsers. */ -export type WebSocketBinaryData = ArrayBuffer | ArrayBufferView; -⋮---- -/** Standard WebSocket ready-state values shared by Node.js and browsers. */ -⋮---- -/** - * The portable portion of a WebSocket connection exposed by this SDK. - * - * Node.js `ws` instances and browser-native `WebSocket` instances both satisfy - * this interface. Runtime-specific extensions remain available through - * `wsOptions`, without making consumers install Node.js or `ws` declarations. - */ -export interface WebSocketLike { - readonly readyState: number; +### Spot REST API groups - binaryType?: string; - onopen: ((event: any) => any) | null; - onmessage: ((event: any) => any) | null; - onerror: ((event: any) => any) | null; - onclose: ((event: any) => any) | null; +| Area | Representative methods | +| ------------------------ | -------------------------------------------------------------------------------------------------------- | +| Market status and time | `getMarketStatus()`, `getTimestamp()` | +| Symbols and currencies | `getTradingSymbols()`, `getCurrencies()`, `getMarketSymbolsSettings()` | +| Market data | `getTicker()`, `getTickers()`, `getMarketDepth()`, `getKlines()`, `getHistoryTrades()` | +| Accounts and balances | `getAccounts()`, `getAccountBalance()`, `getAccountValuation()`, `getAccountLedger()` | +| Spot orders | `submitOrder()`, `getOrder()`, `getOpenOrders()`, `getOrderHistory48h()`, `getMatchResults()` | +| Conditional orders | `placeConditionalOrder()`, `getOpenConditionalOrders()`, `cancelConditionalOrders()` | +| Margin | `getMarginAccountBalance()`, `getCrossMarginBalance()`, `requestMarginLoan()`, `repayCrossMarginLoan()` | +| Transfers | `submitTransfer()`, `submitV2AccountTransfer()`, `submitFuturesTransfer()` | +| Deposits and withdrawals | `getDepositAddress()`, `getWithdrawQuota()`, `getDepositWithdrawHistory()`, `submitWithdraw()` | +| Subaccounts | `getSubUserList()`, `getSubUserAccounts()`, `submitSubUserTransfer()`, `getSubUsersAggregatedBalance()` | +| Broker and rebates | `getBrokerUserRebateStatus()`, `setBrokerSubUserFeeRate()`, `getReferralRebateHistory()` | +| Earn and P2P | `getEarnProjectList()`, `getEarnUserAssets()`, `earnSubscribe()`, `earnRedeem()`, `getP2POrderHistory()` | - /** Runtime implementations accept their own wider binary-data variants. */ - send(data: any): void; - close(code?: number, reason?: string): void; +Transfer, withdrawal, lending, and account-management methods move funds or alter account configuration. Read the current endpoint rules and permissions before calling them. - /** Node.js `ws` extension. Undefined for browser-native sockets. */ - ping?: () => void; - /** Node.js `ws` extension. Undefined for browser-native sockets. */ - terminate?: () => void; - /** Node.js-style event API. Undefined for browser-native sockets. */ - on?: (event: string, listener: (event: unknown) => void) => unknown; +### Futures REST API groups - /** Internal connection identifier attached by the SDK. */ - wsKey?: string; -} -⋮---- -/** Runtime implementations accept their own wider binary-data variants. */ -send(data: any): void; -close(code?: number, reason?: string): void; -⋮---- -/** Node.js `ws` extension. Undefined for browser-native sockets. */ -⋮---- -/** Node.js `ws` extension. Undefined for browser-native sockets. */ -⋮---- -/** Node.js-style event API. Undefined for browser-native sockets. */ -⋮---- -/** Internal connection identifier attached by the SDK. */ -⋮---- -/** Portable shape for a WebSocket message event. */ -export interface MessageEventLike { - target: unknown; - type: string; - data: TData; -} -⋮---- -/** - * Connection options forwarded to the underlying WebSocket implementation. - * Known cross-runtime options are documented while the index signature keeps - * runtime-specific `ws`/agent options available without exposing their types. - */ -export interface WSConnectionOptions { - protocols?: string | string[] | undefined; - agent?: unknown; - headers?: Record | undefined; - followRedirects?: boolean | undefined; - handshakeTimeout?: number | undefined; - maxPayload?: number | undefined; - origin?: string | undefined; - perMessageDeflate?: boolean | object | undefined; - rejectUnauthorized?: boolean | undefined; - [key: string]: any; +| Area | Representative methods | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| USDT-M market data | `getLinearSwapContractInfo()`, `getLinearSwapTicker()`, `getLinearSwapMarketDepth()`, `getLinearSwapFundingRate()` | +| V5 account state | `getMultiAssetMode()`, `getMultiAssetAccountBalance()`, `getMultiAssetBills()` | +| V5 orders | `submitMultiAssetOrder()`, `getMultiAssetOrderInfo()`, `getMultiAssetOpenOrders()`, `getMultiAssetOrderHistory()` | +| V5 positions | `getMultiAssetPositions()`, `getMultiAssetPositionMode()`, `getMultiAssetLeverage()`, `adjustMultiAssetMargin()` | +| V5 collateral and risk | `getMultiAssetCollateralAssets()`, `getMultiAssetRiskLimit()`, `getMultiAssetRiskLimitTiers()`, `getMultiAssetMarketRiskLimit()` | +| Coin-M delivery | `getCoinMDeliveryContractInfo()`, `getCoinMDeliveryTicker()`, `getCoinMDeliveryAccountInfo()`, `submitCoinMDeliveryOrder()` | +| Coin-M perpetuals | `getCoinMPerpContractInfo()`, `getCoinMPerpTicker()`, `getCoinMPerpAccountInfo()`, `submitCoinMPerpOrder()` | +| Conditional order tools | Trigger, take-profit, stop-loss, trailing, and lightning-close methods for the relevant Futures product | +| Futures subaccounts | Permission, asset, position, transfer, and account-state methods for USDT-M and Coin-M products | +| Copy trading | Copy-trader configuration, instruments, followers, orders, positions, and profit-sharing methods | + +Keep the product prefix visible when selecting a Futures method. `getLinearSwap...`, `getMultiAsset...`, `getCoinMDelivery...`, and `getCoinMPerp...` address different interfaces or contract families. + +### WebSocket connection map + +`WS_KEY_MAP` contains the supported connection keys: + +| WebSocket key | Connection purpose | +| ---------------------- | ---------------------------------------------- | +| `spotPublic` | Spot market topics | +| `spotFeed` | Spot MBP and feed topics | +| `spotPrivateV2` | Spot account, order, and trade-clearing topics | +| `spotTrade` | Spot WebSocket API commands | +| `linearSwapPublic` | USDT-M Futures market topics | +| `derivativesPrivateV5` | V5 USDT-M account, trade, and position topics | +| `linearSwapTrade` | USDT-M Futures WebSocket API commands | +| `coinDeliveryPublic` | Coin-margined delivery market topics | +| `coinDeliveryPrivate` | Coin-margined delivery private topics | +| `coinDeliveryTrade` | Coin-margined delivery WebSocket API commands | +| `coinSwapPublic` | Coin-margined perpetual market topics | +| `coinSwapPrivate` | Coin-margined perpetual private topics | +| `coinSwapTrade` | Coin-margined perpetual WebSocket API commands | +| `derivativesIndex` | Index, mark-price, and premium-index topics | +| `derivativesSystem` | Futures system and heartbeat topics | + +Use the exported constants instead of copying their string values so the intended connection stays clear. + + + +## Recover private state after a reconnect + +A WebSocket reconnect restores transport and subscriptions. It does not prove that every private event sent during the gap reached your application. + +Use the REST API to rebuild the affected [account state](https://siebly.io/reference/glossary#accountstate). This process is often called [REST API hydration](https://siebly.io/reference/glossary#rest-hydration). Limit the reload to the product and account affected by the reconnect, following [Scoped Recovery](https://siebly.io/reference/glossary#scoped-recovery). + +The example below replaces its Spot snapshot only after the account, balance, open-order, and match requests all succeed. + + + +```javascript +import { SpotClient, WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; + +const restClient = new SpotClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +const wsClient = new WebsocketClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, +}); + +let spotState = { + accountId: undefined, + balances: [], + openOrders: [], + recentMatches: [], +}; + +async function recoverSpotState() { + let spotAccountId; + let balances; + let openOrders; + let recentMatches; + + try { + const accountsResponse = await restClient.getAccounts(); + const spotAccount = accountsResponse.data.find( + (account) => account.type === 'spot' && account.state === 'working', + ); + spotAccountId = spotAccount?.id; + } catch (error) { + console.error('Recovery account request failed:', error); + return; + } + + if (!spotAccountId) { + console.error('Recovery could not find an active Spot account.'); + return; + } + + try { + const balanceResponse = await restClient.getAccountBalance({ + accountId: spotAccountId, + }); + balances = balanceResponse.data.list; + } catch (error) { + console.error('Recovery balance request failed:', error); + return; + } + + try { + const orderResponse = await restClient.getOpenOrders({ + 'account-id': String(spotAccountId), + symbol: 'btcusdt', + }); + openOrders = orderResponse.data; + } catch (error) { + console.error('Recovery open-order request failed:', error); + return; + } + + try { + const matchResponse = await restClient.getMatchResults({ + symbol: 'btcusdt', + size: 100, + }); + recentMatches = matchResponse.data; + } catch (error) { + console.error('Recovery match request failed:', error); + return; + } + + const replacementState = { + accountId: spotAccountId, + balances, + openOrders, + recentMatches, + }; + + spotState = replacementState; + console.log('Recovered Spot state:', spotState); } -⋮---- -export type EventName = string | symbol; -export type EventListener = (...args: any[]) => void; -⋮---- -/** SDK-owned instance surface compatible with Node's EventEmitter. */ -export interface EventEmitterLike { - addListener(eventName: EventName, listener: EventListener): this; - on(eventName: EventName, listener: EventListener): this; - once(eventName: EventName, listener: EventListener): this; - prependListener(eventName: EventName, listener: EventListener): this; - prependOnceListener(eventName: EventName, listener: EventListener): this; - off(eventName: EventName, listener: EventListener): this; - removeListener(eventName: EventName, listener: EventListener): this; - removeAllListeners(eventName?: EventName): this; - setMaxListeners(count: number): this; - getMaxListeners(): number; - listeners(eventName: EventName): EventListener[]; - rawListeners(eventName: EventName): EventListener[]; - emit(eventName: EventName, ...args: any[]): boolean; - listenerCount(eventName: EventName, listener?: EventListener): number; - eventNames(): EventName[]; + +function main() { + wsClient + .on('reconnected', async (event) => { + if (event.wsKey !== WS_KEY_MAP.spotPrivateV2) { + return; + } + + await recoverSpotState(); + }) + .on('message', (event) => { + console.log('Private update:', event); + }) + .on('exception', (event) => { + console.error('WebSocket exception:', event); + }); + + wsClient.subscribe( + ['accounts.update#1', 'orders#btcusdt', 'trade.clearing#btcusdt#1'], + WS_KEY_MAP.spotPrivateV2, + ); } -⋮---- -addListener(eventName: EventName, listener: EventListener): this; -on(eventName: EventName, listener: EventListener): this; -once(eventName: EventName, listener: EventListener): this; -prependListener(eventName: EventName, listener: EventListener): this; -prependOnceListener(eventName: EventName, listener: EventListener): this; -off(eventName: EventName, listener: EventListener): this; -removeListener(eventName: EventName, listener: EventListener): this; -removeAllListeners(eventName?: EventName): this; -setMaxListeners(count: number): this; -getMaxListeners(): number; -listeners(eventName: EventName): EventListener[]; -rawListeners(eventName: EventName): EventListener[]; -emit(eventName: EventName, ...args: any[]): boolean; -listenerCount(eventName: EventName, listener?: EventListener): number; -eventNames(): EventName[]; -⋮---- -/** Constructor abstraction used to hide runtime-specific emitter declarations. */ -export interface EventEmitterConstructor { - new (options?: { captureRejections?: boolean }): EventEmitterLike; - readonly prototype: EventEmitterLike; - defaultMaxListeners: number; - listenerCount(emitter: EventEmitterLike, eventName: EventName): number; + +main(); +``` + +Call the same recovery function during application startup to create the initial snapshot before applying stream updates. For broader service design, see [Exchange State](https://siebly.io/reference/exchange-state) and [Runtime Workflows](https://siebly.io/reference/runtime-workflows). + +For Futures recovery, follow the same sequence with V5 account balance, positions, open orders, and recent fills. Trigger that reload only when `derivativesPrivateV5` reconnects. + + + +## Choose standard or AWS API hosts + +`SpotClient`, `FuturesClient`, and `WebsocketClient` use HTX's AWS hosts by default. The standard hosts remain available through explicit routing options. + +| Product and network | REST API base URL | Main WebSocket paths | +| ------------------- | ---------------------------- | -------------------------------------------------------------- | +| Spot AWS | `https://api-aws.huobi.pro` | `/ws`, `/feed`, `/ws/v2`, `/ws/trade` | +| Spot standard | `https://api.huobi.pro` | `/ws`, `/feed`, `/ws/v2`, `/ws/trade` | +| Futures AWS | `https://api.hbdm.vn` | `/linear-swap-ws`, `/ws/v5/notification`, `/linear-swap-trade` | +| Futures standard | `https://api.hbdm.com` | `/linear-swap-ws`, `/ws/v5/notification`, `/linear-swap-trade` | +| Futures alternative | `https://api.btcgateway.pro` | REST API alternative supported by `FuturesClient` | + +Coin-margined delivery, coin-margined perpetual, index, and system connections use product-specific paths on the selected Futures WebSocket host. + +Use `baseUrlKey` for the REST API and `wsEnvironment` for WebSockets: + + + +```javascript +import { + FuturesClient, + SpotClient, + WebsocketClient, + WS_KEY_MAP, +} from '@siebly/htx-api'; + +const standardSpotClient = new SpotClient({ + baseUrlKey: 'spot', +}); + +const standardFuturesClient = new FuturesClient({ + baseUrlKey: 'futures', +}); + +const standardWsClient = new WebsocketClient({ + wsEnvironment: 'standard', +}); + +async function main() { + try { + const spotTime = await standardSpotClient.getTimestamp(); + console.log('Standard Spot host time:', spotTime.data); + } catch (error) { + console.error('Standard Spot host request failed:', error); + } + + try { + const futuresTime = await standardFuturesClient.getTimestamp(); + console.log('Standard Futures host time:', futuresTime.ts); + } catch (error) { + console.error('Standard Futures host request failed:', error); + } + + standardWsClient + .on('response', (event) => { + console.log('Standard WebSocket response:', event); + }) + .on('message', (event) => { + console.log('Standard WebSocket message:', event); + }) + .on('exception', (event) => { + console.error('Standard WebSocket exception:', event); + }); + + standardWsClient.subscribe('market.btcusdt.ticker', WS_KEY_MAP.spotPublic); } -⋮---- -listenerCount(emitter: EventEmitterLike, eventName: EventName): number; -================ -File: src/types/websockets/ws-subscriptions.ts -================ -/** - * HTX websocket topics are string templates, e.g.: - * - * - `market.btcusdt.kline.1min` - * - `market.btcusdt.mbp.150` - * - `orders#btcusdt` - * - `orders.btc-usdt` - * - * The exchange publishes a large and evolving topic surface, so the SDK keeps - * the topic type open while exporting common topic groups for discovery and - * private/public routing. - */ -⋮---- -export type WSSpotPublicTopic = (typeof WS_SPOT_PUBLIC_TOPICS)[number]; -export type WSSpotPrivateTopic = (typeof WS_SPOT_PRIVATE_TOPICS)[number]; -export type WSDerivativesPublicTopic = - (typeof WS_DERIVATIVES_PUBLIC_TOPICS)[number]; -export type WSDerivativesPrivateTopic = - (typeof WS_DERIVATIVES_PRIVATE_TOPICS)[number]; -⋮---- -export type WSTopic = - | WSSpotPublicTopic - | WSSpotPrivateTopic - | WSDerivativesPublicTopic - | WSDerivativesPrivateTopic - | string; -⋮---- -export function isPrivateTopic(topic: string): boolean +main(); +``` -================ -File: .npmrc -================ -min-release-age=30 +For AWS routing, use `baseUrlKey: 'spotAWS'`, `baseUrlKey: 'futuresAWS'`, and `wsEnvironment: 'aws'`. The alternative Futures REST API host uses `baseUrlKey: 'futuresAlt1'`. A WebSocket client also follows `restOptions.baseUrlKey` when `wsEnvironment` is omitted. -================ -File: .prettierrc -================ -{ - "tabWidth": 2, - "singleQuote": true, - "trailingComma": "all" +Use `baseUrl` or `wsUrl` only when you need an explicit custom endpoint. Keep that override visible in configuration and deployment logs. + + + +## Use a proxy with the REST API and WebSockets + +Proxy agents work with the REST API clients, streaming WebSockets, and `WebsocketAPIClient`. Install the agent packages used by these examples: + +```bash +npm install https-proxy-agent socks-proxy-agent +``` + +For more background, see [Using a proxy with Siebly SDKs](https://siebly.io/blog/using-proxy-with-siebly-sdks). + +### HTTP or HTTPS proxy + +`HttpsProxyAgent` accepts both `http://` and `https://` proxy URLs. Pass it as `httpsAgent` for the REST API and as `wsOptions.agent` for WebSockets. + + + +```javascript +import { + SpotClient, + WebsocketAPIClient, + WebsocketClient, + WS_KEY_MAP, +} from '@siebly/htx-api'; +import { HttpsProxyAgent } from 'https-proxy-agent'; + +if (!process.env.HTX_PROXY_URL) { + throw new Error('Set HTX_PROXY_URL to an HTTP or HTTPS proxy URL.'); } -================ -File: tsconfig.cjs.json -================ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "commonjs", - "moduleResolution": "bundler", - "outDir": "dist/cjs", - "rootDir": "src", - "target": "esnext" +const agent = new HttpsProxyAgent(process.env.HTX_PROXY_URL); + +const restClient = new SpotClient( + { + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, }, - "include": ["src/**/*.*"] -} + { + httpsAgent: agent, + proxy: false, + }, +); -================ -File: tsconfig.esm.json -================ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "outDir": "dist/mjs", - "rootDir": "src", - "target": "esnext" +const wsClient = new WebsocketClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, + wsOptions: { + agent, }, - "include": ["src/**/*.*"] -} +}); -================ -File: tsconfig.extensions.json -================ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": false, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true +const wsApiClient = new WebsocketAPIClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, + wsOptions: { + agent, }, - "include": ["src/**/*.*"] +}); + +async function main() { + try { + const time = await restClient.getTimestamp(); + console.log('REST API request through proxy:', time.data); + } catch (error) { + console.error('Proxied REST API request failed:', error); + } + + wsClient + .on('response', (event) => { + console.log('Proxied WebSocket response:', event); + }) + .on('message', (event) => { + console.log('Proxied WebSocket message:', event); + }) + .on('exception', (event) => { + console.error('Proxied WebSocket exception:', event); + }); + + wsClient.subscribe('market.btcusdt.ticker', WS_KEY_MAP.spotPublic); + + console.log( + 'WebSocket API client configured:', + Boolean(wsApiClient.getWSClient()), + ); } -================ -File: tsconfig.linting.json -================ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "target": "esnext", - "rootDir": ".", - "allowJs": true, - "noEmit": true, - "types": ["node", "jest"] +main(); +``` + +The same agent can be reused by several clients when they use the same proxy route. + +### SOCKS5 proxy + +Use `SocksProxyAgent` for a `socks5://` or `socks5h://` endpoint. + + + +```javascript +import { + FuturesClient, + WebsocketAPIClient, + WebsocketClient, + WS_KEY_MAP, +} from '@siebly/htx-api'; +import { SocksProxyAgent } from 'socks-proxy-agent'; + +if (!process.env.HTX_SOCKS_PROXY_URL) { + throw new Error('Set HTX_SOCKS_PROXY_URL to a SOCKS5 proxy URL.'); +} + +const agent = new SocksProxyAgent(process.env.HTX_SOCKS_PROXY_URL); + +const restClient = new FuturesClient( + { + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, }, - "include": [ - "src/**/*.*", - "test/**/*.*", - "examples/**/*.*", - "eslint.config.cjs", - "jest.config.cjs" - ] + { + httpsAgent: agent, + proxy: false, + }, +); + +const wsClient = new WebsocketClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, + wsOptions: { + agent, + }, +}); + +const wsApiClient = new WebsocketAPIClient({ + apiKey: process.env.HTX_API_KEY, + apiSecret: process.env.HTX_API_SECRET, + wsOptions: { + agent, + }, +}); + +async function main() { + try { + const time = await restClient.getTimestamp(); + console.log('Futures REST API request through SOCKS5:', time.ts); + } catch (error) { + console.error('Proxied Futures REST API request failed:', error); + } + + wsClient + .on('response', (event) => { + console.log('Proxied Futures WebSocket response:', event); + }) + .on('message', (event) => { + console.log('Proxied Futures WebSocket message:', event); + }) + .on('exception', (event) => { + console.error('Proxied Futures WebSocket exception:', event); + }); + + wsClient.subscribe('market.BTC-USDT.detail', WS_KEY_MAP.linearSwapPublic); + + console.log( + 'Futures WebSocket API client configured:', + Boolean(wsApiClient.getWSClient()), + ); } +main(); +``` + +For a private proxy check, call a read-only account endpoint and confirm that the proxy's egress IP is included in the API key's IP list. + + + +## Production checklist + +### Keep request time accurate + +Private requests use the current timestamp. Synchronize the host clock. If an application maintains its own measured offset, pass a `customTimestampFn` to the REST API client and call `setTimeOffsetMs()` on `WebsocketAPIClient` or its embedded `WebsocketClient`. + +### Read current product rules + +Spot price precision, amount precision, minimum amount, minimum order value, and trading state can change. Futures contract status, contract size, price tick, available leverage, and risk limits can also change. Read current metadata before building an order. + +### Keep account modes explicit + +Record the Futures asset mode, margin mode, and position mode with order intent. A `position_side` suitable for single-side mode is not the same request as a long or short leg in dual-side mode. + +### Respect rate limits + +Rate limits vary by endpoint and may be shared by an IP address or account. Apply backoff to retryable reads, limit repeated metadata requests, and cache stable public information for an appropriate period. + +### Reconcile client order IDs + +Use `generateNewOrderID()` for application-owned orders. Persist the ID before submission. After a timeout or dropped response, query by client order ID before deciding whether to retry. + +### Confirm final order state + +A REST API or WebSocket API submission response confirms the request result. Use the private order stream or an order query to confirm whether the order is open, partially filled, filled, canceled, or rejected. + +### Keep streams healthy + +Monitor `exception`, `reconnecting`, `reconnected`, and `close`. Track the last private update received for each `wsKey`, and run scoped recovery after a gap. + +### Use dead-man switches where appropriate + +Spot provides `setCancelAllAfter()`. V5 Futures provides `setMultiAssetCancelAfter()`. Refresh the selected control at the interval required by HTX and monitor its result. + +### Scope credentials and network access + +Use separate keys when processes need different permissions or egress IPs. Keep credentials out of logs and frontend bundles. Monitor proxy availability and confirm the expected egress IP during deployment. + +### Check current Futures access + +Futures availability, leverage, and API order permissions depend on the account and current HTX requirements. Review [HTX's current Futures API permission guidance](https://www.htx.com/en-in/support/35029113829881/) before deploying order code. + +## FAQ + +### Does `@siebly/htx-api` work with plain JavaScript? + +Yes. Every example in this guide is JavaScript. TypeScript declarations are included for TypeScript projects. + +### Which REST API client should I use? + +Use `SpotClient` for Spot, margin, wallet, Earn, and Spot account operations. Use `FuturesClient` for USDT-M and Coin-M Futures. + +### Why do some REST API results use `data` while others use `tick`? + +HTX endpoints return product-specific response shapes. The SDK returns the parsed endpoint body rather than moving every payload into a new common wrapper. + +### Why does a Spot order need an account ID? + +HTX can return several account records. The account ID selects the Spot, margin, or other account that owns the request. Retrieve it with `getAccounts()`. + +### Why are Spot and Futures symbols formatted differently? + +Spot methods use lowercase compact symbols such as `btcusdt`. USDT-M Futures methods use contract codes such as `BTC-USDT`. Coin-margined products use their documented contract codes and aliases. + +### What is the difference between `response` and `message` WebSocket events? + +`response` contains authentication, subscription, request, or command replies. `message` contains data published for a subscribed topic. + +### What does `wsKey` mean? + +The [WebSocket key](https://siebly.io/reference/glossary#ws-key) identifies the connection that received an event. It also selects the connection used by `subscribe()`. + +### Why does the SDK default to AWS hosts? + +HTX documents AWS endpoints for deployments that benefit from that network route. The SDK uses those endpoints by default and provides explicit standard-host options. + +### How should I confirm an order? + +Use the order ID or client order ID with the matching REST API query, or process the relevant private order stream. A command acknowledgement and final order state are separate pieces of information. + +### Which methods should I use for private USDT-M Futures state? + +Use the V5 methods named `getMultiAsset...`, `submitMultiAsset...`, and related V5 position and risk methods. HTX's [USDT-M API upgrade guidance](https://www.htx.com/support/95034086350701) lists the current interface requirements. + +### What should happen after a private WebSocket reconnect? + +Reload the affected account's balances, positions, open orders, and recent fills through the REST API. Replace local state only after the required reads succeed, then continue applying stream updates. + +### What causes timestamp or signature errors? + +Common causes include clock drift, the wrong API host, a key without the required access, an IP restriction, or modified request parameters after signing. The SDK handles signing, while the application must provide current credentials and an accurate clock. + +### Can I use a proxy for both the REST API and WebSockets? + +Yes. Pass the proxy agent as `httpsAgent` for REST API clients and as `wsOptions.agent` for `WebsocketClient` and `WebsocketAPIClient`. + +### Does a proxy change product or account access? + +A proxy changes the network route and egress IP. HTX still applies the account's permissions, product availability, and current eligibility rules. + +## Next steps + +- Browse the [HTX SDK examples](https://siebly.io/examples/HTX). +- Use the [endpoint map](./endpointFunctionList.md) to find the method for a documented endpoint. +- Review the [Siebly glossary](https://siebly.io/reference/glossary) when implementing order state, acknowledgements, recovery, or reconciliation. +- Read [Exchange State](https://siebly.io/reference/exchange-state) and [Runtime Workflows](https://siebly.io/reference/runtime-workflows) before building a long-running private integration. +- Follow SDK updates and open issues in the [`sieblyio/htx-api` repository](https://github.com/sieblyio/htx-api). + ================ File: examples/Derivatives/Private/account.ts ================ @@ -1129,124 +2289,339 @@ async function start() */ ================ -File: src/lib/websocket/type-guards.ts +File: scripts/clean.mjs ================ -import { - HTXDerivativesWSAPIRequest, - HTXSpotWSAPIRequest, - HTXWSAPIRequest, - WSAPIDerivativesOperation, - WSAPISpotOperation, -} from '../../types/websockets/ws-api.js'; -⋮---- -export function isSpotWSAPIRequest( - request: HTXWSAPIRequest, -): request is HTXSpotWSAPIRequest -⋮---- -export function isDerivativesWSAPIRequest( - request: HTXWSAPIRequest, -): request is HTXDerivativesWSAPIRequest + ================ -File: src/lib/webCryptoAPI.ts +File: scripts/postbuild.mjs ================ -import { neverGuard } from './misc-util.js'; +async function copySourceDeclarations(directory) + +================ +File: src/lib/websocket/logger.ts +================ +export type LogParams = unknown[]; ⋮---- -function bufferToB64(buffer: ArrayBuffer): string +// eslint-disable-next-line @typescript-eslint/no-unused-vars ⋮---- -// function b64ToBytes(b64: string) { -// return Uint8Array.from(globalThis.atob(b64), (c) => c.charCodeAt(0)); -// } +// console.log(_params); ⋮---- -function latin1ToBytes(s: string) +export type DefaultLogger = typeof DefaultLogger; + +================ +File: src/lib/websocket/rest-client-cache.ts +================ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { AxiosRequestConfig } from 'axios'; ⋮---- -export type SignEncodeMethod = 'hex' | 'base64' | 'binary'; -export type SignAlgorithm = 'SHA-256' | 'SHA-512'; +import { FuturesClient } from '../../FuturesClient.js'; +import { SpotClient } from '../../SpotClient.js'; +import { RestClientOptions } from '../requestUtils.js'; +import { DefaultLogger } from './logger.js'; ⋮---- -interface UTF8Encoder { - encode(input?: string): Uint8Array; +interface RestClientStore { + spot: SpotClient; + futures: FuturesClient; } ⋮---- -encode(input?: string): Uint8Array; -⋮---- -export type SignKeyType = 'HMAC' | 'RSASSA-PKCS1-v1_5' | 'Ed25519'; -⋮---- -export function getSignKeyType(secret: string): SignKeyType +interface WebSocketTokenCache { + spot?: { token: string; expiresAtMs: number }; + derivatives?: { token: string; expiresAtMs: number }; +} ⋮---- -// Sometimes, not always, RSA keys include "RSA" in the header. That's a definite RSA key. +/** + * Caches REST clients and WebSocket tokens to avoid redundant requests. + */ +export class RestClientCache ⋮---- -// RSA keys are significantly longer than Ed25519 keys. 150 accounts for length of header & footer +public setLogger(logger: DefaultLogger, loggerCategory: object): void ⋮---- -async function importKey( - pem: string, - type: SignKeyType, - algorithm: SignAlgorithm, - encoder: UTF8Encoder, -): ReturnType +public getSpotRESTClient( + restOptions: RestClientOptions, + requestOptions?: AxiosRequestConfig, +): SpotClient ⋮---- -// const prefixRSA = /-----BEGIN RSA PRIVATE KEY-----/; -// const prefixEd25519 = /-----BEGIN PRIVATE KEY-----/; +public async fetchSpotWebSocketToken( + _restOptions: RestClientOptions, + _requestOptions?: AxiosRequestConfig, +): Promise< ⋮---- -// const suffixRSA = /-----END RSA PRIVATE KEY-----/; -// const suffixEd25519 = /-----END PRIVATE KEY-----/; +// still valid for at least 10s ⋮---- -// const base64Key = pem -// .replace(prefixEd25519, '') -// .replace(prefixRSA, '') -// .replace(suffixEd25519, '') -// .replace(suffixRSA, '') -// .replace(/\s+/g, ''); // Remove spaces and newlines +// const client = this.getSpotRESTClient(restOptions, requestOptions); +const tokenResult = ''; //await client.getWebSocketsToken(); ⋮---- -/** - * Similar to node crypto's `createHash()` function - */ -export async function hashMessage( - message: string, - method: SignEncodeMethod, - algorithm: SignAlgorithm, -): Promise +const token = ''; //tokenResult?.result?.token; ⋮---- -// Equivalent in node:crypto: -// const signMessage = createHash(algorithm) -// .update(signInput) -// .digest('binary'); +public getDerivativesRESTClient( + restOptions: RestClientOptions, + requestOptions?: AxiosRequestConfig, +): FuturesClient + +================ +File: src/lib/event-emitter.browser.ts +================ +// The browser entry intentionally has no bundled declarations. Its API is the +// same long-standing EventEmitter implementation used by browser bundlers. +// @ts-expect-error -- typed through the SDK-owned constructor below +import BrowserEventEmitter from 'events/events.js'; ⋮---- -export interface SignMessageOptions { - isSecretB64Encoded?: boolean; - isInputBinaryString?: boolean; -} +import { + EventEmitterConstructor, + EventListener, + EventName, +} from '../types/websockets/ws-portable.js'; ⋮---- -/** - * Sign a message, with a secret, using the Web Crypto API - */ -export async function signMessage( - message: string, - secret: string, - method: SignEncodeMethod, - algorithm: SignAlgorithm, - options?: SignMessageOptions, -): Promise +/** Browser adapter backed by the established `events` package. */ +export class EventEmitter extends RuntimeEventEmitter ⋮---- -export function checkWebCryptoAPISupported() +listenerCount(eventName: EventName, listener?: EventListener): number ================ -File: src/types/websockets/ws-events.ts +File: src/lib/event-emitter.ts ================ -import { MessageEventLike } from './ws-portable.js'; +import { EventEmitter as NodeEventEmitter } from 'node:events'; ⋮---- -export interface WsDataEvent { - data: TData; - table: string; - wsKey: TWSKey; -} +import { EventEmitterConstructor } from '../types/websockets/ws-portable.js'; ⋮---- -export function isMessageEvent(msg: unknown): msg is MessageEventLike +/** Node.js adapter. The cast keeps Node declarations out of the public API. */ +export class EventEmitter extends RuntimeEventEmitter ================ -File: src/WebsocketAPIClient.ts +File: src/lib/https-agent.browser.ts ================ -import { DefaultLogger } from './lib/websocket/logger.js'; -import { WS_KEY_MAP } from './lib/websocket/websocket-util.js'; +import type { HttpsRequestOptionsLike } from './https-agent.js'; +⋮---- +/** Browser requests must not force Node-only or preflight-triggering headers. */ +⋮---- +/** Browsers manage connection pooling; HTTPS agent settings do not apply. */ +export function configureHttpsKeepAlive( + _requestOptions: HttpsRequestOptionsLike, + _keepAliveMsecs?: number, +): void + +================ +File: src/lib/https-agent.ts +================ +import https from 'node:https'; +⋮---- +export interface HttpsRequestOptionsLike { + httpsAgent?: unknown; +} +⋮---- +/** Configure Node's HTTPS keep-alive agent while preserving caller options. */ +export function configureHttpsKeepAlive( + requestOptions: HttpsRequestOptionsLike, + keepAliveMsecs?: number, +): void + +================ +File: src/types/request/wsapi.types.ts +================ +export type HTXSpotOrderType = + | 'buy-market' + | 'sell-market' + | 'buy-limit' + | 'sell-limit' + | 'buy-ioc' + | 'sell-ioc' + | 'buy-limit-maker' + | 'sell-limit-maker' + | 'buy-stop-limit' + | 'sell-stop-limit' + | 'buy-limit-fok' + | 'sell-limit-fok' + | string; +⋮---- +export interface WSAPISpotOrderParams { + 'account-id': number | string; + symbol: string; + type: HTXSpotOrderType; + amount?: string | number; + price?: string | number; + 'market-amount'?: string | number; + source?: string; + 'client-order-id'?: string; + 'self-match-prevent'?: number; + self_match_prevent?: number; + self_match_prevent_new?: + | 'cancel_taker' + | 'cancel_maker' + | 'cancel_both' + | string; + 'stop-price'?: string | number; + operator?: 'gte' | 'lte' | string; + 'coupon-id'?: string; + [key: string]: unknown; +} +⋮---- +export type WSAPISpotBatchOrderParams = WSAPISpotOrderParams[]; +⋮---- +export interface WSAPISpotMarginOrderParams extends WSAPISpotOrderParams { + 'repay-amount'?: string | number; + 'trade-purpose'?: number; +} +⋮---- +export interface WSAPISpotCancelOrdersParams { + 'order-ids'?: string[]; + 'client-order-ids'?: string[]; +} +⋮---- +export interface WSAPISpotCancelAllOrdersParams { + 'account-id': number | string; + symbol?: string; + types?: string; + side?: 'buy' | 'sell' | string; + size?: number; +} +⋮---- +export type HTXDerivativesDirection = 'buy' | 'sell' | string; +export type HTXDerivativesOffset = 'open' | 'close' | 'both' | string; +⋮---- +export interface WSAPIDerivativesOrderParams { + contract_code?: string; + direction?: HTXDerivativesDirection; + offset?: HTXDerivativesOffset; + price?: string | number; + lever_rate?: number; + volume?: string | number; + order_price_type?: string; + client_order_id?: string | number; + reduce_only?: number; + tp_trigger_price?: string | number; + tp_order_price?: string | number; + tp_order_price_type?: string; + sl_trigger_price?: string | number; + sl_order_price?: string | number; + sl_order_price_type?: string; + self_match_prevent?: string | number; + self_match_prevent_new?: string; + [key: string]: unknown; +} +⋮---- +export type WSAPIDerivativesBatchOrderParams = WSAPIDerivativesOrderParams[]; +⋮---- +export interface WSAPIDerivativesCancelOrderParams { + order_id?: string | number; + order_id_str?: string; + client_order_id?: string | number; + contract_code?: string; + [key: string]: unknown; +} +⋮---- +export interface WSAPIDerivativesCancelAllOrdersParams { + contract_code?: string; + direction?: HTXDerivativesDirection; + offset?: HTXDerivativesOffset; + [key: string]: unknown; +} +⋮---- +export interface WSAPIDerivativesPlaceOrderParams { + contract_code: string; + margin_mode: 'cross' | 'isolated' | string; + position_side?: 'long' | 'short' | 'both' | string; + side: HTXDerivativesDirection; + type: 'market' | 'limit' | 'post_only' | string; + price_match?: string | null; + time_in_force?: 'gtc' | 'ioc' | 'fok' | string; + client_order_id?: string; + price?: string; + volume: string; + reduce_only?: number; + tp_trigger_price?: string; + tp_order_price?: string; + tp_type?: string; + tp_trigger_price_type?: 'last' | 'mark' | string; + sl_trigger_price?: string; + sl_order_price?: string; + sl_type?: string; + sl_trigger_price_type?: 'last' | 'mark' | string; + price_protect?: boolean | string; + self_match_prevent?: string; + [key: string]: unknown; +} +⋮---- +export type WSAPIDerivativesBatchPlaceOrderParams = + WSAPIDerivativesPlaceOrderParams[]; +⋮---- +export interface WSAPIDerivativesV5CancelOrderParams { + contract_code: string; + order_id?: string; + client_order_id?: string; + [key: string]: unknown; +} +⋮---- +export interface WSAPIDerivativesV5CancelAllOrdersParams { + contract_code?: string; + margin_mode?: 'cross' | 'isolated' | string; + [key: string]: unknown; +} + +================ +File: src/types/response/wsapi.types.ts +================ +export interface WSAPIRateLimit { + limit?: string; + interval?: string; + remaining?: string; + reset?: string; +} +⋮---- +export interface WSAPIBaseResponse { + wsKey?: string; + cid?: string; + ch?: TOperation; + op?: TOperation; + status?: 'ok' | 'error' | string; + code?: number; + message?: string | null; + success?: boolean; + data?: TData; + ts?: number; + 'err-code'?: number; + 'err-msg'?: string; + rate_limit?: WSAPIRateLimit; + request?: unknown; +} +⋮---- +export interface WSAPISpotOrderResult { + 'order-id'?: number | string; + 'client-order-id'?: string; + orderId?: number | string; +} +⋮---- +export interface WSAPISpotCancelOrdersResult { + success?: string[]; + failed?: { + 'order-id'?: string; + 'client-order-id'?: string; + 'err-code'?: string; + 'err-msg'?: string; + }[]; +} +⋮---- +export interface WSAPISpotCancelAllOrdersResult { + 'success-count': number; + 'failed-count': number; + 'next-id'?: number | string; +} +⋮---- +export interface WSAPIDerivativesOrderResult { + order_id?: number | string; + order_id_str?: string; + client_order_id?: number | string; +} +⋮---- +export interface WSAPIDerivativesV5OrderResult { + order_id?: string; + client_order_id?: string; +} + +================ +File: src/types/websockets/ws-api.ts +================ +import { WS_KEY_MAP } from '../../lib/websocket/websocket-util.js'; import { WSAPIDerivativesBatchOrderParams, WSAPIDerivativesBatchPlaceOrderParams, @@ -1261,7 +2636,7 @@ import { WSAPISpotCancelOrdersParams, WSAPISpotMarginOrderParams, WSAPISpotOrderParams, -} from './types/request/wsapi.types.js'; +} from '../request/wsapi.types.js'; import { WSAPIBaseResponse, WSAPIDerivativesOrderResult, @@ -1269,429 +2644,408 @@ import { WSAPISpotCancelAllOrdersResult, WSAPISpotCancelOrdersResult, WSAPISpotOrderResult, -} from './types/response/wsapi.types.js'; -import { WSClientConfigurableOptions } from './types/websockets/ws-general.js'; -import { WebsocketClient } from './WebsocketClient.js'; +} from '../response/wsapi.types.js'; ⋮---- -export interface WSAPIClientConfigurableOptions { - /** - * Default: true - * - * Attach default event listeners, which will console log high-level connection events. - */ - attachEventListeners: boolean; +export type Exact = T extends object ? T & Record : T; +⋮---- +export interface WsRequestOperation { + op?: string; + action?: string; + topic?: TWSTopic; + ch?: TWSTopic; + id?: string | number; + cid?: string; } ⋮---- -/** - * Default: true - * - * Attach default event listeners, which will console log high-level connection events. - */ +export interface WSAPIAuthenticationConfirmedFromServer { + action?: 'req'; + op?: 'auth'; + code?: number; + ch?: 'auth'; + 'err-code'?: number; + data?: Record; +} ⋮---- -export class WebsocketAPIClient +export type WSAPIOperation = (typeof WS_API_Operations)[number]; ⋮---- -constructor( - options?: WSClientConfigurableOptions & - Partial, - logger?: DefaultLogger, -) +export type WSAPISpotOperation = + | 'create-order' + | 'create-batchorder' + | 'create-margin-order' + | 'cancelall' + | 'cancel'; ⋮---- -getWSClient(): WebsocketClient +export type WSAPILegacyDerivativesOperation = + | 'create_order' + | 'create_cross_order' + | 'create_batchorder' + | 'create_cross_batchorder' + | 'cancel' + | 'cross_cancel' + | 'cancelall' + | 'cross_cancelall'; ⋮---- -setTimeOffsetMs(newOffset: number): void +export type WSAPIDerivativesV5Operation = + | 'place_order' + | 'place_batch_orders' + | 'cancel_order' + | 'cancel_batch_orders' + | 'cancel_all_orders'; ⋮---- -generateNewOrderID(): string +export type WSAPIDerivativesOperation = + | WSAPILegacyDerivativesOperation + | WSAPIDerivativesV5Operation; ⋮---- -getOrderIdPrefix(): string +export interface WSAPIWsKeyTopicMap { + [WS_KEY_MAP.spotTrade]: WSAPISpotOperation; + [WS_KEY_MAP.linearSwapTrade]: WSAPIDerivativesOperation; + [WS_KEY_MAP.coinDeliveryTrade]: WSAPILegacyDerivativesOperation; + [WS_KEY_MAP.coinSwapTrade]: WSAPILegacyDerivativesOperation; +} ⋮---- -submitSpotOrder( - params: WSAPISpotOrderParams, -): Promise> +export type WSAPIWsKey = keyof WSAPIWsKeyTopicMap; ⋮---- -submitSpotBatchOrders( - params: WSAPISpotBatchOrderParams, -): Promise> +export interface HTXSpotWSAPIRequest< + TOperation extends WSAPISpotOperation = WSAPISpotOperation, + TParams = unknown, +> { + cid: string; + ch: TOperation; + params?: TParams; +} ⋮---- -submitSpotMarginOrder( - params: WSAPISpotMarginOrderParams, -): Promise> +export interface HTXDerivativesWSAPIRequest< + TOperation extends WSAPIDerivativesOperation = WSAPIDerivativesOperation, + TParams = unknown, +> { + op: TOperation; + cid: string; + data?: TParams; +} ⋮---- -cancelSpotOrders( - params: WSAPISpotCancelOrdersParams, -): Promise> +export type HTXWSAPIRequest = HTXSpotWSAPIRequest | HTXDerivativesWSAPIRequest; ⋮---- -cancelAllSpotOrders( - params: WSAPISpotCancelAllOrdersParams, -): Promise> +export type WSAPIResponse< + TData = unknown, + TOperation extends string = string, +> = WSAPIBaseResponse; ⋮---- -submitLinearSwapOrder( - params: WSAPIDerivativesOrderParams, -): Promise> +export interface WSAPITopicRequestParamMap { + 'create-order': WSAPISpotOrderParams; + 'create-batchorder': WSAPISpotBatchOrderParams; + 'create-margin-order': WSAPISpotMarginOrderParams; + cancelall: + | WSAPISpotCancelAllOrdersParams + | WSAPIDerivativesCancelAllOrdersParams; + cancel: WSAPISpotCancelOrdersParams | WSAPIDerivativesCancelOrderParams; + + create_order: WSAPIDerivativesOrderParams; + create_cross_order: WSAPIDerivativesOrderParams; + create_batchorder: WSAPIDerivativesBatchOrderParams; + create_cross_batchorder: WSAPIDerivativesBatchOrderParams; + cross_cancel: WSAPIDerivativesCancelOrderParams; + cross_cancelall: WSAPIDerivativesCancelAllOrdersParams; + + place_order: WSAPIDerivativesPlaceOrderParams; + place_batch_orders: WSAPIDerivativesBatchPlaceOrderParams; + cancel_order: WSAPIDerivativesV5CancelOrderParams; + cancel_batch_orders: WSAPIDerivativesV5CancelOrderParams[]; + cancel_all_orders: WSAPIDerivativesV5CancelAllOrdersParams; +} ⋮---- -submitLinearSwapCrossOrder( - params: WSAPIDerivativesOrderParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'create_cross_order', - params, - ); +export interface WSAPITopicResponseMap { + 'create-order': WSAPIResponse; + 'create-batchorder': WSAPIResponse< + WSAPISpotOrderResult[], + 'create-batchorder' + >; + 'create-margin-order': WSAPIResponse< + WSAPISpotOrderResult, + 'create-margin-order' + >; + cancelall: WSAPIResponse< + WSAPISpotCancelAllOrdersResult | WSAPIDerivativesOrderResult, + 'cancelall' + >; + cancel: WSAPIResponse< + WSAPISpotCancelOrdersResult | WSAPIDerivativesOrderResult, + 'cancel' + >; + + create_order: WSAPIResponse; + create_cross_order: WSAPIResponse< + WSAPIDerivativesOrderResult, + 'create_cross_order' + >; + create_batchorder: WSAPIResponse< + WSAPIDerivativesOrderResult[], + 'create_batchorder' + >; + create_cross_batchorder: WSAPIResponse< + WSAPIDerivativesOrderResult[], + 'create_cross_batchorder' + >; + cross_cancel: WSAPIResponse; + cross_cancelall: WSAPIResponse< + WSAPIDerivativesOrderResult, + 'cross_cancelall' + >; + + place_order: WSAPIResponse; + place_batch_orders: WSAPIResponse< + WSAPIDerivativesV5OrderResult[], + 'place_batch_orders' + >; + cancel_order: WSAPIResponse; + cancel_batch_orders: WSAPIResponse< + WSAPIDerivativesV5OrderResult[], + 'cancel_batch_orders' + >; + cancel_all_orders: WSAPIResponse< + WSAPIDerivativesV5OrderResult, + 'cancel_all_orders' + >; +} + +================ +File: src/types/websockets/ws-portable.ts +================ +/* eslint-disable @typescript-eslint/no-explicit-any */ ⋮---- -submitLinearSwapBatchOrders( - params: WSAPIDerivativesBatchOrderParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'create_batchorder', - params, - ); +/** A binary WebSocket payload supported in both Node.js and browsers. */ +export type WebSocketBinaryData = ArrayBuffer | ArrayBufferView; ⋮---- -submitLinearSwapCrossBatchOrders( - params: WSAPIDerivativesBatchOrderParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'create_cross_batchorder', - params, - ); +/** Standard WebSocket ready-state values shared by Node.js and browsers. */ ⋮---- -cancelLinearSwapOrder( - params: WSAPIDerivativesCancelOrderParams, -): Promise> +/** + * The portable portion of a WebSocket connection exposed by this SDK. + * + * Node.js `ws` instances and browser-native `WebSocket` instances both satisfy + * this interface. Runtime-specific extensions remain available through + * `wsOptions`, without making consumers install Node.js or `ws` declarations. + */ +export interface WebSocketLike { + readonly readyState: number; + + binaryType?: string; + onopen: ((event: any) => any) | null; + onmessage: ((event: any) => any) | null; + onerror: ((event: any) => any) | null; + onclose: ((event: any) => any) | null; + + /** Runtime implementations accept their own wider binary-data variants. */ + send(data: any): void; + close(code?: number, reason?: string): void; + + /** Node.js `ws` extension. Undefined for browser-native sockets. */ + ping?: () => void; + /** Node.js `ws` extension. Undefined for browser-native sockets. */ + terminate?: () => void; + /** Node.js-style event API. Undefined for browser-native sockets. */ + on?: (event: string, listener: (event: unknown) => void) => unknown; + + /** Internal connection identifier attached by the SDK. */ + wsKey?: string; +} ⋮---- -cancelLinearSwapCrossOrder( - params: WSAPIDerivativesCancelOrderParams, -): Promise> +/** Runtime implementations accept their own wider binary-data variants. */ +send(data: any): void; +close(code?: number, reason?: string): void; ⋮---- -cancelAllLinearSwapOrders( - params: WSAPIDerivativesCancelAllOrdersParams, -): Promise> +/** Node.js `ws` extension. Undefined for browser-native sockets. */ ⋮---- -cancelAllLinearSwapCrossOrders( - params: WSAPIDerivativesCancelAllOrdersParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'cross_cancelall', - params, - ); -⋮---- -placeLinearSwapOrder( - params: WSAPIDerivativesPlaceOrderParams, -): Promise> -⋮---- -placeLinearSwapBatchOrders( - params: WSAPIDerivativesBatchPlaceOrderParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'place_batch_orders', - params, - ); -⋮---- -cancelLinearSwapV5Order( - params: WSAPIDerivativesV5CancelOrderParams, -): Promise> -⋮---- -cancelLinearSwapV5BatchOrders( - params: WSAPIDerivativesV5CancelOrderParams[], - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'cancel_batch_orders', - params, - ); -⋮---- -cancelAllLinearSwapV5Orders( - params: WSAPIDerivativesV5CancelAllOrdersParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.linearSwapTrade, - 'cancel_all_orders', - params, - ); +/** Node.js `ws` extension. Undefined for browser-native sockets. */ ⋮---- -submitCoinDeliveryOrder( - params: WSAPIDerivativesOrderParams, -): Promise> +/** Node.js-style event API. Undefined for browser-native sockets. */ ⋮---- -submitCoinDeliveryBatchOrders( - params: WSAPIDerivativesBatchOrderParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.coinDeliveryTrade, - 'create_batchorder', - params, - ); +/** Internal connection identifier attached by the SDK. */ ⋮---- -cancelCoinDeliveryOrder( - params: WSAPIDerivativesCancelOrderParams, -): Promise> +/** Portable shape for a WebSocket message event. */ +export interface MessageEventLike { + target: unknown; + type: string; + data: TData; +} ⋮---- -cancelAllCoinDeliveryOrders( - params: WSAPIDerivativesCancelAllOrdersParams, -): Promise> +/** + * Connection options forwarded to the underlying WebSocket implementation. + * Known cross-runtime options are documented while the index signature keeps + * runtime-specific `ws`/agent options available without exposing their types. + */ +export interface WSConnectionOptions { + protocols?: string | string[] | undefined; + agent?: unknown; + headers?: Record | undefined; + followRedirects?: boolean | undefined; + handshakeTimeout?: number | undefined; + maxPayload?: number | undefined; + origin?: string | undefined; + perMessageDeflate?: boolean | object | undefined; + rejectUnauthorized?: boolean | undefined; + [key: string]: any; +} ⋮---- -submitCoinSwapOrder( - params: WSAPIDerivativesOrderParams, -): Promise> +export type EventName = string | symbol; +export type EventListener = (...args: any[]) => void; ⋮---- -submitCoinSwapBatchOrders( - params: WSAPIDerivativesBatchOrderParams, - ): Promise< - WSAPIBaseResponse - > { - return this.wsClient.sendWSAPIRequest( - WS_KEY_MAP.coinSwapTrade, - 'create_batchorder', - params, - ); +/** SDK-owned instance surface compatible with Node's EventEmitter. */ +export interface EventEmitterLike { + addListener(eventName: EventName, listener: EventListener): this; + on(eventName: EventName, listener: EventListener): this; + once(eventName: EventName, listener: EventListener): this; + prependListener(eventName: EventName, listener: EventListener): this; + prependOnceListener(eventName: EventName, listener: EventListener): this; + off(eventName: EventName, listener: EventListener): this; + removeListener(eventName: EventName, listener: EventListener): this; + removeAllListeners(eventName?: EventName): this; + setMaxListeners(count: number): this; + getMaxListeners(): number; + listeners(eventName: EventName): EventListener[]; + rawListeners(eventName: EventName): EventListener[]; + emit(eventName: EventName, ...args: any[]): boolean; + listenerCount(eventName: EventName, listener?: EventListener): number; + eventNames(): EventName[]; +} ⋮---- -cancelCoinSwapOrder( - params: WSAPIDerivativesCancelOrderParams, -): Promise> +addListener(eventName: EventName, listener: EventListener): this; +on(eventName: EventName, listener: EventListener): this; +once(eventName: EventName, listener: EventListener): this; +prependListener(eventName: EventName, listener: EventListener): this; +prependOnceListener(eventName: EventName, listener: EventListener): this; +off(eventName: EventName, listener: EventListener): this; +removeListener(eventName: EventName, listener: EventListener): this; +removeAllListeners(eventName?: EventName): this; +setMaxListeners(count: number): this; +getMaxListeners(): number; +listeners(eventName: EventName): EventListener[]; +rawListeners(eventName: EventName): EventListener[]; +emit(eventName: EventName, ...args: any[]): boolean; +listenerCount(eventName: EventName, listener?: EventListener): number; +eventNames(): EventName[]; ⋮---- -cancelAllCoinSwapOrders( - params: WSAPIDerivativesCancelAllOrdersParams, -): Promise> +/** Constructor abstraction used to hide runtime-specific emitter declarations. */ +export interface EventEmitterConstructor { + new (options?: { captureRejections?: boolean }): EventEmitterLike; + readonly prototype: EventEmitterLike; + defaultMaxListeners: number; + listenerCount(emitter: EventEmitterLike, eventName: EventName): number; +} ⋮---- -private setupDefaultEventListeners() +listenerCount(emitter: EventEmitterLike, eventName: EventName): number; ================ -File: jest.config.cjs +File: src/types/websockets/ws-subscriptions.ts ================ /** - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration + * HTX websocket topics are string templates, e.g.: + * + * - `market.btcusdt.kline.1min` + * - `market.btcusdt.mbp.150` + * - `orders#btcusdt` + * - `orders.btc-usdt` + * + * The exchange publishes a large and evolving topic surface, so the SDK keeps + * the topic type open while exporting common topic groups for discovery and + * private/public routing. */ ⋮---- -/** @type {import('jest').Config} */ -⋮---- -// All imported modules in your tests should be mocked automatically -// automock: false, -⋮---- -// Stop running tests after `n` failures -// bail: 0, -bail: false, // enable to stop test when an error occur, -⋮---- -// The directory where Jest should store its cached dependency information -// cacheDirectory: "/private/var/folders/kf/2k3sz4px6c9cbyzj1h_b192h0000gn/T/jest_dx", -⋮---- -// Automatically clear mock calls, instances, contexts and results before every test -⋮---- -// Indicates whether the coverage information should be collected while executing the test -⋮---- -// An array of glob patterns indicating a set of files for which coverage information should be collected -⋮---- -// The directory where Jest should output its coverage files -⋮---- -// An array of regexp pattern strings used to skip coverage collection -// coveragePathIgnorePatterns: [ -// "/node_modules/" -// ], -⋮---- -// Indicates which provider should be used to instrument code for coverage -⋮---- -// A list of reporter names that Jest uses when writing coverage reports -// coverageReporters: [ -// "json", -// "text", -// "lcov", -// "clover" -// ], -⋮---- -// setupFiles: ['./test/setupTests.ts'], -⋮---- -// An object that configures minimum threshold enforcement for coverage results -// coverageThreshold: undefined, -⋮---- -// A path to a custom dependency extractor -// dependencyExtractor: undefined, -⋮---- -// Make calling deprecated APIs throw helpful error messages -// errorOnDeprecated: false, -⋮---- -// The default configuration for fake timers -// fakeTimers: { -// "enableGlobally": false -// }, -⋮---- -// Force coverage collection from ignored files using an array of glob patterns -// forceCoverageMatch: [], -⋮---- -// A path to a module which exports an async function that is triggered once before all test suites -// globalSetup: undefined, -⋮---- -// A path to a module which exports an async function that is triggered once after all test suites -// globalTeardown: undefined, -⋮---- -// A set of global variables that need to be available in all test environments -// globals: {}, -⋮---- -// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. -maxWorkers: '1', // run tests sequentially to avoid nonce issues -⋮---- -// An array of directory names to be searched recursively up from the requiring module's location -// moduleDirectories: [ -// "node_modules" -// ], -⋮---- -// An array of file extensions your modules use -⋮---- -// modulePaths: ['src'], -⋮---- -// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module -// moduleNameMapper: {}, +export type WSSpotPublicTopic = (typeof WS_SPOT_PUBLIC_TOPICS)[number]; +export type WSSpotPrivateTopic = (typeof WS_SPOT_PRIVATE_TOPICS)[number]; +export type WSDerivativesPublicTopic = + (typeof WS_DERIVATIVES_PUBLIC_TOPICS)[number]; +export type WSDerivativesPrivateTopic = + (typeof WS_DERIVATIVES_PRIVATE_TOPICS)[number]; ⋮---- -// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader -// modulePathIgnorePatterns: [], +export type WSTopic = + | WSSpotPublicTopic + | WSSpotPrivateTopic + | WSDerivativesPublicTopic + | WSDerivativesPrivateTopic + | string; ⋮---- -// Activates notifications for test results -// notify: false, -⋮---- -// An enum that specifies notification mode. Requires { notify: true } -// notifyMode: "failure-change", -⋮---- -// A preset that is used as a base for Jest's configuration -// preset: undefined, -⋮---- -// Run tests from one or more projects -// projects: undefined, -⋮---- -// Use this configuration option to add custom reporters to Jest -// reporters: undefined, -⋮---- -// Automatically reset mock state before every test -// resetMocks: false, -⋮---- -// Reset the module registry before running each individual test -// resetModules: false, -⋮---- -// A path to a custom resolver -// resolver: undefined, -⋮---- -// Automatically restore mock state and implementation before every test -// restoreMocks: false, -⋮---- -// The root directory that Jest should scan for tests and modules within -// rootDir: undefined, -⋮---- -// A list of paths to directories that Jest should use to search for files in -// roots: [ -// "" -// ], -⋮---- -// Allows you to use a custom runner instead of Jest's default test runner -// runner: "jest-runner", -⋮---- -// The paths to modules that run some code to configure or set up the testing environment before each test -// setupFiles: [], -⋮---- -// A list of paths to modules that run some code to configure or set up the testing framework before each test -// setupFilesAfterEnv: [], -⋮---- -// The number of seconds after which a test is considered as slow and reported as such in the results. -// slowTestThreshold: 5, -⋮---- -// A list of paths to snapshot serializer modules Jest should use for snapshot testing -// snapshotSerializers: [], -⋮---- -// The test environment that will be used for testing -// testEnvironment: "jest-environment-node", -⋮---- -// Options that will be passed to the testEnvironment -// testEnvironmentOptions: {}, -⋮---- -// Adds a location field to test results -// testLocationInResults: false, -⋮---- -// The glob patterns Jest uses to detect test files -⋮---- -// "**/__tests__/**/*.[jt]s?(x)", -⋮---- -// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped -// testPathIgnorePatterns: [ -// "/node_modules/" -// ], -⋮---- -// The regexp pattern or array of patterns that Jest uses to detect test files -// testRegex: [], -⋮---- -// This option allows the use of a custom results processor -// testResultsProcessor: undefined, -⋮---- -// This option allows use of a custom test runner -// testRunner: "jest-circus/runner", -⋮---- -// A map from regular expressions to paths to transformers -// transform: undefined, -⋮---- -// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation -// transformIgnorePatterns: [ -// "/node_modules/", -// "\\.pnp\\.[^\\/]+$" -// ], -⋮---- -// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them -// unmockedModulePathPatterns: undefined, -⋮---- -// Indicates whether each individual test should be reported during the run -// verbose: undefined, -verbose: true, // report individual test -⋮---- -// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode -// watchPathIgnorePatterns: [], -⋮---- -// Whether to use watchman for file crawling -// watchman: true, +export function isPrivateTopic(topic: string): boolean ================ -File: tsconfig.json +File: .npmrc +================ +min-release-age=30 + +================ +File: .prettierrc +================ +{ + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all" +} + +================ +File: tsconfig.cjs.json ================ { + "extends": "./tsconfig.json", "compilerOptions": { - "allowSyntheticDefaultImports": true, - "noEmitOnError": true, - "declaration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": false, - "inlineSourceMap": false, - "lib": ["esnext", "dom"], - "listEmittedFiles": false, - "listFiles": false, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noUnusedParameters": true, - "pretty": true, - "removeComments": false, - "resolveJsonModule": true, - "skipLibCheck": false, - "sourceMap": true, - "strict": true, - "strictNullChecks": true, - "types": ["node"], + "module": "commonjs", + "moduleResolution": "bundler", + "outDir": "dist/cjs", + "rootDir": "src", "target": "esnext" }, - "exclude": ["node_modules", "dist"], "include": ["src/**/*.*"] } +================ +File: tsconfig.esm.json +================ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "dist/mjs", + "rootDir": "src", + "target": "esnext" + }, + "include": ["src/**/*.*"] +} + +================ +File: tsconfig.extensions.json +================ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true + }, + "include": ["src/**/*.*"] +} + +================ +File: tsconfig.linting.json +================ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "target": "esnext", + "rootDir": ".", + "allowJs": true, + "noEmit": true, + "types": ["node", "jest"] + }, + "include": [ + "src/**/*.*", + "test/**/*.*", + "examples/**/*.*", + "eslint.config.cjs", + "jest.config.cjs" + ] +} + ================ File: examples/Derivatives/Private/orderManagement.ts ================ @@ -1731,26 +3085,6 @@ async function getMultiAssetOpenOrders() ⋮---- // getMultiAssetOpenOrders(); -================ -File: examples/Derivatives/WebSockets/wsAPI.RAW.ts -================ -// Raw low-level WS API example using WebsocketClient.sendWSAPIRequest(). -// Prefer WebsocketAPIClient for typed wrapper methods. -⋮---- -import { - DefaultLogger, - LogParams, - WebsocketClient, - WS_KEY_MAP, -} from '../../../src/index.js'; -⋮---- -// Install from npm in your own project: -// import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; -⋮---- -// console.log('trace', ...params); -⋮---- -async function start() - ================ File: examples/Spot/Private/account.ts ================ @@ -1931,6 +3265,25 @@ async function start() ⋮---- // Subscribe to trade clearing for all symbols, mode 1 (trade & cancellation events) +================ +File: src/lib/websocket/type-guards.ts +================ +import { + HTXDerivativesWSAPIRequest, + HTXSpotWSAPIRequest, + HTXWSAPIRequest, + WSAPIDerivativesOperation, + WSAPISpotOperation, +} from '../../types/websockets/ws-api.js'; +⋮---- +export function isSpotWSAPIRequest( + request: HTXWSAPIRequest, +): request is HTXSpotWSAPIRequest +⋮---- +export function isDerivativesWSAPIRequest( + request: HTXWSAPIRequest, +): request is HTXDerivativesWSAPIRequest + ================ File: src/lib/websocket/WsStore.ts ================ @@ -2117,2510 +3470,2391 @@ deleteTopic(key: WsKey, topic: TWSTopicSubscribeEventArgs) // Check if we're subscribed to a topic like this ================ -File: src/lib/websocket/WsStore.types.ts +File: src/lib/webCryptoAPI.ts ================ -import { WebSocketLike } from '../../types/websockets/ws-portable.js'; +import { neverGuard } from './misc-util.js'; ⋮---- -export enum WsConnectionStateEnum { - INITIAL = 0, - CONNECTING = 1, - CONNECTED = 2, - CLOSING = 3, - RECONNECTING = 4, - // ERROR_RECONNECTING = 5, - ERROR = 5, -} +function bufferToB64(buffer: ArrayBuffer): string ⋮---- -// ERROR_RECONNECTING = 5, +// function b64ToBytes(b64: string) { +// return Uint8Array.from(globalThis.atob(b64), (c) => c.charCodeAt(0)); +// } ⋮---- -export interface DeferredPromise { - resolve?: (value: TSuccess | PromiseLike) => void; - reject?: (value?: TError) => void; - promise?: Promise; -} +function latin1ToBytes(s: string) ⋮---- -export interface WSConnectedResult { - wsKey: string; - ws: WebSocketLike; -} +export type SignEncodeMethod = 'hex' | 'base64' | 'binary'; +export type SignAlgorithm = 'SHA-256' | 'SHA-512'; ⋮---- -export interface WsStoredState { - /** The currently active websocket connection */ - ws?: WebSocketLike; - - /** The current lifecycle state of the connection (enum) */ - connectionState?: WsConnectionStateEnum; - connectionStateChangedAt?: Date; - - /** 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; - - /** If a reconnection is in progress, this will have the timer for the delayed reconnect */ - activeReconnectTimer?: ReturnType | undefined; - /** - * When a connection attempt is in progress (even for reconnect), a promise is stored here. - * - * This promise will resolve once connected (and will then get removed); - */ - deferredPromiseStore: Record; - - /** - * All the topics we are expected to be subscribed to on this connection (and we automatically resubscribe to if the connection drops) - * - * A "Set" and a deep-object-match are used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to) - */ - subscribedTopics: Set; - - /** Whether this connection has completed authentication (only applies to private connections) */ - isAuthenticated?: boolean; - - /** Tracks a multi-step authentication attempt that has not completed yet */ - authRequestState?: 'preparing' | 'waitingForEvent' | 'sending'; - - /** Auth-ready event received while the auth request was still being prepared */ - pendingAuthEvent?: object; - - /** - * Whether this connection has completed authentication before for the Websocket API, so it knows to automatically reauth if reconnected - */ - didAuthWSAPI?: boolean; - - /** To reauthenticate on the WS API, which channel do we send to? */ - WSAPIAuthChannel?: string; +interface UTF8Encoder { + encode(input?: string): Uint8Array; } ⋮---- -/** The currently active websocket connection */ +encode(input?: string): Uint8Array; ⋮---- -/** The current lifecycle state of the connection (enum) */ +export type SignKeyType = 'HMAC' | 'RSASSA-PKCS1-v1_5' | 'Ed25519'; ⋮---- -/** A timer that will send an upstream heartbeat (ping) when it expires */ +export function getSignKeyType(secret: string): SignKeyType ⋮---- -/** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ +// Sometimes, not always, RSA keys include "RSA" in the header. That's a definite RSA key. ⋮---- -/** If a reconnection is in progress, this will have the timer for the delayed reconnect */ +// RSA keys are significantly longer than Ed25519 keys. 150 accounts for length of header & footer ⋮---- -/** - * When a connection attempt is in progress (even for reconnect), a promise is stored here. - * - * This promise will resolve once connected (and will then get removed); - */ +async function importKey( + pem: string, + type: SignKeyType, + algorithm: SignAlgorithm, + encoder: UTF8Encoder, +): ReturnType ⋮---- -/** - * All the topics we are expected to be subscribed to on this connection (and we automatically resubscribe to if the connection drops) - * - * A "Set" and a deep-object-match are used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to) - */ +// const prefixRSA = /-----BEGIN RSA PRIVATE KEY-----/; +// const prefixEd25519 = /-----BEGIN PRIVATE KEY-----/; ⋮---- -/** Whether this connection has completed authentication (only applies to private connections) */ +// const suffixRSA = /-----END RSA PRIVATE KEY-----/; +// const suffixEd25519 = /-----END PRIVATE KEY-----/; ⋮---- -/** Tracks a multi-step authentication attempt that has not completed yet */ +// const base64Key = pem +// .replace(prefixEd25519, '') +// .replace(prefixRSA, '') +// .replace(suffixEd25519, '') +// .replace(suffixRSA, '') +// .replace(/\s+/g, ''); // Remove spaces and newlines ⋮---- -/** Auth-ready event received while the auth request was still being prepared */ +/** + * Similar to node crypto's `createHash()` function + */ +export async function hashMessage( + message: string, + method: SignEncodeMethod, + algorithm: SignAlgorithm, +): Promise +⋮---- +// Equivalent in node:crypto: +// const signMessage = createHash(algorithm) +// .update(signInput) +// .digest('binary'); +⋮---- +export interface SignMessageOptions { + isSecretB64Encoded?: boolean; + isInputBinaryString?: boolean; +} ⋮---- /** - * Whether this connection has completed authentication before for the Websocket API, so it knows to automatically reauth if reconnected - */ + * Sign a message, with a secret, using the Web Crypto API + */ +export async function signMessage( + message: string, + secret: string, + method: SignEncodeMethod, + algorithm: SignAlgorithm, + options?: SignMessageOptions, +): Promise ⋮---- -/** To reauthenticate on the WS API, which channel do we send to? */ +export function checkWebCryptoAPISupported() ================ -File: src/lib/misc-util.ts +File: src/types/websockets/ws-events.ts ================ -export function neverGuard(x: never, msg: string): Error -⋮---- -export function isObjectLike(value: unknown): value is Record -⋮---- -export function omitKeys( - source: Record, - keysToOmit: string[], -): Record -⋮---- -export function isRecord(value: unknown): value is Record +import { MessageEventLike } from './ws-portable.js'; ⋮---- -export function removeInternalParamFields(params: TParams): TParams +export interface WsDataEvent { + data: TData; + table: string; + wsKey: TWSKey; +} ⋮---- -export function getStringOrUndefined(value: unknown): string | undefined +export function isMessageEvent(msg: unknown): msg is MessageEventLike ================ -File: src/types/websockets/ws-general.ts +File: src/WebsocketAPIClient.ts ================ -import { AxiosRequestConfig } from 'axios'; -⋮---- -import { RestClientOptions } from '../../lib/requestUtils.js'; -import { HtxWSNetwork } from '../../lib/websocket/websocket-util.js'; -import { WSConnectionOptions } from './ws-portable.js'; +import { DefaultLogger } from './lib/websocket/logger.js'; +import { WS_KEY_MAP } from './lib/websocket/websocket-util.js'; +import { + WSAPIDerivativesBatchOrderParams, + WSAPIDerivativesBatchPlaceOrderParams, + WSAPIDerivativesCancelAllOrdersParams, + WSAPIDerivativesCancelOrderParams, + WSAPIDerivativesOrderParams, + WSAPIDerivativesPlaceOrderParams, + WSAPIDerivativesV5CancelAllOrdersParams, + WSAPIDerivativesV5CancelOrderParams, + WSAPISpotBatchOrderParams, + WSAPISpotCancelAllOrdersParams, + WSAPISpotCancelOrdersParams, + WSAPISpotMarginOrderParams, + WSAPISpotOrderParams, +} from './types/request/wsapi.types.js'; +import { + WSAPIBaseResponse, + WSAPIDerivativesOrderResult, + WSAPIDerivativesV5OrderResult, + WSAPISpotCancelAllOrdersResult, + WSAPISpotCancelOrdersResult, + WSAPISpotOrderResult, +} from './types/response/wsapi.types.js'; +import { WSClientConfigurableOptions } from './types/websockets/ws-general.js'; +import { WebsocketClient } from './WebsocketClient.js'; ⋮---- -/** General configuration for the WebsocketClient */ -export interface WSClientConfigurableOptions { - /** Your API key */ - apiKey?: string; - - /** Your API secret */ - apiSecret?: string; - - /** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds. */ - recvWindow?: number; - - /** How often to check if the connection is alive */ - pingInterval?: number; - - /** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */ - pongTimeout?: number; - - /** Delay in milliseconds before respawning the connection */ - reconnectTimeout?: number; - - restOptions?: RestClientOptions; - requestOptions?: AxiosRequestConfig; - - wsOptions?: WSConnectionOptions; - - wsUrl?: string; - - /** - * Switch between the AWS and Standard WebSocket domains. The AWS domain has better connectivity from certain regions. If you have trouble maintaining a stable connection, try switching the domain. - * - * AWS is used by default. If this field isn't set, but you're setting the baseUrlKey in the embedded restOptions, the WS domain will automatically switch to match the REST domain. - */ - wsEnvironment?: HtxWSNetwork; - +export interface WSAPIClientConfigurableOptions { /** - * Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method + * Default: true * - * Look in the examples folder for a demonstration on using node's createHmac instead. - */ - customSignMessageFn?: (message: string, secret: string) => Promise; - - /** - * If you authenticated the WS API before, automatically try to re-authenticate the WS API if you're disconnected/reconnected for any reason. - */ - reauthWSAPIOnReconnect?: boolean; - - /** - * Whether to use native WebSocket ping/pong frames for heartbeats. + * Attach default event listeners, which will console log high-level connection events. */ - useNativeHeartbeats?: boolean; + attachEventListeners: boolean; } ⋮---- -/** Your API key */ +/** + * Default: true + * + * Attach default event listeners, which will console log high-level connection events. + */ ⋮---- -/** Your API secret */ +export class WebsocketAPIClient ⋮---- -/** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds. */ +constructor( + options?: WSClientConfigurableOptions & + Partial, + logger?: DefaultLogger, +) ⋮---- -/** How often to check if the connection is alive */ +getWSClient(): WebsocketClient ⋮---- -/** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */ +setTimeOffsetMs(newOffset: number): void ⋮---- -/** Delay in milliseconds before respawning the connection */ +generateNewOrderID(): string ⋮---- -/** - * Switch between the AWS and Standard WebSocket domains. The AWS domain has better connectivity from certain regions. If you have trouble maintaining a stable connection, try switching the domain. - * - * AWS is used by default. If this field isn't set, but you're setting the baseUrlKey in the embedded restOptions, the WS domain will automatically switch to match the REST domain. - */ +getOrderIdPrefix(): string ⋮---- -/** - * Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method - * - * Look in the examples folder for a demonstration on using node's createHmac instead. - */ +submitSpotOrder( + params: WSAPISpotOrderParams, +): Promise> ⋮---- -/** - * If you authenticated the WS API before, automatically try to re-authenticate the WS API if you're disconnected/reconnected for any reason. - */ +submitSpotBatchOrders( + params: WSAPISpotBatchOrderParams, +): Promise> ⋮---- -/** - * Whether to use native WebSocket ping/pong frames for heartbeats. - */ +submitSpotMarginOrder( + params: WSAPISpotMarginOrderParams, +): Promise> ⋮---- -/** - * WS configuration that's always defined, regardless of user configuration - * (usually comes from defaults if there's no user-provided values) - */ -export interface WebsocketClientOptions extends WSClientConfigurableOptions { - pingInterval: number; - pongTimeout: number; - reconnectTimeout: number; - recvWindow: number; - - /** - * If true, require a "receipt" that the connection is ready for use (e.g. a specific event type) - */ - requireConnectionReadyConfirmation: boolean; - authPrivateConnectionsOnConnect: boolean; - authPrivateRequests: boolean; - reauthWSAPIOnReconnect: boolean; - - /** - * Whether to use native WebSocket ping/pong frames for heartbeats - */ - useNativeHeartbeats: boolean; -} +cancelSpotOrders( + params: WSAPISpotCancelOrdersParams, +): Promise> ⋮---- -/** - * If true, require a "receipt" that the connection is ready for use (e.g. a specific event type) - */ +cancelAllSpotOrders( + params: WSAPISpotCancelAllOrdersParams, +): Promise> ⋮---- -/** - * Whether to use native WebSocket ping/pong frames for heartbeats - */ +submitLinearSwapOrder( + params: WSAPIDerivativesOrderParams, +): Promise> ⋮---- -export type WsMarket = 'spot' | 'futures'; +submitLinearSwapCrossOrder( + params: WSAPIDerivativesOrderParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'create_cross_order', + params, + ); ⋮---- -export type WsEventInternalSrc = 'event' | 'function' | 'frame'; +submitLinearSwapBatchOrders( + params: WSAPIDerivativesBatchOrderParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'create_batchorder', + params, + ); ⋮---- -export interface WsSpotAuthParams { - authType: 'api'; - accessKey: string; - signatureMethod: 'HmacSHA256' | 'Ed25519'; - signatureVersion: '2.1'; - timestamp: string; - signature: string; -} +submitLinearSwapCrossBatchOrders( + params: WSAPIDerivativesBatchOrderParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'create_cross_batchorder', + params, + ); ⋮---- -export interface WsDerivativesAuthParams { - op: 'auth'; - type: 'api'; - AccessKeyId: string; - SignatureMethod: 'HmacSHA256' | 'Ed25519'; - SignatureVersion: '2'; - Timestamp: string; - Signature: string; -} +cancelLinearSwapOrder( + params: WSAPIDerivativesCancelOrderParams, +): Promise> ⋮---- -/** Vague structure for JSON-parsed incoming msg */ -export type ParsedWsMessage = Record; - -================ -File: .nvmrc -================ -v24.18.0 - -================ -File: eslint.config.cjs -================ -/* eslint-disable @typescript-eslint/no-require-imports */ - -================ -File: examples/Derivatives/Private/submitOrder.ts -================ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { FuturesClient } from '../../../src/index.js'; +cancelLinearSwapCrossOrder( + params: WSAPIDerivativesCancelOrderParams, +): Promise> ⋮---- -// This example shows how to call HTX Derivatives API endpoints for SUBMITTING ORDERS. +cancelAllLinearSwapOrders( + params: WSAPIDerivativesCancelAllOrdersParams, +): Promise> ⋮---- -/** - * import { FuturesClient } from '@siebly/htx-api'; - */ +cancelAllLinearSwapCrossOrders( + params: WSAPIDerivativesCancelAllOrdersParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'cross_cancelall', + params, + ); ⋮---- -async function submitLinearSwapIsolatedOrder() +placeLinearSwapOrder( + params: WSAPIDerivativesPlaceOrderParams, +): Promise> ⋮---- -async function submitLinearSwapCrossOrder() +placeLinearSwapBatchOrders( + params: WSAPIDerivativesBatchPlaceOrderParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'place_batch_orders', + params, + ); ⋮---- -async function submitLinearSwapIsolatedBatchOrders() +cancelLinearSwapV5Order( + params: WSAPIDerivativesV5CancelOrderParams, +): Promise> ⋮---- -async function submitLinearSwapCrossBatchOrders() +cancelLinearSwapV5BatchOrders( + params: WSAPIDerivativesV5CancelOrderParams[], + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'cancel_batch_orders', + params, + ); ⋮---- -async function submitMultiAssetOrder() +cancelAllLinearSwapV5Orders( + params: WSAPIDerivativesV5CancelAllOrdersParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.linearSwapTrade, + 'cancel_all_orders', + params, + ); ⋮---- -async function submitMultiAssetBatchOrders() +submitCoinDeliveryOrder( + params: WSAPIDerivativesOrderParams, +): Promise> ⋮---- -async function submitCoinMDeliveryOrder() +submitCoinDeliveryBatchOrders( + params: WSAPIDerivativesBatchOrderParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.coinDeliveryTrade, + 'create_batchorder', + params, + ); ⋮---- -async function submitCoinMDeliveryBatchOrders() +cancelCoinDeliveryOrder( + params: WSAPIDerivativesCancelOrderParams, +): Promise> ⋮---- -async function submitCoinMPerpOrder() +cancelAllCoinDeliveryOrders( + params: WSAPIDerivativesCancelAllOrdersParams, +): Promise> ⋮---- -async function submitCoinMPerpBatchOrders() +submitCoinSwapOrder( + params: WSAPIDerivativesOrderParams, +): Promise> ⋮---- -async function submitCoinMPerpLightningCloseOrder() +submitCoinSwapBatchOrders( + params: WSAPIDerivativesBatchOrderParams, + ): Promise< + WSAPIBaseResponse + > { + return this.wsClient.sendWSAPIRequest( + WS_KEY_MAP.coinSwapTrade, + 'create_batchorder', + params, + ); ⋮---- -// Uncomment the function you want to test: +cancelCoinSwapOrder( + params: WSAPIDerivativesCancelOrderParams, +): Promise> ⋮---- -// submitLinearSwapIsolatedOrder(); -// submitLinearSwapCrossOrder(); -// submitLinearSwapIsolatedBatchOrders(); -// submitLinearSwapCrossBatchOrders(); -//submitMultiAssetOrder(); -// submitMultiAssetBatchOrders(); -// submitCoinMDeliveryOrder(); -// submitCoinMDeliveryBatchOrders(); -// submitCoinMPerpOrder(); -// submitCoinMPerpBatchOrders(); -// submitCoinMPerpLightningCloseOrder(); +cancelAllCoinSwapOrders( + params: WSAPIDerivativesCancelAllOrdersParams, +): Promise> +⋮---- +private setupDefaultEventListeners() ================ -File: examples/Derivatives/WebSockets/wsAPI.ts +File: jest.config.cjs ================ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// Typed derivatives WS API example using WebsocketAPIClient. +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ ⋮---- -import { - DefaultLogger, - LogParams, - WebsocketAPIClient, - WS_KEY_MAP, -} from '../../../src/index.js'; -⋮---- -// Install from npm in your own project: -// import { WebsocketAPIClient, WS_KEY_MAP } from '@siebly/htx-api'; +/** @type {import('jest').Config} */ ⋮---- -// console.log('trace', ...params); +// All imported modules in your tests should be mocked automatically +// automock: false, ⋮---- -async function start() +// Stop running tests after `n` failures +// bail: 0, +bail: false, // enable to stop test when an error occur, ⋮---- -/** - * Optional: authenticate any trade websocket before the first request. - * The first WS API request will also connect/authenticate automatically. - */ -// await client.getWSClient().connectWSAPI(WS_KEY_MAP.linearSwapTrade); -// await client.getWSClient().connectWSAPI(WS_KEY_MAP.coinDeliveryTrade); -// await client.getWSClient().connectWSAPI(WS_KEY_MAP.coinSwapTrade); +// The directory where Jest should store its cached dependency information +// cacheDirectory: "/private/var/folders/kf/2k3sz4px6c9cbyzj1h_b192h0000gn/T/jest_dx", ⋮---- -/** - * These are live trading commands. Keep this false until the params below - * have been reviewed for your account, contract, and order intent. - */ - -================ -File: examples/Spot/Private/submitOrder.ts -================ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { SpotClient } from '../../../src/index.js'; +// Automatically clear mock calls, instances, contexts and results before every test ⋮---- -// This example shows how to call HTX Spot API endpoints for SUBMITTING ORDERS. +// Indicates whether the coverage information should be collected while executing the test ⋮---- -/** - * import { SpotClient } from '@siebly/htx-api'; - */ +// An array of glob patterns indicating a set of files for which coverage information should be collected ⋮---- -/** - * HTX API uses API Key and API Secret. - * - * API Key Permissions Required: Trade permission (create orders) - */ -/* const client = new SpotClient({ - apiKey: process.env.API_SPOT_KEY || 'insertApiKeyHere', - apiSecret: process.env.API_SPOT_SECRET || 'insertApiSecretHere', -}); */ +// The directory where Jest should output its coverage files ⋮---- -async function getSpotAccountId(): Promise +// An array of regexp pattern strings used to skip coverage collection +// coveragePathIgnorePatterns: [ +// "/node_modules/" +// ], ⋮---- -async function submitMarketOrder() +// Indicates which provider should be used to instrument code for coverage ⋮---- -amount: '1', // buy-market amount is quote currency value (USDT) +// A list of reporter names that Jest uses when writing coverage reports +// coverageReporters: [ +// "json", +// "text", +// "lcov", +// "clover" +// ], ⋮---- -async function submitLimitOrder() +// setupFiles: ['./test/setupTests.ts'], ⋮---- -async function submitLimitMakerOrder() +// An object that configures minimum threshold enforcement for coverage results +// coverageThreshold: undefined, ⋮---- -async function submitBatchOrders() +// A path to a custom dependency extractor +// dependencyExtractor: undefined, ⋮---- -// Uncomment the function you want to test: +// Make calling deprecated APIs throw helpful error messages +// errorOnDeprecated: false, ⋮---- -// submitMarketOrder(); -// submitLimitOrder(); -// submitLimitMakerOrder(); -// submitBatchOrders(); - -================ -File: examples/Spot/WebSockets/wsAPI.RAW.ts -================ -import { - DefaultLogger, - LogParams, - WebsocketClient, - WS_KEY_MAP, -} from '../../../src/index.js'; +// The default configuration for fake timers +// fakeTimers: { +// "enableGlobally": false +// }, ⋮---- -// Install from npm in your own project: -// import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; +// Force coverage collection from ignored files using an array of glob patterns +// forceCoverageMatch: [], ⋮---- -// console.log('trace', ...params); +// A path to a module which exports an async function that is triggered once before all test suites +// globalSetup: undefined, ⋮---- -async function start() +// A path to a module which exports an async function that is triggered once after all test suites +// globalTeardown: undefined, ⋮---- -/** - * WebsocketClient can send low-level WS API request/response commands with - * sendWSAPIRequest(). Use WebsocketAPIClient if you prefer typed wrapper - * methods for the same trading commands. - */ +// A set of global variables that need to be available in all test environments +// globals: {}, ⋮---- -/** - * Optional: connect/authenticate before the first request. - */ -// await client.connectWSAPI(WS_KEY_MAP.spotTrade); - -================ -File: src/lib/websocket/websocket-util.ts -================ -import { MessageEventLike } from '../../types/websockets/ws-events.js'; -import { - WebSocketBinaryData, - WebSocketLike, -} from '../../types/websockets/ws-portable.js'; -import { WSTopic } from '../../types/websockets/ws-subscriptions.js'; +// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. +maxWorkers: '1', // run tests sequentially to avoid nonce issues ⋮---- -/** Spot Market Data */ +// An array of directory names to be searched recursively up from the requiring module's location +// moduleDirectories: [ +// "node_modules" +// ], ⋮---- -/** Spot high-frequency market data */ +// An array of file extensions your modules use ⋮---- -/** Spot Account Events */ +// modulePaths: ['src'], ⋮---- -/** Spot WS API */ +// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module +// moduleNameMapper: {}, ⋮---- -/** Linear futures market data */ +// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader +// modulePathIgnorePatterns: [], ⋮---- -/** Linear futures account events */ +// Activates notifications for test results +// notify: false, ⋮---- -/** Linear futures WS API */ +// An enum that specifies notification mode. Requires { notify: true } +// notifyMode: "failure-change", ⋮---- -/** V5 unified derivatives account events */ +// A preset that is used as a base for Jest's configuration +// preset: undefined, ⋮---- -/** This is used to differentiate between each of the available websocket streams. */ -export type WsKey = (typeof WS_KEY_MAP)[keyof typeof WS_KEY_MAP]; +// Run tests from one or more projects +// projects: undefined, ⋮---- -export type WSOperation = 'subscribe' | 'unsubscribe'; +// Use this configuration option to add custom reporters to Jest +// reporters: undefined, ⋮---- -export type HtxWSNetwork = 'standard' | 'aws'; +// Automatically reset mock state before every test +// resetMocks: false, ⋮---- -/** - * Normalised internal format for a request on a topic, with optional parameters. - * - * - Topic: the exchange topic string. - * - Payload: optional exchange-specific fields to merge into the request. - */ -export interface WSTopicRequest< - TWSTopic extends WSTopic = WSTopic, - TWSPayload = Record, -> { - topic: TWSTopic; - payload?: TWSPayload; -} +// Reset the module registry before running each individual test +// resetModules: false, ⋮---- -/** Conveniently allow users to request either string topics or topic objects. */ -export type WSTopicRequestOrStringTopic< - TWSTopic extends WSTopic, - TWSPayload = Record, -> = WSTopicRequest | string; +// A path to a custom resolver +// resolver: undefined, ⋮---- -export interface HTXSpotMarketWSRequest { - sub?: string | string[]; - unsub?: string | string[]; - req?: string; - id?: string | number; -} +// Automatically restore mock state and implementation before every test +// restoreMocks: false, ⋮---- -export interface HTXSpotPrivateWSRequest { - action: 'sub' | 'unsub' | 'req' | 'ping' | 'pong'; - ch?: string; - params?: Record; - data?: Record; -} +// The root directory that Jest should scan for tests and modules within +// rootDir: undefined, ⋮---- -export interface HTXDerivativesWSRequest { - op: 'sub' | 'unsub' | 'req' | 'ping' | 'pong' | 'auth' | string; - cid?: string; - topic?: string; - data?: Record | Record[]; - [key: string]: unknown; -} +// A list of paths to directories that Jest should use to search for files in +// roots: [ +// "" +// ], ⋮---- -export type HTXWSRequest = - | HTXSpotMarketWSRequest - | HTXSpotPrivateWSRequest - | HTXDerivativesWSRequest; +// Allows you to use a custom runner instead of Jest's default test runner +// runner: "jest-runner", ⋮---- -export interface HTXSpotWSAPIRawRequest< - TOperation extends string = string, - TParams = unknown, -> { - cid: string; - ch: TOperation; - params?: TParams; -} +// The paths to modules that run some code to configure or set up the testing environment before each test +// setupFiles: [], ⋮---- -export interface HTXDerivativesWSAPIRawRequest< - TOperation extends string = string, - TParams = unknown, -> { - op: TOperation; - cid: string; - data?: TParams; -} +// A list of paths to modules that run some code to configure or set up the testing framework before each test +// setupFilesAfterEnv: [], ⋮---- -export type HTXWSAPIRawRequest = - | HTXSpotWSAPIRawRequest - | HTXDerivativesWSAPIRawRequest; +// The number of seconds after which a test is considered as slow and reported as such in the results. +// slowTestThreshold: 5, ⋮---- -/** - * ws.terminate() is undefined in browsers. - * This only works in node.js, not in browsers. - * Does nothing if `ws` is undefined. Does nothing in browsers. - */ -export function safeTerminateWs( - ws?: WebSocketLike | unknown, - fallbackToClose?: boolean, -): boolean +// A list of paths to snapshot serializer modules Jest should use for snapshot testing +// snapshotSerializers: [], ⋮---- -export function isSpotWsKey(wsKey: WsKey): boolean +// The test environment that will be used for testing +// testEnvironment: "jest-environment-node", ⋮---- -export function isDerivativesWsKey(wsKey: WsKey): boolean -⋮---- -export function isPrivateWsKey(wsKey: WsKey): boolean -⋮---- -export function isTradeWsKey(wsKey: WsKey): boolean -⋮---- -export function isGzipWsKey(wsKey: WsKey): boolean +// Options that will be passed to the testEnvironment +// testEnvironmentOptions: {}, ⋮---- -export function getWsBaseUrl(wsKey: WsKey, network: HtxWSNetwork): string +// Adds a location field to test results +// testLocationInResults: false, ⋮---- -export function getWsUrl(wsKey: WsKey, network: HtxWSNetwork): string +// The glob patterns Jest uses to detect test files ⋮---- -export function getPromiseRefForWSAPIRequest( - wsKey: WsKey, - requestEvent: HTXWSAPIRawRequest, -): string +// "**/__tests__/**/*.[jt]s?(x)", ⋮---- -export function getPromiseRefPrefixForWSAPIRequest(wsKey: WsKey): string +// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped +// testPathIgnorePatterns: [ +// "/node_modules/" +// ], ⋮---- -export function isBufferMessageEvent( - msg: unknown, -): msg is MessageEventLike +// The regexp pattern or array of patterns that Jest uses to detect test files +// testRegex: [], ⋮---- -export function isBinaryLike(data: unknown): data is WebSocketBinaryData +// This option allows the use of a custom results processor +// testResultsProcessor: undefined, ⋮---- -export function binaryDataToUint8Array( - data: WebSocketBinaryData, -): Uint8Array +// This option allows use of a custom test runner +// testRunner: "jest-circus/runner", ⋮---- -export function bufferLooksLikeText( - data?: WebSocketBinaryData | null, -): boolean +// A map from regular expressions to paths to transformers +// transform: undefined, ⋮---- -export async function decompressMessageEvent( - event: MessageEventLike, - format: WebSocketCompressionFormat = 'gzip', -): Promise> +// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation +// transformIgnorePatterns: [ +// "/node_modules/", +// "\\.pnp\\.[^\\/]+$" +// ], ⋮---- -start(controller) +// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them +// unmockedModulePathPatterns: undefined, ⋮---- -export type WebSocketCompressionFormat = 'deflate' | 'deflate-raw' | 'gzip'; +// Indicates whether each individual test should be reported during the run +// verbose: undefined, +verbose: true, // report individual test ⋮---- -function isNodeBuffer(data: unknown): data is Uint8Array +// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode +// watchPathIgnorePatterns: [], ⋮---- -Buffer?: +// Whether to use watchman for file crawling +// watchman: true, ================ -File: src/lib/BaseWSClient.ts +File: tsconfig.json ================ -import WebSocketImplementation from 'isomorphic-ws'; +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "noEmitOnError": true, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": false, + "inlineSourceMap": false, + "lib": ["esnext", "dom"], + "listEmittedFiles": false, + "listFiles": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noUnusedParameters": true, + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "skipLibCheck": false, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "types": ["node"], + "target": "esnext" + }, + "exclude": ["node_modules", "dist"], + "include": ["src/**/*.*"] +} + +================ +File: examples/Derivatives/Private/submitOrder.ts +================ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { FuturesClient } from '../../../src/index.js'; ⋮---- -import { - isMessageEvent, - MessageEventLike, -} from '../types/websockets/ws-events.js'; -import { - WebsocketClientOptions, - WSClientConfigurableOptions, - WsEventInternalSrc, -} from '../types/websockets/ws-general.js'; -import { - EventListener, - WebSocketLike, - WSConnectionOptions, -} from '../types/websockets/ws-portable.js'; -import { WSTopic } from '../types/websockets/ws-subscriptions.js'; -import { EventEmitter } from './event-emitter.js'; -import { isObjectLike } from './misc-util.js'; -import { checkWebCryptoAPISupported } from './webCryptoAPI.js'; -import { DefaultLogger } from './websocket/logger.js'; -import { - decompressMessageEvent, - isBufferMessageEvent, - safeTerminateWs, - WSOperation, - WSTopicRequest, - WSTopicRequestOrStringTopic, -} from './websocket/websocket-util.js'; -import { WsStore } from './websocket/WsStore.js'; -import { - DeferredPromise, - WSConnectedResult, - WsConnectionStateEnum, -} from './websocket/WsStore.types.js'; +// This example shows how to call HTX Derivatives API endpoints for SUBMITTING ORDERS. ⋮---- -interface WebSocketConstructor { - new ( - url: string, - protocols?: string | string[], - options?: Omit, - ): WebSocketLike; -} +/** + * import { FuturesClient } from '@siebly/htx-api'; + */ ⋮---- -function isNativeBrowserWebSocket(ws: WebSocketLike): boolean +async function submitLinearSwapIsolatedOrder() ⋮---- -type UseTheExceptionEventInstead = never; -type WSClientEventPayload = Record & { - wsKey: WsKey; -}; +async function submitLinearSwapCrossOrder() ⋮---- -type WSClientEventPayloadOrArray = - | WSClientEventPayload - | WSClientEventPayload[]; +async function submitLinearSwapIsolatedBatchOrders() ⋮---- -interface WSClientEventMap { - /** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */ - open: (evt: { - wsKey: WsKey; - event: unknown; - wsUrl: string; - ws: WebSocketLike; - }) => void; - - /** Reconnecting a dropped connection */ - reconnecting: (evt: { wsKey: WsKey; event: unknown }) => void; - - /** Successfully reconnected a connection that dropped */ - reconnected: (evt: { - wsKey: WsKey; - event: unknown; - wsUrl: string; - ws: WebSocketLike; - }) => void; - - /** Connection closed */ - close: (evt: { wsKey: WsKey; event: unknown }) => void; - - /** Received reply to websocket command (e.g. after subscribing to topics) */ - response: (response: WSClientEventPayloadOrArray) => void; - - /** Received data for topic */ - message: (response: WSClientEventPayloadOrArray) => void; - - /** Exception from ws client OR custom listeners (e.g. if you throw inside your event handler) */ - exception: (response: WSClientEventPayloadOrArray) => void; - - /** - * See for more information: https://github.com/tiagosiebler/bybit-api/issues/413 - * @deprecated Use the 'exception' event instead. The 'error' event had the unintended consequence of throwing an unhandled promise rejection. - */ - error: UseTheExceptionEventInstead; - - /** Confirmation that a connection successfully authenticated */ - authenticated: (event: WSClientEventPayload) => void; -} +async function submitLinearSwapCrossBatchOrders() ⋮---- -/** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */ +async function submitMultiAssetOrder() ⋮---- -/** Reconnecting a dropped connection */ +async function submitMultiAssetBatchOrders() ⋮---- -/** Successfully reconnected a connection that dropped */ +async function submitCoinMDeliveryOrder() ⋮---- -/** Connection closed */ +async function submitCoinMDeliveryBatchOrders() ⋮---- -/** Received reply to websocket command (e.g. after subscribing to topics) */ +async function submitCoinMPerpOrder() ⋮---- -/** Received data for topic */ +async function submitCoinMPerpBatchOrders() ⋮---- -/** Exception from ws client OR custom listeners (e.g. if you throw inside your event handler) */ +async function submitCoinMPerpLightningCloseOrder() ⋮---- -/** - * See for more information: https://github.com/tiagosiebler/bybit-api/issues/413 - * @deprecated Use the 'exception' event instead. The 'error' event had the unintended consequence of throwing an unhandled promise rejection. - */ +// Uncomment the function you want to test: ⋮---- -/** Confirmation that a connection successfully authenticated */ +// submitLinearSwapIsolatedOrder(); +// submitLinearSwapCrossOrder(); +// submitLinearSwapIsolatedBatchOrders(); +// submitLinearSwapCrossBatchOrders(); +//submitMultiAssetOrder(); +// submitMultiAssetBatchOrders(); +// submitCoinMDeliveryOrder(); +// submitCoinMDeliveryBatchOrders(); +// submitCoinMPerpOrder(); +// submitCoinMPerpBatchOrders(); +// submitCoinMPerpLightningCloseOrder(); + +================ +File: examples/Derivatives/WebSockets/wsAPI.RAW.ts +================ +// Raw low-level WS API example using WebsocketClient.sendWSAPIRequest(). +// Prefer WebsocketAPIClient for typed wrapper methods. ⋮---- -export interface EmittableEvent< - TEventType extends keyof WSClientEventMap = - keyof WSClientEventMap, -> { - eventType: - | TEventType - | 'pong' - | 'connectionReady' // tied to "requireConnectionReadyConfirmation"; - | 'connectionReadyForAuth'; // tied to specific events we need to wait for, before we can begin post-connect auth - event: Parameters[TEventType]>[0]; - isWSAPIResponse?: boolean; -} +import { + DefaultLogger, + LogParams, + WebsocketClient, + WS_KEY_MAP, +} from '../../../src/index.js'; ⋮---- -| 'connectionReady' // tied to "requireConnectionReadyConfirmation"; -| 'connectionReadyForAuth'; // tied to specific events we need to wait for, before we can begin post-connect auth +// Install from npm in your own project: +// import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; ⋮---- -// Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837 -export interface BaseWebsocketClient< - TWSKey extends string, - TWSRequestEvent extends object, -> { - /** @internal preserves the request-event generic on the merged class type. */ - readonly __requestEventType?: TWSRequestEvent; - - on>( - event: U, - listener: WSClientEventMap[U], - ): this; - - addListener>( - event: U, - listener: WSClientEventMap[U], - ): this; - - once>( - event: U, - listener: WSClientEventMap[U], - ): this; - - prependListener>( - event: U, - listener: WSClientEventMap[U], - ): this; - - prependOnceListener>( - event: U, - listener: WSClientEventMap[U], - ): this; - - off>( - event: U, - listener: WSClientEventMap[U], - ): this; - - removeListener>( - event: U, - listener: WSClientEventMap[U], - ): this; - - removeAllListeners(event?: keyof WSClientEventMap): this; - - listeners(event: keyof WSClientEventMap): EventListener[]; - - listenerCount( - event: keyof WSClientEventMap, - listener?: WSClientEventMap[keyof WSClientEventMap], - ): number; - - emit>( - event: U, - ...args: Parameters[U]> - ): boolean; -} -⋮---- -/** @internal preserves the request-event generic on the merged class type. */ -⋮---- -on>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -addListener>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -once>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -prependListener>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -prependOnceListener>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -off>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -removeListener>( - event: U, - listener: WSClientEventMap[U], - ): this; -⋮---- -removeAllListeners(event?: keyof WSClientEventMap): this; -⋮---- -listeners(event: keyof WSClientEventMap): EventListener[]; -⋮---- -listenerCount( - event: keyof WSClientEventMap, - listener?: WSClientEventMap[keyof WSClientEventMap], - ): number; +// console.log('trace', ...params); ⋮---- -emit>( - event: U, - ...args: Parameters[U]> - ): boolean; +async function start() + +================ +File: examples/Spot/Private/submitOrder.ts +================ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { SpotClient } from '../../../src/index.js'; ⋮---- -/** - * A midflight WS request event (e.g. subscribe to these topics). - * - * - requestKey: unique identifier for this request, if available. Can be anything as a string. - * - requestEvent: the raw request, as an object, that will be sent on the ws connection. This may contain multiple topics/requests in one object, if the exchange supports it. - */ -export interface MidflightWsRequestEvent { - requestKey: string | number; - requestEvent: TEvent; -} +// This example shows how to call HTX Spot API endpoints for SUBMITTING ORDERS. ⋮---- /** - * Appends wsKey and isWSAPIResponse to all events. - * Some events are arrays, this handles that nested scenario too. + * import { SpotClient } from '@siebly/htx-api'; */ -function getFinalEmittable( - emittable: EmittableEvent, - wsKey: TWSKey, - isWSAPIResponse?: boolean, -): WSClientEventPayload; -function getFinalEmittable( - emittable: EmittableEvent[], - wsKey: TWSKey, - isWSAPIResponse?: boolean, -): WSClientEventPayload[]; -function getFinalEmittable( - emittable: EmittableEvent | EmittableEvent[], - wsKey: TWSKey, - isWSAPIResponse?: boolean, -): WSClientEventPayload | WSClientEventPayload[] -⋮---- -// Some topics just emit an array. -// This is consistent with how it was before the WS API upgrade: -⋮---- -// const { event, ...others } = emittable; -// return { -// ...others, -// event: event.map((subEvent) => -// getFinalEmittable(subEvent, wsKey, isWSAPIResponse), -// ), -// }; ⋮---- /** - * Users can conveniently pass topics as strings or objects (object has topic name + optional params). + * HTX API uses API Key and API Secret. * - * This method normalises topics into objects (object has topic name + optional params). + * API Key Permissions Required: Trade permission (create orders) */ -function getNormalisedTopicRequests( - wsTopicRequests: WSTopicRequestOrStringTopic[], -): WSTopicRequest[] +/* const client = new SpotClient({ + apiKey: process.env.API_SPOT_KEY || 'insertApiKeyHere', + apiSecret: process.env.API_SPOT_SECRET || 'insertApiSecretHere', +}); */ ⋮---- -// passed as string, convert to object +async function getSpotAccountId(): Promise ⋮---- -// already a normalised object, thanks to user +async function submitMarketOrder() ⋮---- -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -export abstract class BaseWebsocketClient< -TWSKey extends string, +amount: '1', // buy-market amount is quote currency value (USDT) ⋮---- -constructor( - options?: WSClientConfigurableOptions & { wsLoggerCategory: string }, - logger?: DefaultLogger, -) +async function submitLimitOrder() ⋮---- -// Requires a confirmation "response" from the ws connection before assuming it is ready +async function submitLimitMakerOrder() ⋮---- -// Automatically auth after opening a connection? +async function submitBatchOrders() ⋮---- -// Automatically include auth/sign/token with every WS request. -// Automatically handled during getWsRequestEvents. +// Uncomment the function you want to test: ⋮---- -// Automatically re-auth WS API, if we were auth'd before and get reconnected +// submitMarketOrder(); +// submitLimitOrder(); +// submitLimitMakerOrder(); +// submitBatchOrders(); + +================ +File: examples/Spot/WebSockets/wsAPI.RAW.ts +================ +import { + DefaultLogger, + LogParams, + WebsocketClient, + WS_KEY_MAP, +} from '../../../src/index.js'; ⋮---- -// Whether to use native heartbeats (depends on the exchange) +// Install from npm in your own project: +// import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; ⋮---- -// Check Web Crypto API support when credentials are provided and no custom sign function is used +// console.log('trace', ...params); +⋮---- +async function start() ⋮---- /** - * Return true if this wsKey connection should automatically authenticate immediately after connecting + * WebsocketClient can send low-level WS API request/response commands with + * sendWSAPIRequest(). Use WebsocketAPIClient if you prefer typed wrapper + * methods for the same trading commands. */ -protected abstract isAuthOnConnectWsKey(wsKey: TWSKey): boolean; -⋮---- -protected abstract isCustomReconnectionNeeded(wsKey: TWSKey): boolean; -⋮---- -protected abstract triggerCustomReconnectionWorkflow( - wsKey: TWSKey, - ): Promise; ⋮---- -protected abstract sendPingEvent(wsKey: TWSKey, ws: WebSocketLike): boolean; +/** + * Optional: connect/authenticate before the first request. + */ +// await client.connectWSAPI(WS_KEY_MAP.spotTrade); + +================ +File: src/lib/websocket/WsStore.types.ts +================ +import { WebSocketLike } from '../../types/websockets/ws-portable.js'; ⋮---- -protected abstract sendPongEvent( - wsKey: TWSKey, - ws: WebSocketLike, - event?: unknown, - ): void; +export enum WsConnectionStateEnum { + INITIAL = 0, + CONNECTING = 1, + CONNECTED = 2, + CLOSING = 3, + RECONNECTING = 4, + // ERROR_RECONNECTING = 5, + ERROR = 5, +} ⋮---- -protected abstract isWsPing(data: unknown): boolean; +// ERROR_RECONNECTING = 5, ⋮---- -protected abstract isWsPong(data: unknown): boolean; +export interface DeferredPromise { + resolve?: (value: TSuccess | PromiseLike) => void; + reject?: (value?: TError) => void; + promise?: Promise; +} ⋮---- -protected abstract authPrivateConnectionsOnConnect(_wsKey: TWSKey): boolean; +export interface WSConnectedResult { + wsKey: string; + ws: WebSocketLike; +} ⋮---- -/** - * Return the event sent to the server to trigger authentication. If the event requires waiting for another event first (e.g. a challenge), return 'waitForEvent' and the library will wait for that event before calling this method again. +export interface WsStoredState { + /** The currently active websocket connection */ + ws?: WebSocketLike; + + /** The current lifecycle state of the connection (enum) */ + connectionState?: WsConnectionStateEnum; + connectionStateChangedAt?: Date; + + /** 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; + + /** If a reconnection is in progress, this will have the timer for the delayed reconnect */ + activeReconnectTimer?: ReturnType | undefined; + /** + * When a connection attempt is in progress (even for reconnect), a promise is stored here. * - * Usually only called once per connection, unless the connection drops/is reset. + * This promise will resolve once connected (and will then get removed); */ -protected abstract getWsAuthRequestEvent( - wsKey: TWSKey, - eventToAuth?: object, - ): Promise; + deferredPromiseStore: Record; + + /** + * All the topics we are expected to be subscribed to on this connection (and we automatically resubscribe to if the connection drops) + * + * A "Set" and a deep-object-match are used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to) + */ + subscribedTopics: Set; + + /** Whether this connection has completed authentication (only applies to private connections) */ + isAuthenticated?: boolean; + + /** Tracks a multi-step authentication attempt that has not completed yet */ + authRequestState?: 'preparing' | 'waitingForEvent' | 'sending'; + + /** Auth-ready event received while the auth request was still being prepared */ + pendingAuthEvent?: object; + + /** + * Whether this connection has completed authentication before for the Websocket API, so it knows to automatically reauth if reconnected + */ + didAuthWSAPI?: boolean; + + /** To reauthenticate on the WS API, which channel do we send to? */ + WSAPIAuthChannel?: string; +} ⋮---- -protected abstract isPrivateTopicRequest( - request: WSTopicRequest, - wsKey: TWSKey, - ): boolean; +/** The currently active websocket connection */ ⋮---- -protected abstract getPrivateWSKeys(): TWSKey[]; +/** The current lifecycle state of the connection (enum) */ ⋮---- -protected abstract getWsUrl(wsKey: TWSKey): Promise; +/** A timer that will send an upstream heartbeat (ping) when it expires */ ⋮---- -protected abstract getMaxTopicsPerSubscribeEvent( - wsKey: TWSKey, - ): number | null; +/** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ ⋮---- -/** - * @returns one or more correctly structured request events for performing a operations over WS. This can vary per exchange spec. - */ -protected abstract getWsRequestEvents( - wsKey: TWSKey, - operation: WSOperation, - requests: WSTopicRequest[], - ): Promise[]>; +/** If a reconnection is in progress, this will have the timer for the delayed reconnect */ ⋮---- /** - * Abstraction called to sort ws events into emittable event types (response to a request, data update, etc) + * When a connection attempt is in progress (even for reconnect), a promise is stored here. + * + * This promise will resolve once connected (and will then get removed); */ -protected abstract resolveEmittableEvents( - wsKey: TWSKey, - event: MessageEventLike, - ): EmittableEvent[]; ⋮---- /** - * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library + * All the topics we are expected to be subscribed to on this connection (and we automatically resubscribe to if the connection drops) + * + * A "Set" and a deep-object-match are used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to) */ -protected abstract connectAll(): Promise[]; -⋮---- -protected isPrivateWsKey(wsKey: TWSKey): boolean -⋮---- -/** Returns auto-incrementing request ID, used to track promise references for async requests */ -protected getNewRequestId(): number -⋮---- -protected abstract sendWSAPIRequest( - wsKey: TWSKey, - operation: string, - params?: unknown, - ): Promise; ⋮---- -protected abstract sendWSAPIRequest( - wsKey: TWSKey, - channel: string, - params: unknown, - ): Promise; +/** Whether this connection has completed authentication (only applies to private connections) */ ⋮---- -public getTimeOffsetMs() +/** Tracks a multi-step authentication attempt that has not completed yet */ ⋮---- -public setTimeOffsetMs(newOffset: number) +/** Auth-ready event received while the auth request was still being prepared */ ⋮---- /** - * Don't call directly! Use subscribe() instead! - * - * Subscribe to one or more topics on a WS connection (identified by WS Key). - * - * - Topics are automatically cached - * - Connections are automatically opened, if not yet connected - * - Authentication is automatically handled - * - Topics are automatically resubscribed to, if something happens to the connection, unless you call unsubsribeTopicsForWsKey(topics, key). - * - * @param wsTopicRequests array of topics to subscribe to - * @param wsKey ws key referring to the ws connection these topics should be subscribed on + * Whether this connection has completed authentication before for the Websocket API, so it knows to automatically reauth if reconnected */ -protected async subscribeTopicsForWsKey( - wsTopicRequests: WSTopicRequestOrStringTopic[], - wsKey: TWSKey, -) ⋮---- -// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically +/** To reauthenticate on the WS API, which channel do we send to? */ + +================ +File: src/lib/misc-util.ts +================ +export function neverGuard(x: never, msg: string): Error ⋮---- -// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect +export function isObjectLike(value: unknown): value is Record ⋮---- -// Subscribe should happen automatically once connected, nothing to do here after topics are added to wsStore. +export function omitKeys( + source: Record, + keysToOmit: string[], +): Record ⋮---- -/** - * Are we in the process of connection? Nothing to send yet. - */ +export function isRecord(value: unknown): value is Record ⋮---- -// We're connected. Check if auth is needed and if already authenticated +export function removeInternalParamFields(params: TParams): TParams ⋮---- -/** - * If not authenticated yet and auth is required, don't request topics yet. - * - * Auth should already automatically be in progress, so no action needed from here. Topics will automatically subscribe post-auth success. - */ +export function getStringOrUndefined(value: unknown): string | undefined + +================ +File: src/types/websockets/ws-general.ts +================ +import { AxiosRequestConfig } from 'axios'; ⋮---- -// Finally, request subscription to topics if the connection is healthy and ready +import { RestClientOptions } from '../../lib/requestUtils.js'; +import { HtxWSNetwork } from '../../lib/websocket/websocket-util.js'; +import { WSConnectionOptions } from './ws-portable.js'; ⋮---- -protected async unsubscribeTopicsForWsKey( - wsTopicRequests: WSTopicRequestOrStringTopic[], - wsKey: TWSKey, -): Promise +/** General configuration for the WebsocketClient */ +export interface WSClientConfigurableOptions { + /** Your API key */ + apiKey?: string; + + /** Your API secret */ + apiSecret?: string; + + /** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds. */ + recvWindow?: number; + + /** How often to check if the connection is alive */ + pingInterval?: number; + + /** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */ + pongTimeout?: number; + + /** Delay in milliseconds before respawning the connection */ + reconnectTimeout?: number; + + restOptions?: RestClientOptions; + requestOptions?: AxiosRequestConfig; + + wsOptions?: WSConnectionOptions; + + wsUrl?: string; + + /** + * Switch between the AWS and Standard WebSocket domains. The AWS domain has better connectivity from certain regions. If you have trouble maintaining a stable connection, try switching the domain. + * + * AWS is used by default. If this field isn't set, but you're setting the baseUrlKey in the embedded restOptions, the WS domain will automatically switch to match the REST domain. + */ + wsEnvironment?: HtxWSNetwork; + + /** + * Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method + * + * Look in the examples folder for a demonstration on using node's createHmac instead. + */ + customSignMessageFn?: (message: string, secret: string) => Promise; + + /** + * If you authenticated the WS API before, automatically try to re-authenticate the WS API if you're disconnected/reconnected for any reason. + */ + reauthWSAPIOnReconnect?: boolean; + + /** + * Whether to use native WebSocket ping/pong frames for heartbeats. + */ + useNativeHeartbeats?: boolean; +} ⋮---- -// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically +/** Your API key */ ⋮---- -// If not connected, don't need to do anything. -// Removing the topic from the store is enough to stop it from being resubscribed to on reconnect. +/** Your API secret */ ⋮---- -// We're connected. Check if auth is needed and if already authenticated +/** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds. */ ⋮---- -/** - * If not authenticated yet and auth is required, don't need to do anything. - * We don't subscribe to topics until auth is complete anyway. - */ +/** How often to check if the connection is alive */ ⋮---- -// Finally, request subscription to topics if the connection is healthy and ready +/** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */ +⋮---- +/** Delay in milliseconds before respawning the connection */ ⋮---- /** - * Splits topic requests into two groups, public & private topic requests + * Switch between the AWS and Standard WebSocket domains. The AWS domain has better connectivity from certain regions. If you have trouble maintaining a stable connection, try switching the domain. + * + * AWS is used by default. If this field isn't set, but you're setting the baseUrlKey in the embedded restOptions, the WS domain will automatically switch to match the REST domain. */ -private sortTopicRequestsIntoPublicPrivate( - wsTopicRequests: WSTopicRequest[], - wsKey: TWSKey, -): -⋮---- -/** Get the WsStore that tracks websockets & topics */ -public getWsStore(): WsStore> -⋮---- -public close(wsKey: TWSKey, force?: boolean) ⋮---- -public closeAll(force?: boolean) +/** + * Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method + * + * Look in the examples folder for a demonstration on using node's createHmac instead. + */ ⋮---- -public isConnected(wsKey: TWSKey): boolean +/** + * If you authenticated the WS API before, automatically try to re-authenticate the WS API if you're disconnected/reconnected for any reason. + */ ⋮---- /** - * Request connection to a specific websocket, instead of waiting for automatic connection. + * Whether to use native WebSocket ping/pong frames for heartbeats. */ -public async connect( - wsKey: TWSKey, - customUrl?: string | undefined, - throwOnError?: boolean, -): Promise -⋮---- -// Important: don't check for RECONNECTING here, or this clashes with reconnectWithDelay()! -⋮---- -private connectToWsUrl(url: string, wsKey: TWSKey): WebSocketLike -⋮---- -// Event handlers for native heartbeats / ping/pong frames -⋮---- -private parseWsError( - context: string, - error: unknown, - wsKey: TWSKey, -): boolean -⋮---- -// Allow retry by default (in some places that call this). Prevent deadloop in hard failure (401) -⋮---- -/** Get a signature, build the auth request and send it */ -private async sendAuthRequest( - wsKey: TWSKey, - eventToAuth?: object, -): Promise -⋮---- -private async advanceAuthRequest( - wsKey: TWSKey, - authPromise: DeferredPromise, - eventToAuth?: object, -): Promise -⋮---- -// If not required, this won't return anything ⋮---- -// Short-circuit this for the next time it's called -⋮---- -private reconnectWithDelay(wsKey: TWSKey, connectionDelayMs: number) -⋮---- -// Some streams need a specialist reconnection workflow. -// E.g. the user data stream can't just be reconnected as is. +/** + * WS configuration that's always defined, regardless of user configuration + * (usually comes from defaults if there's no user-provided values) + */ +export interface WebsocketClientOptions extends WSClientConfigurableOptions { + pingInterval: number; + pongTimeout: number; + reconnectTimeout: number; + recvWindow: number; + + /** + * If true, require a "receipt" that the connection is ready for use (e.g. a specific event type) + */ + requireConnectionReadyConfirmation: boolean; + authPrivateConnectionsOnConnect: boolean; + authPrivateRequests: boolean; + reauthWSAPIOnReconnect: boolean; + + /** + * Whether to use native WebSocket ping/pong frames for heartbeats + */ + useNativeHeartbeats: boolean; +} ⋮---- -private ping(wsKey: TWSKey) +/** + * If true, require a "receipt" that the connection is ready for use (e.g. a specific event type) + */ ⋮---- /** - * Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously. - * If closed, trigger a reconnect immediately + * Whether to use native WebSocket ping/pong frames for heartbeats */ -protected executeReconnectableClose(wsKey: TWSKey, reason: string) ⋮---- -private clearTimers(wsKey: TWSKey) +export type WsMarket = 'spot' | 'futures'; ⋮---- -// Send a ping at intervals -private clearPingTimer(wsKey: TWSKey) +export type WsEventInternalSrc = 'event' | 'function' | 'frame'; ⋮---- -// Expect a pong within a time limit -private clearPongTimer(wsKey: TWSKey) +export interface WsSpotAuthParams { + authType: 'api'; + accessKey: string; + signatureMethod: 'HmacSHA256' | 'Ed25519'; + signatureVersion: '2.1'; + timestamp: string; + signature: string; +} ⋮---- -// this.logger.trace(`Cleared pong timeout for "${wsKey}"`); +export interface WsDerivativesAuthParams { + op: 'auth'; + type: 'api'; + AccessKeyId: string; + SignatureMethod: 'HmacSHA256' | 'Ed25519'; + SignatureVersion: '2'; + Timestamp: string; + Signature: string; +} ⋮---- -// this.logger.trace(`No active pong timer for "${wsKey}"`); +/** Vague structure for JSON-parsed incoming msg */ +export type ParsedWsMessage = Record; + +================ +File: .nvmrc +================ +v24.18.0 + +================ +File: eslint.config.cjs +================ +/* eslint-disable @typescript-eslint/no-require-imports */ + +================ +File: examples/Derivatives/WebSockets/privateWs.ts +================ +import { WebsocketClient, WS_KEY_MAP } from '../../../src/index.js'; ⋮---- -private clearReconnectTimer(wsKey: TWSKey) +// Install from npm in your own project: +// import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; ⋮---- -/** - * Returns a list of string events that can be individually sent upstream to complete subscribing/unsubscribing/etc to these topics - * - * If events are an object, these should be stringified (`return JSON.stringify(event);`) - * Each event returned by this will be sent one at a time - * - * Events are automatically split into smaller batches, by this method, if needed. - */ -protected async getWsOperationEventsForTopics( - topics: WSTopicRequest[], - wsKey: TWSKey, - operation: WSOperation, -): Promise[]> +async function start() ⋮---- -// Events that are ready to send (usually stringified JSON) +// customLogger,// optional: inject custom logger to control logging behavior (e.g. filter out verbose logs, log to file instead of console, etc.) ⋮---- /** - * Simply builds and sends subscribe events for a list of topics for a ws key + * V5 unified private websocket (use this if your account was upgraded to V5): + * wss://api.hbdm.vn/ws/v5/notification * - * @private Use the `subscribe(topics)` or `subscribeTopicsForWsKey(topics, wsKey)` method to subscribe to topics. Send WS message to subscribe to topics. + * account needs no extra params. + * trade and positions require contract_code (use '*' for all contracts). */ -private async requestSubscribeTopics( - wsKey: TWSKey, - wsTopicRequests: WSTopicRequest[], -) -⋮---- -// Automatically splits requests into smaller batches, if needed -⋮---- -`Subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`, // Events: "${JSON.stringify(topics)}" ⋮---- /** - * Simply builds and sends unsubscribe events for a list of topics for a ws key + * Legacy USDT-margined linear swap private websocket: + * wss://api.hbdm.vn/linear-swap-notification * - * @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics. + * Only works for accounts that have NOT migrated to V5 yet. */ -private async requestUnsubscribeTopics( - wsKey: TWSKey, - wsTopicRequests: WSTopicRequest[], -) +// client.subscribe( +// [ +// 'orders.btc-usdt', +// 'orders_cross.btc-usdt', +// 'accounts.btc-usdt', +// 'accounts_cross.USDT', +// 'positions.btc-usdt', +// 'positions_cross.btc-usdt', +// 'matchOrders.btc-usdt', +// 'matchOrders_cross.btc-usdt', +// ], +// WS_KEY_MAP.linearSwapPrivate, +// ); ⋮---- /** - * Try sending a string event on a WS connection (identified by the WS Key) + * Coin-margined perpetual swap private websocket: + * wss://api.hbdm.vn/swap-notification by default. */ -private tryWsSendWithResult( - wsKey: TWSKey, - wsMessage: string, - throwExceptions?: boolean, -): boolean + +================ +File: examples/Derivatives/WebSockets/wsAPI.ts +================ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// Typed derivatives WS API example using WebsocketAPIClient. ⋮---- -public tryWsSend( - wsKey: TWSKey, - wsMessage: string, - throwExceptions?: boolean, -): void +import { + DefaultLogger, + LogParams, + WebsocketAPIClient, + WS_KEY_MAP, +} from '../../../src/index.js'; ⋮---- -/** Send a message while reporting whether it reached the socket API. */ -protected tryWsSendWithStatus( - wsKey: TWSKey, - wsMessage: string, - throwExceptions?: boolean, -): boolean +// Install from npm in your own project: +// import { WebsocketAPIClient, WS_KEY_MAP } from '@siebly/htx-api'; ⋮---- -private async onWsOpen( - event: unknown, - wsKey: TWSKey, - url: string, - ws: WebSocketLike, -) +// console.log('trace', ...params); ⋮---- -private resolveConnectionInProgressPromise(wsKey: TWSKey) +async function start() ⋮---- -// Resolve & cleanup deferred "connection attempt in progress" promise +/** + * Optional: authenticate any trade websocket before the first request. + * The first WS API request will also connect/authenticate automatically. + */ +// await client.getWSClient().connectWSAPI(WS_KEY_MAP.linearSwapTrade); +// await client.getWSClient().connectWSAPI(WS_KEY_MAP.coinDeliveryTrade); +// await client.getWSClient().connectWSAPI(WS_KEY_MAP.coinSwapTrade); ⋮---- /** - * Called automatically once a connection is ready. - * - Some exchanges are ready immediately after the connections open. - * - Some exchanges send an event to confirm the connection is ready for us. - * - * This method is called to act when the connection is ready. Use `requireConnectionReadyConfirmation` to control how this is called. + * These are live trading commands. Keep this false until the params below + * have been reviewed for your account, contract, and order intent. */ -private async onWsReadyForEvents(wsKey: TWSKey) + +================ +File: examples/Spot/WebSockets/wsAPI.ts +================ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + DefaultLogger, + LogParams, + WebsocketAPIClient, +} from '../../../src/index.js'; ⋮---- -// Some websockets require an auth packet to be sent after opening the connection +// Install from npm in your own project: +// import { WebsocketAPIClient } from '@siebly/htx-api'; ⋮---- -// Reconnect to topics known before it connected +// console.log('trace', ...params); ⋮---- -// Request sub to public topics, if any +async function start() ⋮---- -// Request sub to private topics, if auth on connect isn't needed +/** + * The WebsocketAPIClient is a typed REST-like wrapper for WS API + * request/response trading methods. It uses an embedded WebsocketClient for + * connection management and authentication. + */ ⋮---- /** - * Handle subscription to private topics _after_ authentication successfully completes asynchronously. - * - * Only used for exchanges that require auth before sending private topic subscription requests + * Optional: authenticate the spot trade websocket before the first request. + * The first WS API request will also connect/authenticate automatically. */ -private onWsAuthenticated(wsKey: TWSKey, event: unknown) +// await client.getWSClient().connectWSAPI(WS_KEY_MAP.spotTrade); ⋮---- -// Resolve & cleanup deferred "auth attempt in progress" promise +/** + * These are live trading commands. Keep this false until the params below + * have been reviewed for your account, symbol, and order intent. + */ ⋮---- -// Remove before continuing, in case there's more requests queued +// +// Example of a custom order ID: +// to make your own, always include the prefix: +// 'client-order-id': `${client.getOrderIdPrefix()}${Date.now()}`, +// +// or to generate a new unique order ID: +// 'client-order-id': client.generateNewOrderID(), +// +// Note: if you do use the client-order-id, it must be prefixed with `client.getOrderIdPrefix()` and unique. `client.generateNewOrderID()` is the recommended way to generate a unique order ID. +// Do not store state in custom order IDs. They are best used as look-up keys for a local state cache. +// For more guidance refer to the best practices on Siebly.io: +// https://siebly.io/reference/glossary#custom-order-id + +================ +File: src/lib/websocket/websocket-util.ts +================ +import { MessageEventLike } from '../../types/websockets/ws-events.js'; +import { + WebSocketBinaryData, + WebSocketLike, +} from '../../types/websockets/ws-portable.js'; +import { WSTopic } from '../../types/websockets/ws-subscriptions.js'; ⋮---- -private onWsPing( - event: unknown, - wsKey: TWSKey, - ws: WebSocketLike, - source: WsEventInternalSrc, -) +/** Spot Market Data */ ⋮---- -private onWsPong(event: unknown, wsKey: TWSKey, source: WsEventInternalSrc) +/** Spot high-frequency market data */ ⋮---- -// Necessary when native heartbeats are used +/** Spot Account Events */ ⋮---- -private async onWsMessage( - event: unknown, - wsKey: TWSKey, - ws: WebSocketLike, - didDecompress = false, -): Promise +/** Spot WS API */ ⋮---- -// console.log('onMessageRaw: ', (event as any).data); -// any message can clear the pong timer - wouldn't get a message if the ws wasn't working +/** Linear futures market data */ ⋮---- -// console.log(`raw event: `, { data, dataType, emittableEvents }); +/** Linear futures account events */ ⋮---- -// Other event types are automatically emitted here +/** Linear futures WS API */ ⋮---- -// this.logger.trace( -// `onWsMessage().emit(${emittable.eventType}).done()`, -// emittableFinalEvent, -// ); +/** V5 unified derivatives account events */ ⋮---- -// this.logger.trace('Decompressed message event from buffer', { -// ...this.WS_LOGGER_CATEGORY, -// wsKey, -// decompressed, -// }); +/** This is used to differentiate between each of the available websocket streams. */ +export type WsKey = (typeof WS_KEY_MAP)[keyof typeof WS_KEY_MAP]; ⋮---- -private onWsClose(event: unknown, wsKey: TWSKey) +export type WSOperation = 'subscribe' | 'unsubscribe'; ⋮---- -// unintentional close, attempt recovery +export type HtxWSNetwork = 'standard' | 'aws'; ⋮---- -// clean up any pending promises for this connection +/** + * Normalised internal format for a request on a topic, with optional parameters. + * + * - Topic: the exchange topic string. + * - Payload: optional exchange-specific fields to merge into the request. + */ +export interface WSTopicRequest< + TWSTopic extends WSTopic = WSTopic, + TWSPayload = Record, +> { + topic: TWSTopic; + payload?: TWSPayload; +} ⋮---- -// this.clearTopicsPendingSubscriptions(wsKey, true, 'WS Closed'); +/** Conveniently allow users to request either string topics or topic objects. */ +export type WSTopicRequestOrStringTopic< + TWSTopic extends WSTopic, + TWSPayload = Record, +> = WSTopicRequest | string; ⋮---- -// intentional close - clean up -// clean up any pending promises for this connection +export interface HTXSpotMarketWSRequest { + sub?: string | string[]; + unsub?: string | string[]; + req?: string; + id?: string | number; +} ⋮---- -// This was an intentional close, delete all state for this connection, as if it never existed: +export interface HTXSpotPrivateWSRequest { + action: 'sub' | 'unsub' | 'req' | 'ping' | 'pong'; + ch?: string; + params?: Record; + data?: Record; +} ⋮---- -private getWs(wsKey: TWSKey) +export interface HTXDerivativesWSRequest { + op: 'sub' | 'unsub' | 'req' | 'ping' | 'pong' | 'auth' | string; + cid?: string; + topic?: string; + data?: Record | Record[]; + [key: string]: unknown; +} ⋮---- -private setWsState(wsKey: TWSKey, state: WsConnectionStateEnum) +export type HTXWSRequest = + | HTXSpotMarketWSRequest + | HTXSpotPrivateWSRequest + | HTXDerivativesWSRequest; ⋮---- -/** - * Promise-driven method to assert that a ws has successfully connected (will await until connection is open) - */ -protected async assertIsConnected(wsKey: TWSKey): Promise +export interface HTXSpotWSAPIRawRequest< + TOperation extends string = string, + TParams = unknown, +> { + cid: string; + ch: TOperation; + params?: TParams; +} ⋮---- -// Already in progress? Await shared promise and retry +export interface HTXDerivativesWSAPIRawRequest< + TOperation extends string = string, + TParams = unknown, +> { + op: TOperation; + cid: string; + data?: TParams; +} ⋮---- -// Start connection, it should automatically store/return a promise. +export type HTXWSAPIRawRequest = + | HTXSpotWSAPIRawRequest + | HTXDerivativesWSAPIRawRequest; ⋮---- /** - * Promise-driven method to assert that a ws has been successfully authenticated (will await until auth is confirmed) - */ -public async assertIsAuthenticated(wsKey: TWSKey): Promise + * ws.terminate() is undefined in browsers. + * This only works in node.js, not in browsers. + * Does nothing if `ws` is undefined. Does nothing in browsers. + */ +export function safeTerminateWs( + ws?: WebSocketLike | unknown, + fallbackToClose?: boolean, +): boolean ⋮---- -// Already in progress? Await shared promise and retry +export function isSpotWsKey(wsKey: WsKey): boolean ⋮---- -// Start authentication, it should automatically store/return a promise. - -================ -File: src/index.ts -================ - - -================ -File: examples/Derivatives/WebSockets/privateWs.ts -================ -import { WebsocketClient, WS_KEY_MAP } from '../../../src/index.js'; +export function isDerivativesWsKey(wsKey: WsKey): boolean ⋮---- -// Install from npm in your own project: -// import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; +export function isPrivateWsKey(wsKey: WsKey): boolean ⋮---- -async function start() +export function isTradeWsKey(wsKey: WsKey): boolean ⋮---- -// customLogger,// optional: inject custom logger to control logging behavior (e.g. filter out verbose logs, log to file instead of console, etc.) +export function isGzipWsKey(wsKey: WsKey): boolean ⋮---- -/** - * V5 unified private websocket (use this if your account was upgraded to V5): - * wss://api.hbdm.vn/ws/v5/notification - * - * account needs no extra params. - * trade and positions require contract_code (use '*' for all contracts). - */ +export function getWsBaseUrl(wsKey: WsKey, network: HtxWSNetwork): string ⋮---- -/** - * Legacy USDT-margined linear swap private websocket: - * wss://api.hbdm.vn/linear-swap-notification - * - * Only works for accounts that have NOT migrated to V5 yet. - */ -// client.subscribe( -// [ -// 'orders.btc-usdt', -// 'orders_cross.btc-usdt', -// 'accounts.btc-usdt', -// 'accounts_cross.USDT', -// 'positions.btc-usdt', -// 'positions_cross.btc-usdt', -// 'matchOrders.btc-usdt', -// 'matchOrders_cross.btc-usdt', -// ], -// WS_KEY_MAP.linearSwapPrivate, -// ); +export function getWsUrl(wsKey: WsKey, network: HtxWSNetwork): string ⋮---- -/** - * Coin-margined perpetual swap private websocket: - * wss://api.hbdm.vn/swap-notification by default. - */ - -================ -File: examples/Spot/WebSockets/wsAPI.ts -================ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { - DefaultLogger, - LogParams, - WebsocketAPIClient, -} from '../../../src/index.js'; +export function getPromiseRefForWSAPIRequest( + wsKey: WsKey, + requestEvent: HTXWSAPIRawRequest, +): string ⋮---- -// Install from npm in your own project: -// import { WebsocketAPIClient } from '@siebly/htx-api'; +export function getPromiseRefPrefixForWSAPIRequest(wsKey: WsKey): string ⋮---- -// console.log('trace', ...params); +export function isBufferMessageEvent( + msg: unknown, +): msg is MessageEventLike ⋮---- -async function start() +export function isBinaryLike(data: unknown): data is WebSocketBinaryData ⋮---- -/** - * The WebsocketAPIClient is a typed REST-like wrapper for WS API - * request/response trading methods. It uses an embedded WebsocketClient for - * connection management and authentication. - */ +export function binaryDataToUint8Array( + data: WebSocketBinaryData, +): Uint8Array ⋮---- -/** - * Optional: authenticate the spot trade websocket before the first request. - * The first WS API request will also connect/authenticate automatically. - */ -// await client.getWSClient().connectWSAPI(WS_KEY_MAP.spotTrade); +export function bufferLooksLikeText( + data?: WebSocketBinaryData | null, +): boolean ⋮---- -/** - * These are live trading commands. Keep this false until the params below - * have been reviewed for your account, symbol, and order intent. - */ +export async function decompressMessageEvent( + event: MessageEventLike, + format: WebSocketCompressionFormat = 'gzip', +): Promise> ⋮---- -// -// Example of a custom order ID: -// to make your own, always include the prefix: -// 'client-order-id': `${client.getOrderIdPrefix()}${Date.now()}`, -// -// or to generate a new unique order ID: -// 'client-order-id': client.generateNewOrderID(), -// -// Note: if you do use the client-order-id, it must be prefixed with `client.getOrderIdPrefix()` and unique. `client.generateNewOrderID()` is the recommended way to generate a unique order ID. -// Do not store state in custom order IDs. They are best used as look-up keys for a local state cache. -// For more guidance refer to the best practices on Siebly.io: -// https://siebly.io/reference/glossary#custom-order-id +start(controller) +⋮---- +export type WebSocketCompressionFormat = 'deflate' | 'deflate-raw' | 'gzip'; +⋮---- +function isNodeBuffer(data: unknown): data is Uint8Array +⋮---- +Buffer?: ================ -File: src/WebsocketClient.ts +File: src/lib/BaseWSClient.ts ================ +import WebSocketImplementation from 'isomorphic-ws'; +⋮---- import { - BaseWebsocketClient, - EmittableEvent, - MidflightWsRequestEvent, -} from './lib/BaseWSClient.js'; -import { - getStringOrUndefined, - isRecord, - omitKeys, - removeInternalParamFields, -} from './lib/misc-util.js'; + isMessageEvent, + MessageEventLike, +} from '../types/websockets/ws-events.js'; import { - APIIDMain, - generateNewOrderID, - getOrderIdPrefix, - logInvalidOrderId, - REST_CLIENT_TYPE_ENUM, - serializeParams, - validateWSAPICustomOrderID, - validateWSAPIDerivativesChannelKey, -} from './lib/requestUtils.js'; + WebsocketClientOptions, + WSClientConfigurableOptions, + WsEventInternalSrc, +} from '../types/websockets/ws-general.js'; import { - getSignKeyType, - SignAlgorithm, - SignEncodeMethod, - signMessage as signRawMessage, -} from './lib/webCryptoAPI.js'; -import { DefaultLogger } from './lib/websocket/logger.js'; + EventListener, + WebSocketLike, + WSConnectionOptions, +} from '../types/websockets/ws-portable.js'; +import { WSTopic } from '../types/websockets/ws-subscriptions.js'; +import { EventEmitter } from './event-emitter.js'; +import { isObjectLike } from './misc-util.js'; +import { checkWebCryptoAPISupported } from './webCryptoAPI.js'; +import { DefaultLogger } from './websocket/logger.js'; import { - getPromiseRefForWSAPIRequest, - getPromiseRefPrefixForWSAPIRequest, - getWsUrl as getHtxWsUrl, - HTXDerivativesWSAPIRawRequest, - HTXDerivativesWSRequest, - HTXSpotPrivateWSRequest, - HTXSpotWSAPIRawRequest, - HTXWSAPIRawRequest, - HtxWSNetwork, - HTXWSRequest, - isPrivateWsKey, - PRIVATE_WS_KEYS, - TRADE_WS_KEYS, - WS_KEY_MAP, - WS_KEY_PATH_MAP, - WsKey, + decompressMessageEvent, + isBufferMessageEvent, + safeTerminateWs, WSOperation, WSTopicRequest, -} from './lib/websocket/websocket-util.js'; -import { WSConnectedResult } from './lib/websocket/WsStore.types.js'; -import { - SpotV1OrderAutoPlaceReq, - SpotV1OrderPlaceReq, -} from './types/request/spot.types.js'; -import { OrderIdProperty } from './types/response/shared.types.js'; -import { - Exact, - WS_API_Operations, - WSAPIDerivativesOperation, - WSAPIOperation, - WSAPISpotOperation, - WSAPITopicRequestParamMap, - WSAPITopicResponseMap, - WSAPIWsKey, - WSAPIWsKeyTopicMap, -} from './types/websockets/ws-api.js'; -import { MessageEventLike } from './types/websockets/ws-events.js'; -import { - ParsedWsMessage, - WSClientConfigurableOptions, - WsDerivativesAuthParams, - WsSpotAuthParams, -} from './types/websockets/ws-general.js'; -import { - WebSocketLike, - WebSocketReadyState, -} from './types/websockets/ws-portable.js'; + WSTopicRequestOrStringTopic, +} from './websocket/websocket-util.js'; +import { WsStore } from './websocket/WsStore.js'; import { - isPrivateTopic, - WSTopic, -} from './types/websockets/ws-subscriptions.js'; + DeferredPromise, + WSConnectedResult, + WsConnectionStateEnum, +} from './websocket/WsStore.types.js'; ⋮---- -export interface WSAPIRequestFlags { - /** If true, will skip auth requirement for this WS API request. */ - authIsOptional?: boolean | undefined; - /** Optional caller-supplied correlation id. Generated automatically when omitted. */ - cid?: string; +interface WebSocketConstructor { + new ( + url: string, + protocols?: string | string[], + options?: Omit, + ): WebSocketLike; } ⋮---- -/** If true, will skip auth requirement for this WS API request. */ +function isNativeBrowserWebSocket(ws: WebSocketLike): boolean ⋮---- -/** Optional caller-supplied correlation id. Generated automatically when omitted. */ +type UseTheExceptionEventInstead = never; +type WSClientEventPayload = Record & { + wsKey: WsKey; +}; ⋮---- -interface WsEndpointParts { - url: string; - host: string; - path: string; -} +type WSClientEventPayloadOrArray = + | WSClientEventPayload + | WSClientEventPayload[]; ⋮---- -interface WSAPIInflightRequestRef { - wsKey: WSAPIWsKey; - operation: WSAPIOperation; - cid: string; +interface WSClientEventMap { + /** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */ + open: (evt: { + wsKey: WsKey; + event: unknown; + wsUrl: string; + ws: WebSocketLike; + }) => void; + + /** Reconnecting a dropped connection */ + reconnecting: (evt: { wsKey: WsKey; event: unknown }) => void; + + /** Successfully reconnected a connection that dropped */ + reconnected: (evt: { + wsKey: WsKey; + event: unknown; + wsUrl: string; + ws: WebSocketLike; + }) => void; + + /** Connection closed */ + close: (evt: { wsKey: WsKey; event: unknown }) => void; + + /** Received reply to websocket command (e.g. after subscribing to topics) */ + response: (response: WSClientEventPayloadOrArray) => void; + + /** Received data for topic */ + message: (response: WSClientEventPayloadOrArray) => void; + + /** Exception from ws client OR custom listeners (e.g. if you throw inside your event handler) */ + exception: (response: WSClientEventPayloadOrArray) => void; + + /** + * See for more information: https://github.com/tiagosiebler/bybit-api/issues/413 + * @deprecated Use the 'exception' event instead. The 'error' event had the unintended consequence of throwing an unhandled promise rejection. + */ + error: UseTheExceptionEventInstead; + + /** Confirmation that a connection successfully authenticated */ + authenticated: (event: WSClientEventPayload) => void; } ⋮---- -export class WebsocketClient extends BaseWebsocketClient< +/** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */ ⋮---- -constructor(options?: WSClientConfigurableOptions, logger?: DefaultLogger) +/** Reconnecting a dropped connection */ ⋮---- -// For most connections, heartbeats are messages for HTX, not WS-level frames +/** Successfully reconnected a connection that dropped */ ⋮---- -/** - * Request connection of all market/private sockets that are useful for subscriptions. - * Trade-command sockets are intentionally opened lazily by sendWSAPIRequest(). - */ -public connectAll(): Promise[] +/** Connection closed */ ⋮---- -/** - * Ensures a WS API connection is active and, unless skipped, authenticated. - */ -public connectWSAPI( - wsKey: WSAPIWsKey = WS_KEY_MAP.spotTrade, - skipAuth?: boolean, -): Promise +/** Received reply to websocket command (e.g. after subscribing to topics) */ ⋮---- -public subscribe( - requests: - | (WSTopicRequest | WSTopic) - | (WSTopicRequest | WSTopic)[], - wsKey: WsKey, -) +/** Received data for topic */ ⋮---- -public unsubscribe( - requests: - | (WSTopicRequest | WSTopic) - | (WSTopicRequest | WSTopic)[], - wsKey: WsKey, -) +/** Exception from ws client OR custom listeners (e.g. if you throw inside your event handler) */ ⋮---- /** - * WS API Methods - similar to the REST API, but via WebSockets + * See for more information: https://github.com/tiagosiebler/bybit-api/issues/413 + * @deprecated Use the 'exception' event instead. The 'error' event had the unintended consequence of throwing an unhandled promise rejection. */ ⋮---- -/** - * Send a Websocket API event on a connection. Returns a promise that resolves on reply. - * - * Returned promise is rejected if an exception is detected in the reply OR the connection disconnects for any reason (even if automatic reconnect will happen). - * - * After a fresh connection, you should always send a login request first. - * - * If you authenticated once and you're reconnected later (e.g. connection temporarily lost), the SDK will by default automatically: - * - Detect you were authenticated to the WS API before - * - Try to re-authenticate (up to 5 times, in case something (bad timestamp) goes wrong) - * - If it succeeds, it will emit the 'authenticated' event. - * - If it fails and gives up, it will emit an 'exception' event (type: 'wsapi.auth', reason: detailed text). - * - * You can turn off the automatic re-auth WS API logic using `reauthWSAPIOnReconnect: false` in the WSClient config. - * - * @param wsKey - The connection this event is for. - * @param channel - The channel this event is for (e.g. "spot.login" to authenticate) - * @param params - Any request parameters for the payload (contents of req_param in the docs). Signature generation is automatic, only send parameters such as order ID as per the docs. - * @returns Promise - tries to resolve with async WS API response. Rejects if disconnected or exception is seen in async WS API response - */ -async sendWSAPIRequest< - TWSKey extends keyof WSAPIWsKeyTopicMap, - TWSOperation extends keyof WSAPITopicRequestParamMap & - WSAPIWsKeyTopicMap[TWSKey], - TWSParams extends Exact, - TWSAPIResponse extends WSAPITopicResponseMap[TWSOperation] | object = - WSAPITopicResponseMap[TWSOperation], - >( - wsKey: TWSKey, - operation: TWSOperation, - params?: TWSParams, - requestFlags?: WSAPIRequestFlags, - ): Promise; -⋮---- -async sendWSAPIRequest< - TWSKey extends keyof WSAPIWsKeyTopicMap, - TWSOperation extends keyof WSAPITopicRequestParamMap & - WSAPIWsKeyTopicMap[TWSKey], - TWSParams extends Exact, - TWSAPIResponse extends WSAPITopicResponseMap[TWSOperation] | object = - WSAPITopicResponseMap[TWSOperation], - >( - wsKey: TWSKey, - operation: TWSOperation, - params: TWSParams & { signRequest?: boolean }, - requestFlags?: WSAPIRequestFlags, -): Promise +/** Confirmation that a connection successfully authenticated */ ⋮---- -// Used by other exchanges for commands that don't require authentication. +export interface EmittableEvent< + TEventType extends keyof WSClientEventMap = + keyof WSClientEventMap, +> { + eventType: + | TEventType + | 'pong' + | 'connectionReady' // tied to "requireConnectionReadyConfirmation"; + | 'connectionReadyForAuth'; // tied to specific events we need to wait for, before we can begin post-connect auth + event: Parameters[TEventType]>[0]; + isWSAPIResponse?: boolean; +} ⋮---- -protected isCustomReconnectionNeeded(): boolean +| 'connectionReady' // tied to "requireConnectionReadyConfirmation"; +| 'connectionReadyForAuth'; // tied to specific events we need to wait for, before we can begin post-connect auth ⋮---- -public generateNewOrderID(): string +// Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837 +export interface BaseWebsocketClient< + TWSKey extends string, + TWSRequestEvent extends object, +> { + /** @internal preserves the request-event generic on the merged class type. */ + readonly __requestEventType?: TWSRequestEvent; + + on>( + event: U, + listener: WSClientEventMap[U], + ): this; + + addListener>( + event: U, + listener: WSClientEventMap[U], + ): this; + + once>( + event: U, + listener: WSClientEventMap[U], + ): this; + + prependListener>( + event: U, + listener: WSClientEventMap[U], + ): this; + + prependOnceListener>( + event: U, + listener: WSClientEventMap[U], + ): this; + + off>( + event: U, + listener: WSClientEventMap[U], + ): this; + + removeListener>( + event: U, + listener: WSClientEventMap[U], + ): this; + + removeAllListeners(event?: keyof WSClientEventMap): this; + + listeners(event: keyof WSClientEventMap): EventListener[]; + + listenerCount( + event: keyof WSClientEventMap, + listener?: WSClientEventMap[keyof WSClientEventMap], + ): number; + + emit>( + event: U, + ...args: Parameters[U]> + ): boolean; +} ⋮---- -public getOrderIdPrefix(): string +/** @internal preserves the request-event generic on the merged class type. */ ⋮---- -protected async triggerCustomReconnectionWorkflow(): Promise +on>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -protected async getWsUrl(wsKey: WsKey): Promise +addListener>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -protected sendPingEvent(wsKey: WsKey, ws: WebSocketLike): boolean +once>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -// These WS Keys use native ping frames, since the JSON ping is not supported for these websocket endpoints (confirmed by HTX) +prependListener>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -protected sendPongEvent(wsKey: WsKey, _ws: WebSocketLike, event?: unknown) +prependOnceListener>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -protected isWsPing(event: unknown): boolean +off>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -protected isWsPong(event: unknown): boolean +removeListener>( + event: U, + listener: WSClientEventMap[U], + ): this; ⋮---- -protected resolveEmittableEvents( - wsKey: WsKey, - event: MessageEventLike, -): EmittableEvent[] +removeAllListeners(event?: keyof WSClientEventMap): this; ⋮---- -protected isPrivateTopicRequest( - request: WSTopicRequest, - wsKey: WsKey, -): boolean +listeners(event: keyof WSClientEventMap): EventListener[]; ⋮---- -protected getPrivateWSKeys(): WsKey[] +listenerCount( + event: keyof WSClientEventMap, + listener?: WSClientEventMap[keyof WSClientEventMap], + ): number; ⋮---- -protected authPrivateConnectionsOnConnect(wsKey: WsKey): boolean +emit>( + event: U, + ...args: Parameters[U]> + ): boolean; ⋮---- -protected isAuthOnConnectWsKey(wsKey: WsKey): boolean +/** + * A midflight WS request event (e.g. subscribe to these topics). + * + * - requestKey: unique identifier for this request, if available. Can be anything as a string. + * - requestEvent: the raw request, as an object, that will be sent on the ws connection. This may contain multiple topics/requests in one object, if the exchange supports it. + */ +export interface MidflightWsRequestEvent { + requestKey: string | number; + requestEvent: TEvent; +} ⋮---- -protected getMaxTopicsPerSubscribeEvent(): number | null +/** + * Appends wsKey and isWSAPIResponse to all events. + * Some events are arrays, this handles that nested scenario too. + */ +function getFinalEmittable( + emittable: EmittableEvent, + wsKey: TWSKey, + isWSAPIResponse?: boolean, +): WSClientEventPayload; +function getFinalEmittable( + emittable: EmittableEvent[], + wsKey: TWSKey, + isWSAPIResponse?: boolean, +): WSClientEventPayload[]; +function getFinalEmittable( + emittable: EmittableEvent | EmittableEvent[], + wsKey: TWSKey, + isWSAPIResponse?: boolean, +): WSClientEventPayload | WSClientEventPayload[] ⋮---- -protected async getWsRequestEvents( - wsKey: WsKey, - operation: WSOperation, - requests: WSTopicRequest[], -): Promise[]> +// Some topics just emit an array. +// This is consistent with how it was before the WS API upgrade: ⋮---- -protected async getWsAuthRequestEvent( - wsKey: WsKey, -): Promise +// const { event, ...others } = emittable; +// return { +// ...others, +// event: event.map((subEvent) => +// getFinalEmittable(subEvent, wsKey, isWSAPIResponse), +// ), +// }; ⋮---- -private getWsApiRequestEvent< - TWSKey extends keyof WSAPIWsKeyTopicMap, - TWSOperation extends keyof WSAPITopicRequestParamMap & - WSAPIWsKeyTopicMap[TWSKey], - TWSParams extends Exact, - >( - wsKey: TWSKey, - operation: TWSOperation, - params: TWSParams | undefined, - cid: string, -): HTXWSAPIRawRequest +/** + * Users can conveniently pass topics as strings or objects (object has topic name + optional params). + * + * This method normalises topics into objects (object has topic name + optional params). + */ +function getNormalisedTopicRequests( + wsTopicRequests: WSTopicRequestOrStringTopic[], +): WSTopicRequest[] ⋮---- -private resolveWSAPIResponse( - wsKey: WsKey, - parsed: ParsedWsMessage, -): EmittableEvent[] +// passed as string, convert to object ⋮---- -private async getSpotAuthParams(wsKey: WsKey): Promise +// already a normalised object, thanks to user ⋮---- -private async getDerivativesAuthParams( - wsKey: WsKey, -): Promise +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export abstract class BaseWebsocketClient< +TWSKey extends string, ⋮---- -private async signHtxParams( - host: string, - path: string, - params: Record, -): Promise +constructor( + options?: WSClientConfigurableOptions & { wsLoggerCategory: string }, + logger?: DefaultLogger, +) ⋮---- -private async signMessage( - paramsStr: string, - secret: string, - method: SignEncodeMethod, - algorithm: SignAlgorithm, -): Promise +// Requires a confirmation "response" from the ws connection before assuming it is ready ⋮---- -private getSignatureMethod(): 'HmacSHA256' | 'Ed25519' +// Automatically auth after opening a connection? ⋮---- -private getTimestampForSignature(): string +// Automatically include auth/sign/token with every WS request. +// Automatically handled during getWsRequestEvents. ⋮---- -private async getWsEndpointParts(wsKey: WsKey): Promise +// Automatically re-auth WS API, if we were auth'd before and get reconnected ⋮---- -private getWsNetwork(): HtxWSNetwork +// Whether to use native heartbeats (depends on the exchange) ⋮---- -private isMarketProtocolWsKey(wsKey: WsKey): boolean +// Check Web Crypto API support when credentials are provided and no custom sign function is used ⋮---- -private isSpotPrivateProtocolWsKey(wsKey: WsKey): boolean +/** + * Return true if this wsKey connection should automatically authenticate immediately after connecting + */ +protected abstract isAuthOnConnectWsKey(wsKey: TWSKey): boolean; ⋮---- -private isDerivativesTopicProtocolWsKey(wsKey: WsKey): boolean +protected abstract isCustomReconnectionNeeded(wsKey: TWSKey): boolean; ⋮---- -private isDerivativesOperationProtocolWsKey(wsKey: WsKey): boolean +protected abstract triggerCustomReconnectionWorkflow( + wsKey: TWSKey, + ): Promise; ⋮---- -private parseWsMessage(event: unknown): ParsedWsMessage | undefined +protected abstract sendPingEvent(wsKey: TWSKey, ws: WebSocketLike): boolean; ⋮---- -private getHeartbeatTimestamp(parsed?: ParsedWsMessage): string | number +protected abstract sendPongEvent( + wsKey: TWSKey, + ws: WebSocketLike, + event?: unknown, + ): void; ⋮---- -private getPayloadRecord(payload: unknown): Record +protected abstract isWsPing(data: unknown): boolean; ⋮---- -private getPayloadStringOrNumber( - payload: Record, - key: string, -): string | number | undefined +protected abstract isWsPong(data: unknown): boolean; ⋮---- -private isAuthResponse(parsed: ParsedWsMessage): boolean +protected abstract authPrivateConnectionsOnConnect(_wsKey: TWSKey): boolean; ⋮---- -private isWSAPIResponse(wsKey: WsKey, parsed: ParsedWsMessage): boolean +/** + * Return the event sent to the server to trigger authentication. If the event requires waiting for another event first (e.g. a challenge), return 'waitForEvent' and the library will wait for that event before calling this method again. + * + * Usually only called once per connection, unless the connection drops/is reset. + */ +protected abstract getWsAuthRequestEvent( + wsKey: TWSKey, + eventToAuth?: object, + ): Promise; ⋮---- -private isSubscriptionResponse(parsed: ParsedWsMessage): boolean +protected abstract isPrivateTopicRequest( + request: WSTopicRequest, + wsKey: TWSKey, + ): boolean; ⋮---- -private isRequestResponse(parsed: ParsedWsMessage): boolean +protected abstract getPrivateWSKeys(): TWSKey[]; ⋮---- -private isErrorEvent(parsed: ParsedWsMessage): boolean +protected abstract getWsUrl(wsKey: TWSKey): Promise; ⋮---- -private getEventOperation(parsed: ParsedWsMessage): string | undefined +protected abstract getMaxTopicsPerSubscribeEvent( + wsKey: TWSKey, + ): number | null; ⋮---- -private trackWSAPIRequestRef( - promiseRef: string, - wsKey: WSAPIWsKey, - operation: WSAPIOperation, - cid: string, -): void +/** + * @returns one or more correctly structured request events for performing a operations over WS. This can vary per exchange spec. + */ +protected abstract getWsRequestEvents( + wsKey: TWSKey, + operation: WSOperation, + requests: WSTopicRequest[], + ): Promise[]>; ⋮---- -private getPromiseRefForWSAPIResponse( - wsKey: WsKey, - parsed: ParsedWsMessage, -): string | undefined +/** + * Abstraction called to sort ws events into emittable event types (response to a request, data update, etc) + */ +protected abstract resolveEmittableEvents( + wsKey: TWSKey, + event: MessageEventLike, + ): EmittableEvent[]; +⋮---- +/** + * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library + */ +protected abstract connectAll(): Promise[]; +⋮---- +protected isPrivateWsKey(wsKey: TWSKey): boolean +⋮---- +/** Returns auto-incrementing request ID, used to track promise references for async requests */ +protected getNewRequestId(): number +⋮---- +protected abstract sendWSAPIRequest( + wsKey: TWSKey, + operation: string, + params?: unknown, + ): Promise; +⋮---- +protected abstract sendWSAPIRequest( + wsKey: TWSKey, + channel: string, + params: unknown, + ): Promise; +⋮---- +public getTimeOffsetMs() +⋮---- +public setTimeOffsetMs(newOffset: number) ⋮---- /** + * Don't call directly! Use subscribe() instead! * - * Misc Utility Methods + * Subscribe to one or more topics on a WS connection (identified by WS Key). * + * - Topics are automatically cached + * - Connections are automatically opened, if not yet connected + * - Authentication is automatically handled + * - Topics are automatically resubscribed to, if something happens to the connection, unless you call unsubsribeTopicsForWsKey(topics, key). + * + * @param wsTopicRequests array of topics to subscribe to + * @param wsKey ws key referring to the ws connection these topics should be subscribed on */ +protected async subscribeTopicsForWsKey( + wsTopicRequests: WSTopicRequestOrStringTopic[], + wsKey: TWSKey, +) +⋮---- +// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically +⋮---- +// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect +⋮---- +// Subscribe should happen automatically once connected, nothing to do here after topics are added to wsStore. ⋮---- /** - * Validate syntax meets requirements set by HTX. - * Log warning if not. + * Are we in the process of connection? Nothing to send yet. + */ +⋮---- +// We're connected. Check if auth is needed and if already authenticated +⋮---- +/** + * If not authenticated yet and auth is required, don't request topics yet. + * + * Auth should already automatically be in progress, so no action needed from here. Topics will automatically subscribe post-auth success. + */ +⋮---- +// Finally, request subscription to topics if the connection is healthy and ready +⋮---- +protected async unsubscribeTopicsForWsKey( + wsTopicRequests: WSTopicRequestOrStringTopic[], + wsKey: TWSKey, +): Promise +⋮---- +// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically +⋮---- +// If not connected, don't need to do anything. +// Removing the topic from the store is enough to stop it from being resubscribed to on reconnect. +⋮---- +// We're connected. Check if auth is needed and if already authenticated +⋮---- +/** + * If not authenticated yet and auth is required, don't need to do anything. + * We don't subscribe to topics until auth is complete anyway. + */ +⋮---- +// Finally, request subscription to topics if the connection is healthy and ready +⋮---- +/** + * Splits topic requests into two groups, public & private topic requests */ -public validateOrderId( - params: SpotV1OrderPlaceReq | SpotV1OrderAutoPlaceReq, - orderIdProperty: OrderIdProperty, -): void - -================ -File: README.md -================ -# Node.js & JavaScript SDK for HTX REST APIs & WebSockets - -[![Build & Test](https://github.com/sieblyio/htx-api/actions/workflows/e2etest.yml/badge.svg?branch=main)](https://github.com/sieblyio/htx-api/actions/workflows/e2etest.yml) -[![npm version](https://img.shields.io/npm/v/%40siebly%2Fhtx-api)][1] -[![npm size](https://img.shields.io/bundlephobia/min/%40siebly%2Fhtx-api/latest)][1] -[![npm downloads](https://img.shields.io/npm/dt/%40siebly%2Fhtx-api)][1] -[![last commit](https://img.shields.io/github/last-commit/sieblyio/htx-api)][1] -[![Telegram](https://img.shields.io/badge/chat-on%20telegram-blue.svg)](https://t.me/nodetraders) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/sieblyio/htx-api) - -

- - - - SDK Logo - - -

- -[1]: https://www.npmjs.com/package/@siebly/htx-api - -Complete & robust JavaScript & Node.js SDK for the HTX REST APIs and WebSockets: - -- Professional, robust & performant HTX SDK with extensive production use in live trading environments. -- Complete integration with HTX REST APIs and WebSockets. - - Dedicated REST clients for Spot and Derivatives (linear swap, coin swap, delivery futures) - - Unified WebSocket client for all markets -- Complete TypeScript support (with type declarations for most API requests & responses). - - Strongly typed requests and responses. - - Automated end-to-end tests ensuring reliability. -- Actively maintained with a modern, promise-driven interface. -- Robust WebSocket integration with configurable connection heartbeats & automatic reconnect then resubscribe workflows. - - Event driven messaging. - - Smart WebSocket persistence with automatic reconnection handling. - - Emit `reconnected` event when dropped connection is restored. - - Support for both public and private WebSocket streams. -- Portable HMAC signing support for trusted server-side runtimes. -- Automatically supports both ESM and CJS projects. -- Heavy automated end-to-end testing with real API calls. -- Proxy support via axios integration. -- Active community support & collaboration in telegram: [Node.js Algo Traders](https://t.me/nodetraders). - -## Table of Contents - -- [Installation](#installation) -- [Examples](#examples) -- [Issues & Discussion](#issues--discussion) -- [Related Projects](#related-projects) -- [Documentation](#documentation) -- [Structure](#structure) -- [Usage](#usage) - - [REST API Clients](#rest-api) - - [Spot Trading](#spot-trading) - - [Derivatives (Futures) Trading](#derivatives-futures-trading) - - [WebSockets](#websockets) - - [Public WebSocket Streams](#public-websocket-streams) - - [Private WebSocket Streams](#private-websocket-streams) - - [WebSocket API (WebsocketAPIClient)](#websocket-api-websocketapiclient) -- [Customise Logging](#customise-logging) -- [Browser/Frontend Usage](#browserfrontend-usage) - - [React and Vite](#react-and-vite) - - [Webpack](#webpack) - - [Browser Requirements](#browser-requirements) - - [Security and CORS](#security-and-cors) -- [LLMs & AI](#use-with-llms--ai) -- [Used By](#used-by) -- [Contributions & Thanks](#contributions--thanks) - -## Installation - -`npm install --save @siebly/htx-api` - -Node.js usage requires Node 22.13.0 or newer. Browser applications should follow the public-data and credential-safety guidance below. - -## Examples - -Refer to the [examples](./examples) folder for implementation demos, including: - -- **Spot WebSocket Examples**: public and private market/account streams -- **Derivatives WebSocket Examples**: linear swap, coin swap, delivery futures, and index streams - -## Issues & Discussion - -- Issues? Check the [issues tab](https://github.com/sieblyio/htx-api/issues). -- Discuss & collaborate with other node devs? Join our [Node.js Algo Traders](https://t.me/nodetraders) engineering community on telegram. -- Follow our announcement channel for real-time updates on [X/Twitter](https://x.com/sieblyio) - - - -## Related Projects - -Check out our JavaScript/TypeScript/Node.js SDKs & Projects: +private sortTopicRequestsIntoPublicPrivate( + wsTopicRequests: WSTopicRequest[], + wsKey: TWSKey, +): +⋮---- +/** Get the WsStore that tracks websockets & topics */ +public getWsStore(): WsStore> +⋮---- +public close(wsKey: TWSKey, force?: boolean) +⋮---- +public closeAll(force?: boolean) +⋮---- +public isConnected(wsKey: TWSKey): boolean +⋮---- +/** + * Request connection to a specific websocket, instead of waiting for automatic connection. + */ +public async connect( + wsKey: TWSKey, + customUrl?: string | undefined, + throwOnError?: boolean, +): Promise +⋮---- +// Important: don't check for RECONNECTING here, or this clashes with reconnectWithDelay()! +⋮---- +private connectToWsUrl(url: string, wsKey: TWSKey): WebSocketLike +⋮---- +// Event handlers for native heartbeats / ping/pong frames +⋮---- +private parseWsError( + context: string, + error: unknown, + wsKey: TWSKey, +): boolean +⋮---- +// Allow retry by default (in some places that call this). Prevent deadloop in hard failure (401) +⋮---- +/** Get a signature, build the auth request and send it */ +private async sendAuthRequest( + wsKey: TWSKey, + eventToAuth?: object, +): Promise +⋮---- +private async advanceAuthRequest( + wsKey: TWSKey, + authPromise: DeferredPromise, + eventToAuth?: object, +): Promise +⋮---- +// If not required, this won't return anything +⋮---- +// Short-circuit this for the next time it's called +⋮---- +private reconnectWithDelay(wsKey: TWSKey, connectionDelayMs: number) +⋮---- +// Some streams need a specialist reconnection workflow. +// E.g. the user data stream can't just be reconnected as is. +⋮---- +private ping(wsKey: TWSKey) +⋮---- +/** + * Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously. + * If closed, trigger a reconnect immediately + */ +protected executeReconnectableClose(wsKey: TWSKey, reason: string) +⋮---- +private clearTimers(wsKey: TWSKey) +⋮---- +// Send a ping at intervals +private clearPingTimer(wsKey: TWSKey) +⋮---- +// Expect a pong within a time limit +private clearPongTimer(wsKey: TWSKey) +⋮---- +// this.logger.trace(`Cleared pong timeout for "${wsKey}"`); +⋮---- +// this.logger.trace(`No active pong timer for "${wsKey}"`); +⋮---- +private clearReconnectTimer(wsKey: TWSKey) +⋮---- +/** + * Returns a list of string events that can be individually sent upstream to complete subscribing/unsubscribing/etc to these topics + * + * If events are an object, these should be stringified (`return JSON.stringify(event);`) + * Each event returned by this will be sent one at a time + * + * Events are automatically split into smaller batches, by this method, if needed. + */ +protected async getWsOperationEventsForTopics( + topics: WSTopicRequest[], + wsKey: TWSKey, + operation: WSOperation, +): Promise[]> +⋮---- +// Events that are ready to send (usually stringified JSON) +⋮---- +/** + * Simply builds and sends subscribe events for a list of topics for a ws key + * + * @private Use the `subscribe(topics)` or `subscribeTopicsForWsKey(topics, wsKey)` method to subscribe to topics. Send WS message to subscribe to topics. + */ +private async requestSubscribeTopics( + wsKey: TWSKey, + wsTopicRequests: WSTopicRequest[], +) +⋮---- +// Automatically splits requests into smaller batches, if needed +⋮---- +`Subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`, // Events: "${JSON.stringify(topics)}" +⋮---- +/** + * Simply builds and sends unsubscribe events for a list of topics for a ws key + * + * @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics. + */ +private async requestUnsubscribeTopics( + wsKey: TWSKey, + wsTopicRequests: WSTopicRequest[], +) +⋮---- +/** + * Try sending a string event on a WS connection (identified by the WS Key) + */ +private tryWsSendWithResult( + wsKey: TWSKey, + wsMessage: string, + throwExceptions?: boolean, +): boolean +⋮---- +public tryWsSend( + wsKey: TWSKey, + wsMessage: string, + throwExceptions?: boolean, +): void +⋮---- +/** Send a message while reporting whether it reached the socket API. */ +protected tryWsSendWithStatus( + wsKey: TWSKey, + wsMessage: string, + throwExceptions?: boolean, +): boolean +⋮---- +private async onWsOpen( + event: unknown, + wsKey: TWSKey, + url: string, + ws: WebSocketLike, +) +⋮---- +private resolveConnectionInProgressPromise(wsKey: TWSKey) +⋮---- +// Resolve & cleanup deferred "connection attempt in progress" promise +⋮---- +/** + * Called automatically once a connection is ready. + * - Some exchanges are ready immediately after the connections open. + * - Some exchanges send an event to confirm the connection is ready for us. + * + * This method is called to act when the connection is ready. Use `requireConnectionReadyConfirmation` to control how this is called. + */ +private async onWsReadyForEvents(wsKey: TWSKey) +⋮---- +// Some websockets require an auth packet to be sent after opening the connection +⋮---- +// Reconnect to topics known before it connected +⋮---- +// Request sub to public topics, if any +⋮---- +// Request sub to private topics, if auth on connect isn't needed +⋮---- +/** + * Handle subscription to private topics _after_ authentication successfully completes asynchronously. + * + * Only used for exchanges that require auth before sending private topic subscription requests + */ +private onWsAuthenticated(wsKey: TWSKey, event: unknown) +⋮---- +// Resolve & cleanup deferred "auth attempt in progress" promise +⋮---- +// Remove before continuing, in case there's more requests queued +⋮---- +private onWsPing( + event: unknown, + wsKey: TWSKey, + ws: WebSocketLike, + source: WsEventInternalSrc, +) +⋮---- +private onWsPong(event: unknown, wsKey: TWSKey, source: WsEventInternalSrc) +⋮---- +// Necessary when native heartbeats are used +⋮---- +private async onWsMessage( + event: unknown, + wsKey: TWSKey, + ws: WebSocketLike, + didDecompress = false, +): Promise +⋮---- +// console.log('onMessageRaw: ', (event as any).data); +// any message can clear the pong timer - wouldn't get a message if the ws wasn't working +⋮---- +// console.log(`raw event: `, { data, dataType, emittableEvents }); +⋮---- +// Other event types are automatically emitted here +⋮---- +// this.logger.trace( +// `onWsMessage().emit(${emittable.eventType}).done()`, +// emittableFinalEvent, +// ); +⋮---- +// this.logger.trace('Decompressed message event from buffer', { +// ...this.WS_LOGGER_CATEGORY, +// wsKey, +// decompressed, +// }); +⋮---- +private onWsClose(event: unknown, wsKey: TWSKey) +⋮---- +// unintentional close, attempt recovery +⋮---- +// clean up any pending promises for this connection +⋮---- +// this.clearTopicsPendingSubscriptions(wsKey, true, 'WS Closed'); +⋮---- +// intentional close - clean up +// clean up any pending promises for this connection +⋮---- +// This was an intentional close, delete all state for this connection, as if it never existed: +⋮---- +private getWs(wsKey: TWSKey) +⋮---- +private setWsState(wsKey: TWSKey, state: WsConnectionStateEnum) +⋮---- +/** + * Promise-driven method to assert that a ws has successfully connected (will await until connection is open) + */ +protected async assertIsConnected(wsKey: TWSKey): Promise +⋮---- +// Already in progress? Await shared promise and retry +⋮---- +// Start connection, it should automatically store/return a promise. +⋮---- +/** + * Promise-driven method to assert that a ws has been successfully authenticated (will await until auth is confirmed) + */ +public async assertIsAuthenticated(wsKey: TWSKey): Promise +⋮---- +// Already in progress? Await shared promise and retry +⋮---- +// Start authentication, it should automatically store/return a promise. -- Visit our website: [https://Siebly.io](https://siebly.io/?ref=gh) -- Try our REST API & WebSocket SDKs published on npmjs: - - [Bybit Node.js SDK: bybit-api](https://www.npmjs.com/package/bybit-api) - - [Kraken Node.js SDK: @siebly/kraken-api](https://www.npmjs.com/package/@siebly/kraken-api) - - [OKX Node.js SDK: okx-api](https://www.npmjs.com/package/okx-api) - - [Binance Node.js SDK: binance](https://www.npmjs.com/package/binance) - - [Gate (gate.com) Node.js SDK: gateio-api](https://www.npmjs.com/package/gateio-api) - - [Bitget Node.js SDK: bitget-api](https://www.npmjs.com/package/bitget-api) - - [Kucoin Node.js SDK: kucoin-api](https://www.npmjs.com/package/kucoin-api) - - [Coinbase Node.js SDK: coinbase-api](https://www.npmjs.com/package/coinbase-api) - - [HTX Node.js SDK: @siebly/htx-api](https://www.npmjs.com/package/@siebly/htx-api) -- Try my misc utilities: - - [OrderBooks Node.js: orderbooks](https://www.npmjs.com/package/orderbooks) - - [Crypto Exchange Account State Cache: accountstate](https://www.npmjs.com/package/accountstate) -- Check out my examples: - - [awesome-crypto-examples Node.js](https://github.com/tiagosiebler/awesome-crypto-examples) - - -## Documentation - -Most methods accept JS objects. These can be populated using parameters specified by HTX's API documentation, or check the type definition in each class within this repository. - -### API Documentation Links - -- [HTX API Documentation](https://www.htx.com/en-us/opend/newApiPages/) - - [Spot Trading API](https://www.htx.com/en-us/opend/newApiPages/) - - [Derivatives Trading API](https://www.htx.com/en-us/opend/newApiPages/) -- [REST Endpoint Function List](./docs/endpointFunctionList.md) - -## Structure - -This project uses typescript. Resources are stored in 2 key structures: - -- [src](./src) - the whole connector written in typescript -- [examples](./examples) - some implementation examples & demonstrations. Contributions are welcome! - ---- - -# Usage - -Create API credentials on HTX's website: - -- [HTX API Key Management](https://www.htx.com/apikey/) - -## REST API - -The SDK provides dedicated REST clients for different trading products: - -- **SpotClient** - for spot trading, margin, earn, sub-accounts, and account operations -- **FuturesClient** - for linear swap, coin swap, delivery futures, and copy trading operations - -Both clients default to HTX's AWS CDN domains for better connectivity. You can override this via `baseUrlKey` in the client options if needed. - -### Spot Trading - -To use HTX's Spot APIs, import (or require) the `SpotClient`: - -```javascript -import { SpotClient } from '@siebly/htx-api'; -// or if you prefer require: -// const { SpotClient } = require('@siebly/htx-api'); - -// For public endpoints, API credentials are optional -const publicClient = new SpotClient(); - -// For private endpoints, provide API credentials -const client = new SpotClient({ - apiKey: 'your-api-key', - apiSecret: 'your-api-secret', -}); - -// Public API Examples - -// Get ticker information -const ticker = await publicClient.getTicker({ - symbol: 'btcusdt', -}); -console.log('Ticker: ', ticker); - -// Get order book -const orderBook = await publicClient.getMarketDepth({ - symbol: 'btcusdt', - depth: 10, -}); -console.log('Order Book: ', orderBook); - -// Private API Examples (requires authentication) - -// Get accounts and balances -client - .getAccounts() - .then(async (accounts) => { - console.log('Accounts: ', accounts); - - const accountId = accounts.data?.[0]?.id; - if (accountId) { - const balance = await client.getAccountBalance({ accountId }); - console.log('Account Balance: ', balance); - } - }) - .catch((err) => { - console.error('Error: ', err); - }); - -// Submit a limit order -client - .submitOrder({ - 'account-id': 'your-account-id', - symbol: 'btcusdt', - type: 'buy-limit', - amount: '0.0001', - price: '10000', - 'client-order-id': client.generateNewOrderID(), - }) - .then((result) => { - console.log('Limit Order Result: ', result); - }) - .catch((err) => { - console.error('Error: ', err); - }); - -// Submit batch of orders (max 10 per batch) -client - .submitBatchOrders([ - { - 'account-id': 'your-account-id', - symbol: 'btcusdt', - type: 'buy-limit', - amount: '0.0001', - price: '10000.00', - 'client-order-id': client.generateNewOrderID(), - }, - { - 'account-id': 'your-account-id', - symbol: 'btcusdt', - type: 'sell-limit', - amount: '0.0001', - price: '13000.00', - 'client-order-id': client.generateNewOrderID(), - }, - ]) - .then((result) => { - console.log('Batch Order Result: ', JSON.stringify(result, null, 2)); - }) - .catch((err) => { - console.error('Error: ', err); - }); -``` - -See [SpotClient](./src/SpotClient.ts) for further information. - -### Derivatives (Futures) Trading - -Use the `FuturesClient` for futures and swap trading operations: - -```javascript -import { FuturesClient } from '@siebly/htx-api'; -// or if you prefer require: -// const { FuturesClient } = require('@siebly/htx-api'); - -// For public endpoints, API credentials are optional -const publicClient = new FuturesClient(); - -// For private endpoints, provide API credentials -const client = new FuturesClient({ - apiKey: 'your-api-key', - apiSecret: 'your-api-secret', -}); - -// Public API Examples - -// Get order book for a specific contract -const orderBook = await publicClient.getLinearSwapMarketDepth({ - contract_code: 'BTC-USDT', - type: 'step0', -}); -console.log('Futures Order Book: ', orderBook); - -// Get ticker information -const ticker = await publicClient.getLinearSwapTicker({ - contract_code: 'BTC-USDT', -}); -console.log('Futures Ticker: ', ticker); - -// Private API Examples (requires authentication) - -// Get cross margin account info -client - .getLinearSwapCrossAccountInfo() - .then((accounts) => { - console.log('Cross Account Info: ', accounts); - }) - .catch((err) => { - console.error('Error: ', err); - }); - -// Submit a cross margin limit order -client - .submitLinearSwapCrossOrder({ - contract_code: 'BTC-USDT', - direction: 'buy', - volume: 1, - lever_rate: 5, - order_price_type: 'limit', - price: 10000, - }) - .then((result) => { - console.log('Limit Order Result: ', JSON.stringify(result, null, 2)); - }) - .catch((err) => { - console.error('Error: ', err); - }); -``` +================ +File: src/types/response/shared.types.ts +================ +import { RestClientOptions } from '../../lib/requestUtils.js'; +⋮---- +export interface GenericAPIError { + code: number; + message: string; + body: TBody; + headers: Record; + requestOptions: RestClientOptions; + requestParams: Record; +} +⋮---- +// HTX TYPES +⋮---- +/** HTX spot API success response. TData is the payload. TPayloadKey defaults to "data" but can be overridden (e.g. "tick"). */ +export type SpotAPISuccessResponse< + TData, + TPayloadKey extends string = 'data', +> = { + status: string; + success?: boolean; + ok?: boolean; + code?: string; + ch?: string; + ts?: number; +} & Record; +⋮---- +/** HTX spot API error response. */ +export interface SpotAPIErrorResponse { + message?: string; + 'err-code'?: string; + 'err-msg'?: string; +} +⋮---- +/** HTX linear-swap (futures) API success response. TPayloadKey: data (default), tick, ticks. */ +export type FuturesAPISuccessResponse< + TData, + TPayloadKey extends string = 'data', +> = { + code?: number; + msg?: string; + status?: string; + ch?: string; + ts?: number; +} & Record; +⋮---- +export type SpotAPIResponse = + | SpotAPISuccessResponse + | SpotAPIErrorResponse; +⋮---- +export type OrderIdProperty = 'client-order-id'; +export type SpotOrderIdProperty = OrderIdProperty | 'clientOrderId'; -See [FuturesClient](./src/FuturesClient.ts) for further information. +================ +File: src/index.ts +================ -## WebSockets - -HTX supports two types of WebSocket connections: - -1. **WebSocket Subscriptions** - Real-time market data and account updates via the `WebsocketClient` -2. **WebSocket API** - REST-like request/response trading via `WebsocketAPIClient` or `sendWSAPIRequest()` - -### WebSocket Subscriptions (WebsocketClient) - -The unified `WebsocketClient` handles all HTX WebSocket streams with automatic connection management and reconnection. - -Key WebSocket features: - -- Event driven messaging -- Smart WebSocket persistence with automatic reconnection -- Heartbeat mechanisms to detect disconnections -- Automatic resubscription after reconnection -- Support for Spot and Derivatives markets (linear swap, coin swap, delivery) -- Support for both public and private WebSocket streams - -Each connection is tracked using a `WsKey` (see [WS_KEY_MAP](./src/lib/websocket/websocket-util.ts)). Pass the appropriate `WsKey` when subscribing so the client routes your request to the correct endpoint. - -### Public WebSocket Streams - -For public market data, API credentials are not required: - -```javascript -import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; -// or if you prefer require: -// const { WebsocketClient, WS_KEY_MAP } = require('@siebly/htx-api'); - -// Create WebSocket client for public streams -const wsClient = new WebsocketClient(); - -// Set up event handlers -wsClient.on('open', (data) => { - console.log('WebSocket connected: ', data?.wsKey); -}); - -wsClient.on('message', (data) => { - console.log('Data received: ', JSON.stringify(data, null, 2)); -}); - -wsClient.on('reconnected', (data) => { - console.log('WebSocket reconnected: ', data); -}); - -wsClient.on('exception', (data) => { - console.error('WebSocket error: ', data); -}); - -// Spot - Subscribe to public data streams -wsClient.subscribe( - [ - 'market.btcusdt.kline.1min', - 'market.btcusdt.ticker', - 'market.btcusdt.depth.step0', - 'market.btcusdt.trade.detail', - ], - WS_KEY_MAP.spotPublic, -); - -// Spot feed - high-frequency topics such as BBO and MBP -wsClient.subscribe( - ['market.btcusdt.mbp.5', 'market.btcusdt.trade.detail'], - WS_KEY_MAP.spotFeed, -); - -// Linear swap (USDT-margined) public streams -wsClient.subscribe( - [ - 'market.BTC-USDT.kline.1min', - 'market.BTC-USDT.detail', - 'market.BTC-USDT.trade.detail', - 'market.BTC-USDT.bbo', - ], - WS_KEY_MAP.linearSwapPublic, -); - -// Derivatives index/mark price streams -wsClient.subscribe( - ['market.BTC-USDT.index.1min', 'market.BTC-USDT.mark_price.1min'], - WS_KEY_MAP.derivativesIndex, -); -``` - -### Private WebSocket Streams - -For private account data streams, API credentials are required: - -```javascript -import { WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; - -// Create WebSocket client with API credentials for private streams -const wsClient = new WebsocketClient({ - apiKey: 'your-api-key', - apiSecret: 'your-api-secret', -}); - -// Set up event handlers -wsClient.on('open', (data) => { - console.log('Private WebSocket connected: ', data?.wsKey); -}); - -wsClient.on('message', (data) => { - console.log('Private data received: ', JSON.stringify(data, null, 2)); -}); - -wsClient.on('authenticated', (data) => { - console.log('WebSocket authenticated: ', data); -}); - -wsClient.on('response', (data) => { - console.log('WebSocket response: ', data); -}); - -wsClient.on('exception', (data) => { - console.error('WebSocket error: ', data); -}); - -// Spot - Subscribe to private data streams -// Note: SDK automatically handles authentication before subscribing -wsClient.subscribe( - ['accounts.update#1', 'orders#btcusdt', 'trade.clearing#btcusdt#0'], - WS_KEY_MAP.spotPrivateV2, -); - -// Linear swap private streams -wsClient.subscribe( - ['orders.BTC-USDT', 'accounts.BTC-USDT', 'positions.BTC-USDT'], - WS_KEY_MAP.linearSwapPrivate, -); -``` - -For more comprehensive examples, including custom logging and error handling, check the [examples](./examples/Spot/WebSockets) and [examples](./examples/Derivatives/WebSockets) folders. - -### WebSocket API (WebsocketAPIClient) - -Use `WebsocketAPIClient` for typed REST-like trading methods over a persisted WebSocket connection, or call `WebsocketClient.sendWSAPIRequest()` directly for lower-level control. - -Trade connections connect and authenticate lazily on the first request. Optional pre-connect: `client.getWSClient().connectWSAPI(WS_KEY_MAP.spotTrade)`. - -Trade keys: `spotTrade`, `linearSwapTrade`, `coinDeliveryTrade`, `coinSwapTrade`. - -```javascript -import { WebsocketAPIClient, WS_KEY_MAP } from '@siebly/htx-api'; - -const client = new WebsocketAPIClient({ - apiKey: 'your-api-key', - apiSecret: 'your-api-secret', -}); - -// Spot -const order = await client.submitSpotOrder({ - 'account-id': 123456, - symbol: 'btcusdt', - type: 'buy-limit', - amount: '0.001', - price: '20000', - source: 'spot-api', -}); - -// Derivatives -const linearOrder = await client.placeLinearSwapOrder({ - contract_code: 'BTC-USDT', - margin_mode: 'cross', - position_side: 'long', - side: 'buy', - type: 'limit', - time_in_force: 'gtc', - price: '20000', - volume: '1', - reduce_only: 0, -}); -``` - -See [WebsocketAPIClient](./src/WebsocketAPIClient.ts) for all typed methods. Examples: [Spot](./examples/Spot/WebSockets/wsAPI.ts), [Derivatives](./examples/Derivatives/WebSockets/wsAPI.ts), [raw spot](./examples/Spot/WebSockets/wsAPI.RAW.ts), [raw derivatives](./examples/Derivatives/WebSockets/wsAPI.RAW.ts). - ---- - -## Customise Logging - -Pass a custom logger which supports the log methods `trace`, `info` and `error`, or override methods from the default logger as desired. - -```javascript -import { WebsocketClient, DefaultLogger } from '@siebly/htx-api'; - -// E.g. customise logging for only the trace level: -const customLogger = { - ...DefaultLogger, - trace: (...params) => { - // console.log('trace', ...params); - }, - info: (...params) => { - console.log('info', ...params); - }, - error: (...params) => { - console.error('error', ...params); - }, -}; - -const ws = new WebsocketClient( - { - apiKey: 'apiKeyHere', - apiSecret: 'apiSecretHere', - }, - customLogger, -); -``` - -In rare situations, you may want to see the raw HTTP requests being built as well as the API response. These can be enabled by setting the `HTXTRACE` env var to `true`. - -## Browser/Frontend Usage - -The package's ESM entry can be imported directly by modern frontend bundlers. Do not build or copy a separate SDK bundle into your application. - -Browser applications should use the SDK only for public market data. Keep API keys, API secrets, authenticated REST calls, private WebSocket subscriptions, and WebSocket API trading on a trusted backend. - -### React and Vite - -Install and import the package normally; no SDK-specific Vite plugin or Node.js polyfill is required: - -```bash -npm install @siebly/htx-api -``` - -```tsx -import { useEffect, useState } from 'react'; -import { SpotClient, WebsocketClient, WS_KEY_MAP } from '@siebly/htx-api'; - -export function BtcTicker() { - const [ticker, setTicker] = useState(); - - useEffect(() => { - const restClient = new SpotClient(); - const wsClient = new WebsocketClient(); - - restClient.getTicker({ symbol: 'btcusdt' }).then(setTicker); - - const onMessage = (message: unknown) => setTicker(message); - wsClient.on('message', onMessage); - wsClient.subscribe('market.btcusdt.ticker', WS_KEY_MAP.spotPublic); - - return () => { - wsClient.off('message', onMessage); - wsClient.closeAll(); - }; - }, []); - - return
{JSON.stringify(ticker, null, 2)}
; -} -``` - -The cleanup is important during navigation, hot reloads, and React Strict Mode development checks so that an old socket is not left reconnecting in the background. - -### Webpack - -Webpack 5 can consume the same package entry directly: - -```javascript -import { SpotClient, WebsocketClient } from '@siebly/htx-api'; -``` - -Use a normal `target: 'web'` application build. The SDK does not require a checked-in UMD bundle or `resolve.fallback` shims for Node.js core modules. - -Direct `