From 3f70ab93e6fd6e6581cb85fcd6a0e0e0928ebd3d Mon Sep 17 00:00:00 2001 From: TuxedoFish <“harryliversedge@gmail.com”> Date: Tue, 8 Sep 2026 10:40:05 +0100 Subject: [PATCH 1/2] Exchange endpoints: createVault, setReferrer Adds two missing /exchange actions following the CONTRIBUTING.md pattern: - createVault: creates a new vault. L1 action whose action carries its own "nonce" field equal to the envelope nonce (like agentSendAsset), and returns the new vault's address on success. Does not take a vaultAddress parameter (it creates a vault rather than acting through an existing one). - setReferrer: sets a referral code for the calling wallet. Plain L1 action, no vaultAddress support, reuses SimpleResponse/parseSimpleResponse like vaultTransfer/borrowLend. Request/response shapes are cross-checked against the official TS SDK (@nktkas/hyperliquid, src/api/exchange/_methods/{createVault,setReferrer}.ts) since neither action is documented on the public gitbook docs page. Not verified live against testnet in this environment (no funded testnet wallet available) - please double-check before relying on this. Wires up RestApi (sync + async) and WebsocketApi for both, adds request-builder and response-parser tests to tests/rest_vaults_test.cpp (the file already covering the related vaultDetails/vaultTransfer/referral endpoints), adds wallet-required smoke tests to tests/websocket_api_exchange_test.cpp, adds examples/rest_create_vault.cpp and examples/rest_set_referrer.cpp, and updates README's exchange actions coverage table (36 -> 38 of 68 implemented). Closes #122 Closes #125 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018Le73N2EZjGLsCqmCScAse --- README.md | 6 +- examples/rest_create_vault.cpp | 31 ++++ examples/rest_set_referrer.cpp | 27 ++++ include/hyperliquid/rest/RestApi.h | 12 +- .../hyperliquid/rest/RestApiMessageParser.h | 1 + .../hyperliquid/rest/RestEndpointListener.h | 1 + include/hyperliquid/types/RequestTypes.h | 20 +++ include/hyperliquid/types/ResponseTypes.h | 9 ++ include/hyperliquid/websocket/WebsocketApi.h | 6 + src/messages/ExchangeRequestBuilder.cpp | 29 ++++ src/messages/ExchangeRequestBuilder.h | 4 + src/rest/RestApi.cpp | 26 ++++ src/rest/RestApiMessageParser.cpp | 47 ++++++ src/signing/Signing.cpp | 11 +- src/websocket/WebsocketApi.cpp | 16 +++ tests/rest_vaults_test.cpp | 136 ++++++++++++++++++ tests/websocket_api_exchange_test.cpp | 10 ++ 17 files changed, 381 insertions(+), 11 deletions(-) create mode 100644 examples/rest_create_vault.cpp create mode 100644 examples/rest_set_referrer.cpp diff --git a/README.md b/README.md index 2a07e94..7c38a2b 100644 --- a/README.md +++ b/README.md @@ -198,13 +198,14 @@ Legend: ✅ implemented — ⬜ not yet implemented. | `userDexAbstraction` (deprecated, use `userSetAbstraction`) | ✅ | `RestApi::userDexAbstraction` | | `borrowLend` | ✅ | `RestApi::borrowLend` | | `perpDeploy` (`registerAsset2` only - see note below) | ✅ | `RestApi::perpDeployRegisterAsset2` | +| `setReferrer` | ✅ | `RestApi::setReferrer` | | `activateOutcomeDeployer` | ⬜ | | | `agentEnableDexAbstraction` (deprecated, use `agentSetAbstraction`) | ⬜ | | | `authorizeAqav2Role` | ⬜ | | | `claimRewards` | ⬜ | | | `convertToMultiSigUser` | ⬜ | | | `createSubAccount` | ✅ | `RestApi::createSubAccount` | -| `createVault` | ⬜ | | +| `createVault` | ✅ | `RestApi::createVault` | | `cSignerAction` | ⬜ | | | `cValidatorAction` | ⬜ | | | `evmUserModify` | ⬜ | | @@ -213,7 +214,6 @@ Legend: ✅ implemented — ⬜ not yet implemented. | `linkStakingUser` | ⬜ | | | `registerReferrer` | ⬜ | | | `setDisplayName` | ⬜ | | -| `setReferrer` | ⬜ | | | `spotDeploy` (`registerToken2`) | ✅ | `RestApi::spotDeployRegisterToken2` | | `spotDeploy` (`userGenesis`) | ✅ | `RestApi::spotDeployUserGenesis` | | `spotDeploy` (`genesis`) | ✅ | `RestApi::spotDeployGenesis` | @@ -236,7 +236,7 @@ Legend: ✅ implemented — ⬜ not yet implemented. | `vaultDistribute` | ⬜ | | | `vaultModify` | ⬜ | | -39 of 68 documented exchange actions implemented on REST (`RestApi`). `WebsocketApi` covers a smaller subset — `placeOrder`, `cancelOrder`, `cancelOrderByCloid`, `scheduleCancel`, `modifyOrder`, `batchModifyOrder` — plus posting `meta`/`spotMeta`/`outcomeMeta`/`perpDexs` info reads over the socket; the newer transfer/staking/TWAP actions are REST-only so far. +41 of 68 documented exchange actions implemented on REST (`RestApi`). `WebsocketApi` covers a smaller subset — `placeOrder`, `cancelOrder`, `cancelOrderByCloid`, `scheduleCancel`, `modifyOrder`, `batchModifyOrder` — plus posting `meta`/`spotMeta`/`outcomeMeta`/`perpDexs` info reads over the socket; the newer transfer/staking/TWAP actions are REST-only so far. `perpDeploy` is a large multi-variant action (16 sub-actions sharing `"type": "perpDeploy"`); only `registerAsset2` (deploying a new HIP-3 perp asset, optionally creating a new dex) is implemented. The other 15 variants (`registerAsset`, `setOracle`, `setFundingMultipliers`, `setFundingInterestRates`, `haltTrading`, `setMarginTableIds`, `insertMarginTable`, `setFeeRecipient`, `setOpenInterestCaps`, `setSubDeployers`, `setMarginModes`, `setFeeScale`, `setGrowthModes`, `setPerpAnnotation`, `disableDex`) are post-deployment admin/config actions for an already-deployed dex and are not yet implemented. diff --git a/examples/rest_create_vault.cpp b/examples/rest_create_vault.cpp new file mode 100644 index 0000000..8554dac --- /dev/null +++ b/examples/rest_create_vault.cpp @@ -0,0 +1,31 @@ +#include "test_config.h" + +#include +#include +#include + +int main() +{ + auto wallet = loadWalletFromConfig(); + hyperliquid::setLogLevel(hyperliquid::LogLevel::Debug); + + hyperliquid::ApiConfig config; + config.env = hyperliquid::Environment::Testnet; + config.wallet = wallet; + + hyperliquid::RestApi api(config); + + hyperliquid::CreateVaultRequest req; + req.name = "SDK Test Vault"; + req.description = "Vault created by hyperliquid-sdk-cpp's rest_create_vault example."; + req.initialUsd = 100.0; // Hyperliquid requires at least 100 USD to create a vault. + + auto resp = api.createVault(req); + spdlog::info("createVault: status={} type={}", resp.status, resp.type); + if (resp.vaultAddress) + spdlog::info(" New vault address: {}", *resp.vaultAddress); + if (resp.error) + spdlog::info(" Error: {}", *resp.error); + + return 0; +} diff --git a/examples/rest_set_referrer.cpp b/examples/rest_set_referrer.cpp new file mode 100644 index 0000000..3b031b5 --- /dev/null +++ b/examples/rest_set_referrer.cpp @@ -0,0 +1,27 @@ +#include "test_config.h" + +#include +#include +#include + +int main() +{ + auto wallet = loadWalletFromConfig(); + hyperliquid::setLogLevel(hyperliquid::LogLevel::Debug); + + hyperliquid::ApiConfig config; + config.env = hyperliquid::Environment::Testnet; + config.wallet = wallet; + + hyperliquid::RestApi api(config); + + hyperliquid::SetReferrerRequest req; + req.code = "SDKTEST"; // Replace with a real testnet referral code before running. + + auto resp = api.setReferrer(req); + spdlog::info("setReferrer: status={} type={}", resp.status, resp.type); + if (resp.error) + spdlog::info(" Error: {}", *resp.error); + + return 0; +} diff --git a/include/hyperliquid/rest/RestApi.h b/include/hyperliquid/rest/RestApi.h index 09dcba2..89d978b 100644 --- a/include/hyperliquid/rest/RestApi.h +++ b/include/hyperliquid/rest/RestApi.h @@ -146,9 +146,9 @@ class RestApi { const std::optional& vaultAddress = std::nullopt); TwapCancelResponse twapCancel(const TwapCancelRequest& request, const std::optional& vaultAddress = std::nullopt); - // vaultTransfer/hip3LiquidatorTransfer/createSubAccount/subAccountTransfer/borrowLend/ - // usdClassTransfer/sendAsset/usdSend/spotSend/withdraw3/approveBuilderFee move funds (or - // manage sub-accounts) against the calling wallet directly (their target + // vaultTransfer/hip3LiquidatorTransfer/createSubAccount/subAccountTransfer/createVault/ + // borrowLend/usdClassTransfer/sendAsset/usdSend/spotSend/withdraw3/approveBuilderFee move + // funds (or manage sub-accounts/vaults) against the calling wallet directly (their target // vault/dex/destination/sub-account/etc. is a field of the request itself, or there is no // vault-like target at all), so unlike the other exchange methods they do not take a // vaultAddress parameter. @@ -156,6 +156,7 @@ class RestApi { SimpleResponse hip3LiquidatorTransfer(const Hip3LiquidatorTransferRequest& request); CreateSubAccountResponse createSubAccount(const CreateSubAccountRequest& request); SimpleResponse subAccountTransfer(const SubAccountTransferRequest& request); + CreateVaultResponse createVault(const CreateVaultRequest& request); SimpleResponse borrowLend(const BorrowLendRequest& request); SimpleResponse spotDeployRegisterToken2(const SpotDeployRegisterToken2Request& request); SimpleResponse spotDeployUserGenesis(const SpotDeployUserGenesisRequest& request); @@ -170,6 +171,9 @@ class RestApi { SimpleResponse withdraw3(const Withdraw3Request& request); SimpleResponse approveBuilderFee(const ApproveBuilderFeeRequest& request); SimpleResponse userSetAbstraction(const UserSetAbstractionRequest& request); + // setReferrer sets a referral code for the calling wallet itself, so like the transfer-style + // actions above it does not take a vaultAddress parameter. + SimpleResponse setReferrer(const SetReferrerRequest& request); SimpleResponse cDeposit(uint64_t wei); SimpleResponse cWithdraw(uint64_t wei); SimpleResponse tokenDelegate(const TokenDelegateRequest& request); @@ -297,6 +301,7 @@ class RestApi { void hip3LiquidatorTransferAsync(const Hip3LiquidatorTransferRequest& request); void createSubAccountAsync(const CreateSubAccountRequest& request); void subAccountTransferAsync(const SubAccountTransferRequest& request); + void createVaultAsync(const CreateVaultRequest& request); void borrowLendAsync(const BorrowLendRequest& request); void spotDeployRegisterToken2Async(const SpotDeployRegisterToken2Request& request); void spotDeployUserGenesisAsync(const SpotDeployUserGenesisRequest& request); @@ -311,6 +316,7 @@ class RestApi { void withdraw3Async(const Withdraw3Request& request); void approveBuilderFeeAsync(const ApproveBuilderFeeRequest& request); void userSetAbstractionAsync(const UserSetAbstractionRequest& request); + void setReferrerAsync(const SetReferrerRequest& request); void cDepositAsync(uint64_t wei); void cWithdrawAsync(uint64_t wei); void tokenDelegateAsync(const TokenDelegateRequest& request); diff --git a/include/hyperliquid/rest/RestApiMessageParser.h b/include/hyperliquid/rest/RestApiMessageParser.h index c0a702a..14edd14 100644 --- a/include/hyperliquid/rest/RestApiMessageParser.h +++ b/include/hyperliquid/rest/RestApiMessageParser.h @@ -91,6 +91,7 @@ namespace hyperliquid TwapOrderResponse parseTwapOrder(const std::string& message); TwapCancelResponse parseTwapCancel(const std::string& message); CreateSubAccountResponse parseCreateSubAccount(const std::string& message); + CreateVaultResponse parseCreateVault(const std::string& message); DelegationsResponse parseDelegations(const std::string& message); DelegatorSummaryResponse parseDelegatorSummary(const std::string& message); DelegatorHistoryResponse parseDelegatorHistory(const std::string& message); diff --git a/include/hyperliquid/rest/RestEndpointListener.h b/include/hyperliquid/rest/RestEndpointListener.h index 6f09540..5cab38f 100644 --- a/include/hyperliquid/rest/RestEndpointListener.h +++ b/include/hyperliquid/rest/RestEndpointListener.h @@ -70,6 +70,7 @@ class RestEndpointListener { virtual void onTwapOrder(const TwapOrderResponse&, std::optional = std::nullopt) {} virtual void onTwapCancel(const TwapCancelResponse&, std::optional = std::nullopt) {} virtual void onCreateSubAccount(const CreateSubAccountResponse&, std::optional = std::nullopt) {} + virtual void onCreateVault(const CreateVaultResponse&, std::optional = std::nullopt) {} virtual void onDelegations(const DelegationsResponse&, std::optional = std::nullopt) {} virtual void onDelegatorSummary(const DelegatorSummaryResponse&, std::optional = std::nullopt) {} virtual void onDelegatorHistory(const DelegatorHistoryResponse&, std::optional = std::nullopt) {} diff --git a/include/hyperliquid/types/RequestTypes.h b/include/hyperliquid/types/RequestTypes.h index 8381373..5376e22 100644 --- a/include/hyperliquid/types/RequestTypes.h +++ b/include/hyperliquid/types/RequestTypes.h @@ -229,6 +229,7 @@ namespace hyperliquid Hip3LiquidatorTransfer, CreateSubAccount, SubAccountTransfer, + CreateVault, BorrowLend, SpotDeployRegisterToken2, SpotDeployUserGenesis, @@ -243,6 +244,7 @@ namespace hyperliquid Withdraw3, ApproveBuilderFee, UserSetAbstraction, + SetReferrer, // Exchange endpoints (signed, user-signed action) CDeposit, @@ -340,6 +342,7 @@ namespace hyperliquid case RestEndpointType::Hip3LiquidatorTransfer: return "hip3LiquidatorTransfer"; case RestEndpointType::CreateSubAccount: return "createSubAccount"; case RestEndpointType::SubAccountTransfer: return "subAccountTransfer"; + case RestEndpointType::CreateVault: return "createVault"; case RestEndpointType::BorrowLend: return "borrowLend"; case RestEndpointType::SpotDeployRegisterToken2: return "spotDeploy"; case RestEndpointType::SpotDeployUserGenesis: return "spotDeploy"; @@ -354,6 +357,7 @@ namespace hyperliquid case RestEndpointType::Withdraw3: return "withdraw3"; case RestEndpointType::ApproveBuilderFee: return "approveBuilderFee"; case RestEndpointType::UserSetAbstraction: return "userSetAbstraction"; + case RestEndpointType::SetReferrer: return "setReferrer"; case RestEndpointType::CDeposit: return "cDeposit"; case RestEndpointType::CWithdraw: return "cWithdraw"; @@ -452,6 +456,7 @@ namespace hyperliquid case RestEndpointType::Hip3LiquidatorTransfer: return true; case RestEndpointType::CreateSubAccount: return true; case RestEndpointType::SubAccountTransfer: return true; + case RestEndpointType::CreateVault: return true; case RestEndpointType::BorrowLend: return true; case RestEndpointType::SpotDeployRegisterToken2: return true; case RestEndpointType::SpotDeployUserGenesis: return true; @@ -466,6 +471,7 @@ namespace hyperliquid case RestEndpointType::Withdraw3: return true; case RestEndpointType::ApproveBuilderFee: return true; case RestEndpointType::UserSetAbstraction: return true; + case RestEndpointType::SetReferrer: return true; case RestEndpointType::CDeposit: return true; case RestEndpointType::CWithdraw: return true; @@ -941,6 +947,20 @@ namespace hyperliquid std::optional destination; }; + struct CreateVaultRequest + { + std::string name; + std::string description; + // USD, converted to raw 1e6 units on the wire (see ExchangeRequestBuilder::createVault). + // Hyperliquid requires at least 100 USD to create a vault; not enforced client-side. + double initialUsd; + }; + + struct SetReferrerRequest + { + std::string code; + }; + inline int outcomeEncoding(int outcomeIndex, int side) { return 10 * outcomeIndex + side; diff --git a/include/hyperliquid/types/ResponseTypes.h b/include/hyperliquid/types/ResponseTypes.h index 43cf27b..45decb3 100644 --- a/include/hyperliquid/types/ResponseTypes.h +++ b/include/hyperliquid/types/ResponseTypes.h @@ -1539,6 +1539,15 @@ namespace hyperliquid std::optional error; }; + struct CreateVaultResponse + { + std::string status; + std::string type; + // The newly created vault's address (response.data on success). + std::optional vaultAddress; + std::optional error; + }; + struct TwapCancelResponse { std::string status; diff --git a/include/hyperliquid/websocket/WebsocketApi.h b/include/hyperliquid/websocket/WebsocketApi.h index 3a0d2b5..d50941b 100644 --- a/include/hyperliquid/websocket/WebsocketApi.h +++ b/include/hyperliquid/websocket/WebsocketApi.h @@ -209,6 +209,9 @@ namespace hyperliquid void subAccountTransfer(const SubAccountTransferRequest& request, std::optional correlationId = std::nullopt); + void createVault(const CreateVaultRequest& request, + std::optional correlationId = std::nullopt); + void usdClassTransfer(const UsdClassTransferRequest& request, std::optional correlationId = std::nullopt); void sendAsset(const SendAssetRequest& request, @@ -226,6 +229,9 @@ namespace hyperliquid void userSetAbstraction(const UserSetAbstractionRequest& request, std::optional correlationId = std::nullopt); + void setReferrer(const SetReferrerRequest& request, + std::optional correlationId = std::nullopt); + void cDeposit(uint64_t wei, std::optional correlationId = std::nullopt); void cWithdraw(uint64_t wei, diff --git a/src/messages/ExchangeRequestBuilder.cpp b/src/messages/ExchangeRequestBuilder.cpp index e262d61..15adddf 100644 --- a/src/messages/ExchangeRequestBuilder.cpp +++ b/src/messages/ExchangeRequestBuilder.cpp @@ -425,6 +425,24 @@ namespace hyperliquid return body; } + nlohmann::ordered_json ExchangeRequestBuilder::createVault(const CreateVaultRequest& request) const + { + nlohmann::ordered_json action; + action["type"] = "createVault"; + action["name"] = request.name; + action["description"] = request.description; + // Same raw-1e6-unit USDC representation as vaultTransfer's "usd" field above. + action["initialUsd"] = static_cast(std::llround(request.initialUsd * 1e6)); + // createVault is the one L1 action (besides agentSendAsset) whose action carries its own + // "nonce" field, required to equal the envelope nonce - injected downstream by + // Signing::prepareBody, not here (see the RestEndpointType::CreateVault special-case + // next to AgentSendAsset's). + + nlohmann::ordered_json body; + body["action"] = action; + return body; + } + nlohmann::ordered_json ExchangeRequestBuilder::borrowLend(const BorrowLendRequest& request) const { nlohmann::ordered_json action; @@ -693,6 +711,17 @@ namespace hyperliquid return body; } + nlohmann::ordered_json ExchangeRequestBuilder::setReferrer(const SetReferrerRequest& request) const + { + nlohmann::ordered_json action; + action["type"] = "setReferrer"; + action["code"] = request.code; + + nlohmann::ordered_json body; + body["action"] = action; + return body; + } + nlohmann::ordered_json ExchangeRequestBuilder::tokenDelegate(const TokenDelegateRequest& request) const { nlohmann::ordered_json action; diff --git a/src/messages/ExchangeRequestBuilder.h b/src/messages/ExchangeRequestBuilder.h index 6cb5a1a..2c5a583 100644 --- a/src/messages/ExchangeRequestBuilder.h +++ b/src/messages/ExchangeRequestBuilder.h @@ -51,6 +51,8 @@ class ExchangeRequestBuilder { nlohmann::ordered_json subAccountTransfer(const SubAccountTransferRequest& request) const; + nlohmann::ordered_json createVault(const CreateVaultRequest& request) const; + nlohmann::ordered_json borrowLend(const BorrowLendRequest& request) const; nlohmann::ordered_json spotDeployRegisterToken2(const SpotDeployRegisterToken2Request& request) const; @@ -79,6 +81,8 @@ class ExchangeRequestBuilder { nlohmann::ordered_json userSetAbstraction(const UserSetAbstractionRequest& request) const; + nlohmann::ordered_json setReferrer(const SetReferrerRequest& request) const; + nlohmann::ordered_json cDeposit(uint64_t wei) const; nlohmann::ordered_json cWithdraw(uint64_t wei) const; diff --git a/src/rest/RestApi.cpp b/src/rest/RestApi.cpp index 85fa072..b237920 100644 --- a/src/rest/RestApi.cpp +++ b/src/rest/RestApi.cpp @@ -652,6 +652,13 @@ SimpleResponse RestApi::subAccountTransfer(const SubAccountTransferRequest& requ impl_->exchangeRequestBuilder.subAccountTransfer(request))); } +CreateVaultResponse RestApi::createVault(const CreateVaultRequest& request) +{ + return RestApiMessageParser().parseCreateVault( + impl_->signAndSendSync(RestEndpointType::CreateVault, + impl_->exchangeRequestBuilder.createVault(request))); +} + SimpleResponse RestApi::borrowLend(const BorrowLendRequest& request) { return RestApiMessageParser().parseSimpleResponse( @@ -750,6 +757,13 @@ SimpleResponse RestApi::userSetAbstraction(const UserSetAbstractionRequest& requ impl_->exchangeRequestBuilder.userSetAbstraction(request))); } +SimpleResponse RestApi::setReferrer(const SetReferrerRequest& request) +{ + return RestApiMessageParser().parseSimpleResponse( + impl_->signAndSendSync(RestEndpointType::SetReferrer, + impl_->exchangeRequestBuilder.setReferrer(request))); +} + SimpleResponse RestApi::cDeposit(uint64_t wei) { return RestApiMessageParser().parseSimpleResponse( @@ -1262,6 +1276,12 @@ void RestApi::subAccountTransferAsync(const SubAccountTransferRequest& request) impl_->exchangeRequestBuilder.subAccountTransfer(request)); } +void RestApi::createVaultAsync(const CreateVaultRequest& request) +{ + impl_->signAndSend(RestEndpointType::CreateVault, + impl_->exchangeRequestBuilder.createVault(request)); +} + void RestApi::borrowLendAsync(const BorrowLendRequest& request) { impl_->signAndSend(RestEndpointType::BorrowLend, @@ -1346,6 +1366,12 @@ void RestApi::userSetAbstractionAsync(const UserSetAbstractionRequest& request) impl_->exchangeRequestBuilder.userSetAbstraction(request)); } +void RestApi::setReferrerAsync(const SetReferrerRequest& request) +{ + impl_->signAndSend(RestEndpointType::SetReferrer, + impl_->exchangeRequestBuilder.setReferrer(request)); +} + void RestApi::cDepositAsync(uint64_t wei) { impl_->signAndSend(RestEndpointType::CDeposit, impl_->exchangeRequestBuilder.cDeposit(wei)); diff --git a/src/rest/RestApiMessageParser.cpp b/src/rest/RestApiMessageParser.cpp index ecf0155..12de89a 100644 --- a/src/rest/RestApiMessageParser.cpp +++ b/src/rest/RestApiMessageParser.cpp @@ -208,6 +208,7 @@ namespace hyperliquid case RestEndpointType::Withdraw3: case RestEndpointType::ApproveBuilderFee: case RestEndpointType::UserSetAbstraction: + case RestEndpointType::SetReferrer: case RestEndpointType::CDeposit: case RestEndpointType::CWithdraw: case RestEndpointType::TokenDelegate: @@ -228,6 +229,9 @@ namespace hyperliquid case RestEndpointType::CreateSubAccount: listener.onCreateSubAccount(parseCreateSubAccount(message), correlationId); break; + case RestEndpointType::CreateVault: + listener.onCreateVault(parseCreateVault(message), correlationId); + break; case RestEndpointType::Delegations: listener.onDelegations(parseDelegations(message), correlationId); break; @@ -621,6 +625,44 @@ namespace hyperliquid return response; } + CreateVaultResponse parseCreateVault(const std::string& message) + { + CreateVaultResponse response; + padded = simdjson::padded_string(message.data(), message.size()); + auto doc = parser.iterate(padded); + + try + { + validateStructure(message); + + response.status = std::string(doc["status"].get_string().value()); + + if (response.status != "ok") + { + simdjson::ondemand::value resp; + if (doc["response"].get(resp) == simdjson::SUCCESS + && resp.type().value() == simdjson::ondemand::json_type::string) + { + response.error = std::string(resp.get_string().value()); + } + return response; + } + + auto resp = doc["response"].get_object().value(); + response.type = std::string(resp["type"].get_string().value()); + + simdjson::ondemand::value data; + if (resp["data"].get(data) == simdjson::SUCCESS) + response.vaultAddress = std::string(data.get_string().value()); + } + catch (const simdjson::simdjson_error& err) + { + getLogger()->error("RestMessageParser: parse error in createVault: {}\n raw: {}", err.what(), message); + } + + return response; + } + static void parseOutcomeSpec(simdjson::ondemand::object& obj, Outcome& outcome) { outcome.outcome = static_cast(obj["outcome"].get_int64().value()); @@ -3649,6 +3691,11 @@ namespace hyperliquid return impl_->parseCreateSubAccount(message); } + CreateVaultResponse RestApiMessageParser::parseCreateVault(const std::string& message) + { + return impl_->parseCreateVault(message); + } + DelegationsResponse RestApiMessageParser::parseDelegations(const std::string& message) { return impl_->parseDelegations(message); diff --git a/src/signing/Signing.cpp b/src/signing/Signing.cpp index 9693903..1fd77e7 100644 --- a/src/signing/Signing.cpp +++ b/src/signing/Signing.cpp @@ -96,11 +96,12 @@ nlohmann::ordered_json Signing::prepareBody( else { auto action = body["action"]; - // agentSendAsset is an unusual L1 action: the exchange requires the action's own - // nonce field to equal the envelope nonce below, and checks it strictly (server - // rejects the request as a whole with a deserialize error if it's missing), unlike - // every other L1 action here which carries no embedded nonce at all. - if (type == RestEndpointType::AgentSendAsset) action["nonce"] = nonce; + // agentSendAsset and createVault are unusual L1 actions: the exchange requires the + // action's own nonce field to equal the envelope nonce below, and checks it strictly + // (server rejects the request as a whole with a deserialize error if it's missing), + // unlike every other L1 action here which carries no embedded nonce at all. + if (type == RestEndpointType::AgentSendAsset || type == RestEndpointType::CreateVault) + action["nonce"] = nonce; signature = signL1Action( config.wallet.value(), action, vaultAddress, nonce, expiresAfter, isMainnet); body["action"] = action; diff --git a/src/websocket/WebsocketApi.cpp b/src/websocket/WebsocketApi.cpp index f085efe..ee3244a 100644 --- a/src/websocket/WebsocketApi.cpp +++ b/src/websocket/WebsocketApi.cpp @@ -755,6 +755,14 @@ namespace hyperliquid std::nullopt, std::nullopt, correlationId); } + void WebsocketApi::createVault(const CreateVaultRequest& request, + std::optional correlationId) + { + return impl_->signAndSend(RestEndpointType::CreateVault, + impl_->exchangeRequestBuilder.createVault(request), + std::nullopt, std::nullopt, correlationId); + } + void WebsocketApi::usdClassTransfer(const UsdClassTransferRequest& request, std::optional correlationId) { @@ -811,6 +819,14 @@ namespace hyperliquid std::nullopt, std::nullopt, correlationId); } + void WebsocketApi::setReferrer(const SetReferrerRequest& request, + std::optional correlationId) + { + return impl_->signAndSend(RestEndpointType::SetReferrer, + impl_->exchangeRequestBuilder.setReferrer(request), + std::nullopt, std::nullopt, correlationId); + } + void WebsocketApi::cDeposit(uint64_t wei, std::optional correlationId) { diff --git a/tests/rest_vaults_test.cpp b/tests/rest_vaults_test.cpp index cbcc743..bcf9adf 100644 --- a/tests/rest_vaults_test.cpp +++ b/tests/rest_vaults_test.cpp @@ -1,6 +1,7 @@ #include #include "messages/InfoRequestBuilder.h" +#include "messages/ExchangeRequestBuilder.h" #include "hyperliquid/rest/RestApiMessageParser.h" #include @@ -52,6 +53,69 @@ TEST(InfoRequestBuilderTest, UserRole) EXPECT_EQ(body["user"], "0xabc"); } +// --- createVault / setReferrer (exchange, L1 actions) --- +// Field shapes (action = {type, name, description, initialUsd} for createVault, plain L1-action +// signing with initialUsd converted to raw 1e6 USDC units like vaultTransfer's "usd"; action = +// {type, code} for setReferrer) are cross-checked against the official TS SDK +// (@nktkas/hyperliquid, src/api/exchange/_methods/{createVault,setReferrer}.ts), not a live +// capture - please double-check before relying on this. createVault's action also carries its +// own "nonce" field equal to the envelope nonce (injected downstream by Signing::prepareBody, +// see the RestEndpointType::CreateVault special-case next to agentSendAsset's), so it isn't +// present in the builder's own output below. + +TEST(CreateVaultBuilder, BodyShapeAndFieldOrder) +{ + ExchangeRequestBuilder builder; + + CreateVaultRequest request; + request.name = "My Vault"; + request.description = "A vault that does things."; + request.initialUsd = 100.0; + + auto body = builder.createVault(request); + ASSERT_TRUE(body.contains("action")); + const auto& action = body["action"]; + EXPECT_EQ(action["type"], "createVault"); + EXPECT_EQ(action["name"], "My Vault"); + EXPECT_EQ(action["description"], "A vault that does things."); + EXPECT_EQ(action["initialUsd"].get(), 100000000ULL); + + auto keys = std::vector(); + for (auto it = action.begin(); it != action.end(); ++it) keys.push_back(it.key()); + EXPECT_EQ(keys, (std::vector{"type", "name", "description", "initialUsd"})); +} + +TEST(CreateVaultBuilder, InitialUsdScalesFractionalDollarsToRawUnits) +{ + ExchangeRequestBuilder builder; + + CreateVaultRequest request; + request.name = "My Vault"; + request.description = "A vault that does things."; + request.initialUsd = 100.5; + + auto body = builder.createVault(request); + EXPECT_EQ(body["action"]["initialUsd"].get(), 100500000ULL); +} + +TEST(SetReferrerBuilder, BodyShapeAndFieldOrder) +{ + ExchangeRequestBuilder builder; + + SetReferrerRequest request; + request.code = "ABC123"; + + auto body = builder.setReferrer(request); + ASSERT_TRUE(body.contains("action")); + const auto& action = body["action"]; + EXPECT_EQ(action["type"], "setReferrer"); + EXPECT_EQ(action["code"], "ABC123"); + + auto keys = std::vector(); + for (auto it = action.begin(); it != action.end(); ++it) keys.push_back(it.key()); + EXPECT_EQ(keys, (std::vector{"type", "code"})); +} + // --- Response parsing --- // Fixtures below are real payloads captured against Hyperliquid testnet, not fabricated - // several diverged from what the original implementation assumed (see referral tests). @@ -193,3 +257,75 @@ TEST(RestApiMessageParserVaultsTest, ParseReferralWithReferredBy) EXPECT_FALSE(response.referrerState.has_value()); EXPECT_FALSE(response.tokenToState.has_value()); } + +// createVault's success response shape ({"status":"ok","response":{"type":"createVault", +// "data":"0x..."}}) is cross-checked against the official TS SDK's CreateVaultResponse type, +// not a live capture - please double-check before relying on this. +TEST(RestApiMessageParserVaultsTest, ParseCreateVaultSuccess) +{ + std::string message = R"({ + "status": "ok", + "response": { + "type": "createVault", + "data": "0x1719884eb866cb12b2287399b15f7db5e7d775ea" + } + })"; + + RestApiMessageParser parser; + auto response = parser.parseCreateVault(message); + + EXPECT_EQ(response.status, "ok"); + EXPECT_EQ(response.type, "createVault"); + ASSERT_TRUE(response.vaultAddress.has_value()); + EXPECT_EQ(*response.vaultAddress, "0x1719884eb866cb12b2287399b15f7db5e7d775ea"); + EXPECT_FALSE(response.error.has_value()); +} + +TEST(RestApiMessageParserVaultsTest, ParseCreateVaultError) +{ + std::string message = R"({ + "status": "err", + "response": "Initial balance must be at least 100 USD." + })"; + + RestApiMessageParser parser; + auto response = parser.parseCreateVault(message); + + EXPECT_EQ(response.status, "err"); + ASSERT_TRUE(response.error.has_value()); + EXPECT_EQ(*response.error, "Initial balance must be at least 100 USD."); + EXPECT_FALSE(response.vaultAddress.has_value()); +} + +// setReferrer returns SimpleResponse's usual {"status":"ok","response":{"type":"default"}} +// shape, same as vaultTransfer/hip3LiquidatorTransfer above. +TEST(RestApiMessageParserVaultsTest, ParseSetReferrerSuccess) +{ + std::string message = R"({ + "status": "ok", + "response": { + "type": "default" + } + })"; + + RestApiMessageParser parser; + auto response = parser.parseSimpleResponse(message); + + EXPECT_EQ(response.status, "ok"); + EXPECT_FALSE(response.error.has_value()); +} + +TEST(RestApiMessageParserVaultsTest, ParseSetReferrerError) +{ + std::string message = R"({ + "status": "err", + "response": "Referral code does not exist." + })"; + + RestApiMessageParser parser; + auto response = parser.parseSimpleResponse(message); + + EXPECT_EQ(response.status, "err"); + ASSERT_TRUE(response.error.has_value()); + EXPECT_EQ(*response.error, "Referral code does not exist."); +} diff --git a/tests/websocket_api_exchange_test.cpp b/tests/websocket_api_exchange_test.cpp index edd8620..a447575 100644 --- a/tests/websocket_api_exchange_test.cpp +++ b/tests/websocket_api_exchange_test.cpp @@ -102,6 +102,12 @@ TEST(WebsocketApiExchangeBackfill, VaultAndHip3ActionsThrowWithoutWallet) subAccountTransferReq.isDeposit = true; subAccountTransferReq.usd = 10.0; EXPECT_THROW(ws.subAccountTransfer(subAccountTransferReq, 4), std::invalid_argument); + + CreateVaultRequest createVaultReq; + createVaultReq.name = "My Vault"; + createVaultReq.description = "A vault that does things."; + createVaultReq.initialUsd = 100.0; + EXPECT_THROW(ws.createVault(createVaultReq, 5), std::invalid_argument); } TEST(WebsocketApiExchangeBackfill, StakingActionsThrowWithoutWallet) @@ -189,6 +195,10 @@ TEST(WebsocketApiExchangeBackfill, AgentAndAbstractionActionsThrowWithoutWallet) userDexReq.user = "0x5e9ee1089755c3435139848e47e6635505d5a13a"; userDexReq.enabled = false; EXPECT_THROW(ws.userDexAbstraction(userDexReq, 5), std::invalid_argument); + + SetReferrerRequest setReferrerReq; + setReferrerReq.code = "ABC123"; + EXPECT_THROW(ws.setReferrer(setReferrerReq, 6), std::invalid_argument); } TEST(WebsocketApiExchangeBackfill, MiscActionsThrowWithoutWallet) From 84beec6728b177f2c18b5f7b24d873eb3e2cc894 Mon Sep 17 00:00:00 2001 From: TuxedoFish <“harryliversedge@gmail.com”> Date: Tue, 8 Sep 2026 16:56:08 +0100 Subject: [PATCH 2/2] Remove unnecessary comments flagged in review --- include/hyperliquid/rest/RestApi.h | 2 -- include/hyperliquid/types/ResponseTypes.h | 1 - src/messages/ExchangeRequestBuilder.cpp | 4 ---- 3 files changed, 7 deletions(-) diff --git a/include/hyperliquid/rest/RestApi.h b/include/hyperliquid/rest/RestApi.h index 89d978b..94fd5d9 100644 --- a/include/hyperliquid/rest/RestApi.h +++ b/include/hyperliquid/rest/RestApi.h @@ -171,8 +171,6 @@ class RestApi { SimpleResponse withdraw3(const Withdraw3Request& request); SimpleResponse approveBuilderFee(const ApproveBuilderFeeRequest& request); SimpleResponse userSetAbstraction(const UserSetAbstractionRequest& request); - // setReferrer sets a referral code for the calling wallet itself, so like the transfer-style - // actions above it does not take a vaultAddress parameter. SimpleResponse setReferrer(const SetReferrerRequest& request); SimpleResponse cDeposit(uint64_t wei); SimpleResponse cWithdraw(uint64_t wei); diff --git a/include/hyperliquid/types/ResponseTypes.h b/include/hyperliquid/types/ResponseTypes.h index 45decb3..0eb9b5a 100644 --- a/include/hyperliquid/types/ResponseTypes.h +++ b/include/hyperliquid/types/ResponseTypes.h @@ -1543,7 +1543,6 @@ namespace hyperliquid { std::string status; std::string type; - // The newly created vault's address (response.data on success). std::optional vaultAddress; std::optional error; }; diff --git a/src/messages/ExchangeRequestBuilder.cpp b/src/messages/ExchangeRequestBuilder.cpp index 15adddf..89da832 100644 --- a/src/messages/ExchangeRequestBuilder.cpp +++ b/src/messages/ExchangeRequestBuilder.cpp @@ -433,10 +433,6 @@ namespace hyperliquid action["description"] = request.description; // Same raw-1e6-unit USDC representation as vaultTransfer's "usd" field above. action["initialUsd"] = static_cast(std::llround(request.initialUsd * 1e6)); - // createVault is the one L1 action (besides agentSendAsset) whose action carries its own - // "nonce" field, required to equal the envelope nonce - injected downstream by - // Signing::prepareBody, not here (see the RestEndpointType::CreateVault special-case - // next to AgentSendAsset's). nlohmann::ordered_json body; body["action"] = action;