diff --git a/README.md b/README.md index 2f06834..69eee51 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Legend: ✅ implemented — ⬜ not yet implemented. | General | `subAccounts2` | ⬜ | | | General | `twapHistory` | ✅ | `RestApi::twapHistory` | | General | `usdcRouting` | ⬜ | | -| General | `userBorrowLendInterest` | ⬜ | | +| General | `userBorrowLendInterest` | ✅ | `RestApi::userBorrowLendInterest` | | General | `userToMultiSigSigners` | ⬜ | | | General | `userTwapSliceFillsByTime` | ✅ | `RestApi::userTwapSliceFillsByTime` | | General | `validatorL1Votes` | ⬜ | | @@ -146,7 +146,7 @@ Legend: ✅ implemented — ⬜ not yet implemented. | Perpetuals | `perpAnnotation` | ✅ | `RestApi::perpAnnotation` | | Perpetuals | `perpCategories` | ✅ | `RestApi::perpCategories` | | Perpetuals | `perpConciseAnnotations` | ✅ | `RestApi::perpConciseAnnotations` | -| Perpetuals | `liquidatable` | ⬜ | | +| Perpetuals | `liquidatable` | ✅ | `RestApi::liquidatable` | | Perpetuals | `marginTable` | ⬜ | | | Perpetuals | `maxMarketOrderNtls` | ⬜ | | | Perpetuals | `recentTrades` | ✅ | `RestApi::recentTrades` | @@ -161,7 +161,7 @@ Legend: ✅ implemented — ⬜ not yet implemented. | Spot / Outcomes | `outcomeDeployerLimits` | ⬜ | | | Spot / Outcomes | `outcomeTemplates` | ⬜ | | -56 of 78 documented info endpoints implemented. One (`tokenDetails`) has a `RestEndpointType` enum value reserved but no request builder or method yet. +58 of 78 documented info endpoints implemented. One (`tokenDetails`) has a `RestEndpointType` enum value reserved but no request builder or method yet. ### Exchange actions (`/exchange`) diff --git a/examples/rest_borrow_lend.cpp b/examples/rest_borrow_lend.cpp index d03009e..5f0d055 100644 --- a/examples/rest_borrow_lend.cpp +++ b/examples/rest_borrow_lend.cpp @@ -4,6 +4,8 @@ #include #include +#include + int main() { auto wallet = loadWalletFromConfig(); hyperliquid::setLogLevel(hyperliquid::LogLevel::Debug); @@ -39,5 +41,17 @@ int main() { position.token, position.supply.value, position.borrow.value); } + spdlog::info("=== userBorrowLendInterest(own wallet, last 30d) ==="); + uint64_t now = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + uint64_t thirtyDaysAgo = now - 30ULL * 24ULL * 3600ULL * 1000ULL; + auto interest = api.userBorrowLendInterest(wallet.accountAddress, thirtyDaysAgo); + spdlog::info("{} interest entries", interest.interest.size()); + for (const auto& entry : interest.interest) { + spdlog::info(" time={} token={} borrow={} supply={}", + entry.time, entry.token, entry.borrow, entry.supply); + } + return 0; } diff --git a/examples/rest_info.cpp b/examples/rest_info.cpp index 994a657..c8f2a74 100644 --- a/examples/rest_info.cpp +++ b/examples/rest_info.cpp @@ -31,6 +31,13 @@ int main() auto mids = api.allMids(); spdlog::info("{} mids", mids.mids.size()); + spdlog::info("=== liquidatable ==="); + auto liquidatable = api.liquidatable(); + spdlog::info("{} liquidatable positions", liquidatable.positions.size()); + for (const auto& p : liquidatable.positions) + spdlog::info(" user={} isolatedAsset={} marginAvailable=[{}, {}]", + p.user, p.isolatedAsset, p.marginAvailable[0], p.marginAvailable[1]); + uint64_t now = static_cast( std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count()); diff --git a/include/hyperliquid/rest/RestApi.h b/include/hyperliquid/rest/RestApi.h index 0dedadd..d0b9635 100644 --- a/include/hyperliquid/rest/RestApi.h +++ b/include/hyperliquid/rest/RestApi.h @@ -114,6 +114,10 @@ class RestApi { BorrowLendUserStateResponse borrowLendUserState(const std::string& user); BorrowLendReserveState borrowLendReserveState(int token); AllBorrowLendReserveStatesResponse allBorrowLendReserveStates(); + UserBorrowLendInterestResponse userBorrowLendInterest(const std::string& user, + uint64_t startTime, + const std::optional& endTime = std::nullopt); + LiquidatableResponse liquidatable(); UserDexAbstractionResponse userDexAbstractionState(const std::string& user); UserAbstractionResponse userAbstraction(const std::string& user); PlaceOrderResponse placeOrder(const std::vector& orders, @@ -251,6 +255,10 @@ class RestApi { void borrowLendUserStateAsync(const std::string& user); void borrowLendReserveStateAsync(int token); void allBorrowLendReserveStatesAsync(); + void userBorrowLendInterestAsync(const std::string& user, + uint64_t startTime, + const std::optional& endTime = std::nullopt); + void liquidatableAsync(); void userDexAbstractionStateAsync(const std::string& user); void userAbstractionAsync(const std::string& user); void placeOrderAsync(const std::vector& orders, diff --git a/include/hyperliquid/rest/RestApiMessageParser.h b/include/hyperliquid/rest/RestApiMessageParser.h index cf26bf6..628f6b7 100644 --- a/include/hyperliquid/rest/RestApiMessageParser.h +++ b/include/hyperliquid/rest/RestApiMessageParser.h @@ -82,6 +82,8 @@ namespace hyperliquid BorrowLendUserStateResponse parseBorrowLendUserState(const std::string& message); BorrowLendReserveState parseBorrowLendReserveState(const std::string& message); AllBorrowLendReserveStatesResponse parseAllBorrowLendReserveStates(const std::string& message); + UserBorrowLendInterestResponse parseUserBorrowLendInterest(const std::string& message); + LiquidatableResponse parseLiquidatable(const std::string& message); PlaceOrderResponse parsePlaceOrder(const std::string& message); CancelOrderResponse parseCancelOrder(const std::string& message); ModifyOrderResponse parseModifyOrder(const std::string& message); diff --git a/include/hyperliquid/rest/RestEndpointListener.h b/include/hyperliquid/rest/RestEndpointListener.h index d1cfcfb..49e1774 100644 --- a/include/hyperliquid/rest/RestEndpointListener.h +++ b/include/hyperliquid/rest/RestEndpointListener.h @@ -76,6 +76,8 @@ class RestEndpointListener { virtual void onBorrowLendUserState(const BorrowLendUserStateResponse&, std::optional = std::nullopt) {} virtual void onBorrowLendReserveState(const BorrowLendReserveState&, std::optional = std::nullopt) {} virtual void onAllBorrowLendReserveStates(const AllBorrowLendReserveStatesResponse&, std::optional = std::nullopt) {} + virtual void onUserBorrowLendInterest(const UserBorrowLendInterestResponse&, std::optional = std::nullopt) {} + virtual void onLiquidatable(const LiquidatableResponse&, std::optional = std::nullopt) {} virtual void onUserDexAbstractionState(const UserDexAbstractionResponse&, std::optional = std::nullopt) {} virtual void onUserAbstraction(const UserAbstractionResponse&, std::optional = std::nullopt) {} }; diff --git a/include/hyperliquid/types/RequestTypes.h b/include/hyperliquid/types/RequestTypes.h index b549cba..1809397 100644 --- a/include/hyperliquid/types/RequestTypes.h +++ b/include/hyperliquid/types/RequestTypes.h @@ -206,6 +206,8 @@ namespace hyperliquid BorrowLendUserState, BorrowLendReserveState, AllBorrowLendReserveStates, + UserBorrowLendInterest, + Liquidatable, UserDexAbstractionState, UserAbstraction, @@ -312,6 +314,8 @@ namespace hyperliquid case RestEndpointType::BorrowLendUserState: return "borrowLendUserState"; case RestEndpointType::BorrowLendReserveState: return "borrowLendReserveState"; case RestEndpointType::AllBorrowLendReserveStates: return "allBorrowLendReserveStates"; + case RestEndpointType::UserBorrowLendInterest: return "userBorrowLendInterest"; + case RestEndpointType::Liquidatable: return "liquidatable"; case RestEndpointType::UserDexAbstractionState: return "userDexAbstraction"; case RestEndpointType::UserAbstraction: return "userAbstraction"; @@ -418,6 +422,8 @@ namespace hyperliquid case RestEndpointType::BorrowLendUserState: return false; case RestEndpointType::BorrowLendReserveState: return false; case RestEndpointType::AllBorrowLendReserveStates: return false; + case RestEndpointType::UserBorrowLendInterest: return false; + case RestEndpointType::Liquidatable: return false; case RestEndpointType::UserDexAbstractionState: return false; case RestEndpointType::UserAbstraction: return false; diff --git a/include/hyperliquid/types/ResponseTypes.h b/include/hyperliquid/types/ResponseTypes.h index 51cd82b..3a860df 100644 --- a/include/hyperliquid/types/ResponseTypes.h +++ b/include/hyperliquid/types/ResponseTypes.h @@ -1701,6 +1701,32 @@ namespace hyperliquid std::optional healthFactor; }; + struct UserBorrowLendInterestEntry + { + uint64_t time = 0; + std::string token; + double borrow = 0.0; + double supply = 0.0; + }; + + struct UserBorrowLendInterestResponse + { + std::vector interest; + }; + + struct LiquidatableEntry + { + std::string user; + int isolatedAsset = 0; + // Meaning of the two values is not documented upstream. + std::array marginAvailable{}; + }; + + struct LiquidatableResponse + { + std::vector positions; + }; + // --- HIP-3 deployer (perp dex abstraction) --- struct PerpDexLimitsCoinCap diff --git a/include/hyperliquid/websocket/WebsocketApi.h b/include/hyperliquid/websocket/WebsocketApi.h index d33d554..4b214c2 100644 --- a/include/hyperliquid/websocket/WebsocketApi.h +++ b/include/hyperliquid/websocket/WebsocketApi.h @@ -135,6 +135,11 @@ namespace hyperliquid void borrowLendUserState(const std::string& user, std::optional correlationId = std::nullopt); void borrowLendReserveState(int token, std::optional correlationId = std::nullopt); void allBorrowLendReserveStates(std::optional correlationId = std::nullopt); + void userBorrowLendInterest(const std::string& user, + uint64_t startTime, + const std::optional& endTime = std::nullopt, + std::optional correlationId = std::nullopt); + void liquidatable(std::optional correlationId = std::nullopt); void spotDeployState(const std::string& user, std::optional correlationId = std::nullopt); void spotPairDeployAuctionStatus(std::optional correlationId = std::nullopt); diff --git a/src/messages/InfoRequestBuilder.cpp b/src/messages/InfoRequestBuilder.cpp index d179101..573280d 100644 --- a/src/messages/InfoRequestBuilder.cpp +++ b/src/messages/InfoRequestBuilder.cpp @@ -480,6 +480,25 @@ nlohmann::ordered_json InfoRequestBuilder::allBorrowLendReserveStates() return body; } +nlohmann::ordered_json InfoRequestBuilder::userBorrowLendInterest(const std::string& user, + uint64_t startTime, + const std::optional& endTime) +{ + nlohmann::ordered_json body; + body["type"] = toString(RestEndpointType::UserBorrowLendInterest); + body["user"] = user; + body["startTime"] = startTime; + if (endTime) body["endTime"] = *endTime; + return body; +} + +nlohmann::ordered_json InfoRequestBuilder::liquidatable() +{ + nlohmann::ordered_json body; + body["type"] = toString(RestEndpointType::Liquidatable); + return body; +} + nlohmann::ordered_json InfoRequestBuilder::userDexAbstractionState(const std::string& user) { nlohmann::ordered_json body; diff --git a/src/messages/InfoRequestBuilder.h b/src/messages/InfoRequestBuilder.h index d41ebfb..a08fd57 100644 --- a/src/messages/InfoRequestBuilder.h +++ b/src/messages/InfoRequestBuilder.h @@ -95,6 +95,10 @@ class InfoRequestBuilder { static nlohmann::ordered_json borrowLendUserState(const std::string& user); static nlohmann::ordered_json borrowLendReserveState(int token); static nlohmann::ordered_json allBorrowLendReserveStates(); + static nlohmann::ordered_json userBorrowLendInterest(const std::string& user, + uint64_t startTime, + const std::optional& endTime = std::nullopt); + static nlohmann::ordered_json liquidatable(); static nlohmann::ordered_json userDexAbstractionState(const std::string& user); static nlohmann::ordered_json userAbstraction(const std::string& user); diff --git a/src/rest/RestApi.cpp b/src/rest/RestApi.cpp index 8a1f7ef..8750477 100644 --- a/src/rest/RestApi.cpp +++ b/src/rest/RestApi.cpp @@ -482,6 +482,21 @@ AllBorrowLendReserveStatesResponse RestApi::allBorrowLendReserveStates() InfoRequestBuilder::allBorrowLendReserveStates())); } +UserBorrowLendInterestResponse RestApi::userBorrowLendInterest(const std::string& user, + uint64_t startTime, + const std::optional& endTime) +{ + return RestApiMessageParser().parseUserBorrowLendInterest( + impl_->signAndSendSync(RestEndpointType::UserBorrowLendInterest, + InfoRequestBuilder::userBorrowLendInterest(user, startTime, endTime))); +} + +LiquidatableResponse RestApi::liquidatable() +{ + return RestApiMessageParser().parseLiquidatable( + impl_->signAndSendSync(RestEndpointType::Liquidatable, InfoRequestBuilder::liquidatable())); +} + UserDexAbstractionResponse RestApi::userDexAbstractionState(const std::string& user) { return RestApiMessageParser().parseUserDexAbstractionState( @@ -1072,6 +1087,19 @@ void RestApi::allBorrowLendReserveStatesAsync() impl_->signAndSend(RestEndpointType::AllBorrowLendReserveStates, InfoRequestBuilder::allBorrowLendReserveStates()); } +void RestApi::userBorrowLendInterestAsync(const std::string& user, + uint64_t startTime, + const std::optional& endTime) +{ + impl_->signAndSend(RestEndpointType::UserBorrowLendInterest, + InfoRequestBuilder::userBorrowLendInterest(user, startTime, endTime)); +} + +void RestApi::liquidatableAsync() +{ + impl_->signAndSend(RestEndpointType::Liquidatable, InfoRequestBuilder::liquidatable()); +} + void RestApi::userDexAbstractionStateAsync(const std::string& user) { impl_->signAndSend(RestEndpointType::UserDexAbstractionState, InfoRequestBuilder::userDexAbstractionState(user)); diff --git a/src/rest/RestApiMessageParser.cpp b/src/rest/RestApiMessageParser.cpp index f9fa513..7a6b807 100644 --- a/src/rest/RestApiMessageParser.cpp +++ b/src/rest/RestApiMessageParser.cpp @@ -169,6 +169,12 @@ namespace hyperliquid case RestEndpointType::AllBorrowLendReserveStates: listener.onAllBorrowLendReserveStates(parseAllBorrowLendReserveStates(message), correlationId); break; + case RestEndpointType::UserBorrowLendInterest: + listener.onUserBorrowLendInterest(parseUserBorrowLendInterest(message), correlationId); + break; + case RestEndpointType::Liquidatable: + listener.onLiquidatable(parseLiquidatable(message), correlationId); + break; case RestEndpointType::PlaceOrder: listener.onPlaceOrder(parsePlaceOrder(message), correlationId); break; @@ -1898,6 +1904,80 @@ namespace hyperliquid return response; } + UserBorrowLendInterestResponse parseUserBorrowLendInterest(const std::string& message) + { + UserBorrowLendInterestResponse response; + padded = simdjson::padded_string(message.data(), message.size()); + auto doc = parser.iterate(padded); + + try + { + validateStructure(message); + + auto arr = doc.get_array().value(); + for (auto entry : arr) + { + auto obj = entry.get_object().value(); + UserBorrowLendInterestEntry item{}; + item.time = obj["time"].get_uint64().value(); + item.token = std::string(obj["token"].get_string().value()); + item.borrow = parseNumberField(obj, "borrow"); + item.supply = parseNumberField(obj, "supply"); + response.interest.push_back(std::move(item)); + } + } + catch (const simdjson::simdjson_error& err) + { + getLogger()->error("RestMessageParser: parse error in userBorrowLendInterest: {}\n raw: {}", err.what(), message); + } + + return response; + } + + LiquidatableResponse parseLiquidatable(const std::string& message) + { + LiquidatableResponse response; + padded = simdjson::padded_string(message.data(), message.size()); + auto doc = parser.iterate(padded); + + try + { + validateStructure(message); + + auto arr = doc.get_array().value(); + for (auto entry : arr) + { + auto obj = entry.get_object().value(); + LiquidatableEntry item{}; + item.user = std::string(obj["user"].get_string().value()); + + auto positionIndexObj = obj["positionIndex"].get_object().value(); + auto isolatedObj = positionIndexObj["isolated"].get_object().value(); + item.isolatedAsset = static_cast(isolatedObj["asset"].get_int64().value()); + + auto marginArr = obj["marginAvailable"].get_array().value(); + auto iter = marginArr.begin(); + if (iter != marginArr.end()) + { + item.marginAvailable[0] = (*iter).get_double().value(); + ++iter; + if (iter != marginArr.end()) + { + item.marginAvailable[1] = (*iter).get_double().value(); + } + } + + response.positions.push_back(std::move(item)); + } + } + catch (const simdjson::simdjson_error& err) + { + getLogger()->error("RestMessageParser: parse error in liquidatable: {}\n raw: {}", err.what(), message); + } + + return response; + } + ClearinghouseState parseClearinghouseStateObj(simdjson::ondemand::object& doc) { ClearinghouseState response{}; @@ -3446,6 +3526,16 @@ namespace hyperliquid return impl_->parseAllBorrowLendReserveStates(message); } + UserBorrowLendInterestResponse RestApiMessageParser::parseUserBorrowLendInterest(const std::string& message) + { + return impl_->parseUserBorrowLendInterest(message); + } + + LiquidatableResponse RestApiMessageParser::parseLiquidatable(const std::string& message) + { + return impl_->parseLiquidatable(message); + } + PlaceOrderResponse RestApiMessageParser::parsePlaceOrder(const std::string& message) { return impl_->parsePlaceOrder(message); diff --git a/src/websocket/WebsocketApi.cpp b/src/websocket/WebsocketApi.cpp index 23394e0..354e382 100644 --- a/src/websocket/WebsocketApi.cpp +++ b/src/websocket/WebsocketApi.cpp @@ -594,6 +594,20 @@ namespace hyperliquid std::nullopt, std::nullopt, correlationId); } + void WebsocketApi::userBorrowLendInterest(const std::string& user, uint64_t startTime, + const std::optional& endTime, std::optional correlationId) + { + return impl_->signAndSend(RestEndpointType::UserBorrowLendInterest, + InfoRequestBuilder::userBorrowLendInterest(user, startTime, endTime), + std::nullopt, std::nullopt, correlationId); + } + + void WebsocketApi::liquidatable(std::optional correlationId) + { + return impl_->signAndSend(RestEndpointType::Liquidatable, InfoRequestBuilder::liquidatable(), + std::nullopt, std::nullopt, correlationId); + } + void WebsocketApi::placeOrder(const std::vector& orders, Grouping grouping, const std::optional& builder, diff --git a/tests/rest_borrow_lend_test.cpp b/tests/rest_borrow_lend_test.cpp index ea2be37..e42cfeb 100644 --- a/tests/rest_borrow_lend_test.cpp +++ b/tests/rest_borrow_lend_test.cpp @@ -28,6 +28,23 @@ TEST(InfoRequestBuilderTest, AllBorrowLendReserveStates) EXPECT_EQ(body["type"], "allBorrowLendReserveStates"); } +TEST(InfoRequestBuilderTest, UserBorrowLendInterestNoEndTime) +{ + auto body = InfoRequestBuilder::userBorrowLendInterest("0xabc", 1700000000000ULL); + EXPECT_EQ(body["type"], "userBorrowLendInterest"); + EXPECT_EQ(body["user"], "0xabc"); + EXPECT_EQ(body["startTime"], 1700000000000ULL); + EXPECT_FALSE(body.contains("endTime")); +} + +TEST(InfoRequestBuilderTest, UserBorrowLendInterestWithEndTime) +{ + auto body = InfoRequestBuilder::userBorrowLendInterest("0xabc", 1700000000000ULL, 1700100000000ULL); + EXPECT_EQ(body["type"], "userBorrowLendInterest"); + EXPECT_EQ(body["startTime"], 1700000000000ULL); + EXPECT_EQ(body["endTime"], 1700100000000ULL); +} + TEST(RestApiMessageParserInfoTest, ParseBorrowLendReserveState) { std::string message = R"({ @@ -120,6 +137,34 @@ TEST(RestApiMessageParserInfoTest, ParseBorrowLendUserStateHealthFactorPresent) EXPECT_DOUBLE_EQ(*response.healthFactor, 1.5); } +TEST(RestApiMessageParserInfoTest, ParseUserBorrowLendInterestEmpty) +{ + // Live mainnet response for accounts with no borrow/lend activity in the requested window. + std::string message = R"([])"; + + RestApiMessageParser parser; + auto response = parser.parseUserBorrowLendInterest(message); + + EXPECT_TRUE(response.interest.empty()); +} + +TEST(RestApiMessageParserInfoTest, ParseUserBorrowLendInterestPopulated) +{ + // Live testnet response (POST /info {"type":"userBorrowLendInterest","user":"0x6829..."}). + std::string message = R"([ + {"time": 1788868810000, "token": "USDC", "borrow": "0.0", "supply": "0.0000008"} + ])"; + + RestApiMessageParser parser; + auto response = parser.parseUserBorrowLendInterest(message); + + ASSERT_EQ(response.interest.size(), 1u); + EXPECT_EQ(response.interest[0].time, 1788868810000ULL); + EXPECT_EQ(response.interest[0].token, "USDC"); + EXPECT_DOUBLE_EQ(response.interest[0].borrow, 0.0); + EXPECT_DOUBLE_EQ(response.interest[0].supply, 0.0000008); +} + // --- borrowLend exchange action (write side) --- // // Field shapes below (action = {type, operation, token, amount}, plain L1-action signing with no diff --git a/tests/rest_market_data_test.cpp b/tests/rest_market_data_test.cpp index c853fe2..ea6371a 100644 --- a/tests/rest_market_data_test.cpp +++ b/tests/rest_market_data_test.cpp @@ -425,3 +425,44 @@ TEST(RestApiMessageParserInfoTest, ParseActiveAssetDataIsolatedHip3) ASSERT_TRUE(data.leverageRawUsd.has_value()); EXPECT_DOUBLE_EQ(*data.leverageRawUsd, 123.45); } + +TEST(InfoRequestBuilderTest, Liquidatable) +{ + auto body = InfoRequestBuilder::liquidatable(); + EXPECT_EQ(body["type"], "liquidatable"); + EXPECT_FALSE(body.contains("user")); +} + +TEST(RestApiMessageParserInfoTest, ParseLiquidatableEmpty) +{ + // Live mainnet response with zero accounts currently liquidatable. + std::string message = R"([])"; + + RestApiMessageParser parser; + auto response = parser.parseLiquidatable(message); + + EXPECT_TRUE(response.positions.empty()); +} + +TEST(RestApiMessageParserInfoTest, ParseLiquidatablePopulated) +{ + // Shape taken from the official TS SDK (@nktkas/hyperliquid) source, not a live capture - + // the endpoint returned an empty array for every account tried, so this couldn't be + // confirmed against a populated payload. + std::string message = R"([ + { + "user": "0x31ca8395cf837de08b24da3f660e77761dfb974", + "positionIndex": {"isolated": {"asset": 5}}, + "marginAvailable": [12.5, 0.0] + } + ])"; + + RestApiMessageParser parser; + auto response = parser.parseLiquidatable(message); + + ASSERT_EQ(response.positions.size(), 1u); + EXPECT_EQ(response.positions[0].user, "0x31ca8395cf837de08b24da3f660e77761dfb974"); + EXPECT_EQ(response.positions[0].isolatedAsset, 5); + EXPECT_DOUBLE_EQ(response.positions[0].marginAvailable[0], 12.5); + EXPECT_DOUBLE_EQ(response.positions[0].marginAvailable[1], 0.0); +} diff --git a/tests/websocket_api_test.cpp b/tests/websocket_api_test.cpp index 16bf7d9..abbea8b 100644 --- a/tests/websocket_api_test.cpp +++ b/tests/websocket_api_test.cpp @@ -55,6 +55,20 @@ TEST(InfoRequestBuilderPayloads, BorrowLendReserveState) EXPECT_EQ(body["token"], 0); } +TEST(InfoRequestBuilderPayloads, UserBorrowLendInterest) +{ + auto body = InfoRequestBuilder::userBorrowLendInterest("0xabc", 1700000000000ULL); + EXPECT_EQ(body["type"], "userBorrowLendInterest"); + EXPECT_EQ(body["user"], "0xabc"); + EXPECT_EQ(body["startTime"], 1700000000000ULL); +} + +TEST(InfoRequestBuilderPayloads, Liquidatable) +{ + auto body = InfoRequestBuilder::liquidatable(); + EXPECT_EQ(body["type"], "liquidatable"); +} + namespace { struct NoopListener : WebsocketApiListener @@ -114,6 +128,8 @@ TEST(WebsocketApiInfoWrappers, CompileLinkAndInvokeWithoutCrashing) EXPECT_NO_THROW(ws.borrowLendUserState("0xabc")); EXPECT_NO_THROW(ws.borrowLendReserveState(0)); EXPECT_NO_THROW(ws.allBorrowLendReserveStates()); + EXPECT_NO_THROW(ws.userBorrowLendInterest("0xabc", 0)); + EXPECT_NO_THROW(ws.liquidatable()); EXPECT_NO_THROW(ws.perpCategories()); EXPECT_NO_THROW(ws.perpConciseAnnotations()); EXPECT_NO_THROW(ws.allPerpMetas());