Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Legend: ✅ implemented — ⬜ not yet implemented.
| `authorizeAqav2Role` | ⬜ | |
| `claimRewards` | ⬜ | |
| `convertToMultiSigUser` | ⬜ | |
| `createSubAccount` | ⬜ | |
| `createSubAccount` | ✅ | `RestApi::createSubAccount` |
| `createVault` | ⬜ | |
| `cSignerAction` | ⬜ | |
| `cValidatorAction` | ⬜ | |
Expand All @@ -228,7 +228,7 @@ Legend: ✅ implemented — ⬜ not yet implemented.
| `stakingLinkDisableTradingUser` | ⬜ | |
| `subAccountModify` | ⬜ | |
| `subAccountSpotTransfer` | ⬜ | |
| `subAccountTransfer` | ⬜ | |
| `subAccountTransfer` | ✅ | `RestApi::subAccountTransfer` |
| `topUpIsolatedOnlyMargin` | ⬜ | |
| `userOutcome` | ⬜ | |
| `userPortfolioMargin` | ⬜ | |
Expand Down
43 changes: 43 additions & 0 deletions examples/rest_sub_account.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "test_config.h"

#include <hyperliquid/rest/RestApi.h>
#include <hyperliquid/config/Config.h>
#include <spdlog/spdlog.h>

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::CreateSubAccountRequest createReq;
createReq.name = "example-sub-account";

auto createResp = api.createSubAccount(createReq);
spdlog::info("createSubAccount: status={} type={}", createResp.status, createResp.type);
if (createResp.error)
spdlog::info(" Error: {}", *createResp.error);
if (!createResp.subAccountUser)
{
spdlog::error("createSubAccount did not return a sub-account address, aborting");
return 1;
}
spdlog::info(" New sub-account address: {}", *createResp.subAccountUser);

hyperliquid::SubAccountTransferRequest transferReq;
transferReq.subAccountUser = *createResp.subAccountUser;
transferReq.isDeposit = true;
transferReq.usd = 1.0;

auto transferResp = api.subAccountTransfer(transferReq);
spdlog::info("subAccountTransfer (deposit): status={} type={}", transferResp.status, transferResp.type);
if (transferResp.error)
spdlog::info(" Error: {}", *transferResp.error);

return 0;
}
55 changes: 55 additions & 0 deletions examples/ws_sub_account.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include <chrono>
#include <thread>

#include <spdlog/spdlog.h>

#include "test_config.h"
#include "hyperliquid/websocket/WebsocketApi.h"
#include "hyperliquid/websocket/WebsocketApiListener.h"

class PostResponseLogger : public hyperliquid::WebsocketApiListener
{
public:
void onPostResponse(const std::string& rawJson, hyperliquid::RestEndpointType type,
std::optional<uint64_t> correlationId) override
{
spdlog::info("[post response] type={} correlationId={} payload={}",
hyperliquid::toString(type), correlationId.value_or(0), rawJson);
}

void onConnected() override
{
spdlog::info("Connected");
}
};

int main()
{
auto wallet = loadWalletFromConfig();
hyperliquid::setLogLevel(hyperliquid::LogLevel::Debug);

hyperliquid::ApiConfig config;
config.env = hyperliquid::Environment::Testnet;
config.wallet = wallet;

PostResponseLogger logger;
hyperliquid::WebsocketApi ws(config, logger);

ws.start();
std::this_thread::sleep_for(std::chrono::seconds(2));

uint64_t correlationId = 1;
auto wait = []() { std::this_thread::sleep_for(std::chrono::seconds(3)); };

hyperliquid::CreateSubAccountRequest createReq;
createReq.name = "example-sub-account";
ws.createSubAccount(createReq, correlationId++); wait();

// The websocket post-response path only reports a generic SimpleResponse for exchange
// actions (see PostResponseDispatch.cpp), so the new sub-account's address isn't available
// here - check the logged raw payload, or use RestApi::createSubAccount for the typed
// CreateSubAccountResponse with subAccountUser populated.

ws.stop();
return 0;
}
14 changes: 10 additions & 4 deletions include/hyperliquid/rest/RestApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,16 @@ class RestApi {
const std::optional<std::string>& vaultAddress = std::nullopt);
TwapCancelResponse twapCancel(const TwapCancelRequest& request,
const std::optional<std::string>& vaultAddress = std::nullopt);
// vaultTransfer/hip3LiquidatorTransfer/borrowLend/usdClassTransfer/sendAsset/usdSend/spotSend/
// withdraw3/approveBuilderFee move funds against the calling wallet directly (their target
// vault/dex/destination/etc. is a field of the request itself), so unlike the other exchange
// methods they do not take a vaultAddress parameter.
// vaultTransfer/hip3LiquidatorTransfer/createSubAccount/subAccountTransfer/borrowLend/
// usdClassTransfer/sendAsset/usdSend/spotSend/withdraw3/approveBuilderFee move funds (or
// manage sub-accounts) 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.
SimpleResponse vaultTransfer(const VaultTransferRequest& request);
SimpleResponse hip3LiquidatorTransfer(const Hip3LiquidatorTransferRequest& request);
CreateSubAccountResponse createSubAccount(const CreateSubAccountRequest& request);
SimpleResponse subAccountTransfer(const SubAccountTransferRequest& request);
SimpleResponse borrowLend(const BorrowLendRequest& request);
SimpleResponse spotDeployRegisterToken2(const SpotDeployRegisterToken2Request& request);
SimpleResponse spotDeployUserGenesis(const SpotDeployUserGenesisRequest& request);
Expand Down Expand Up @@ -288,6 +292,8 @@ class RestApi {
const std::optional<std::string>& vaultAddress = std::nullopt);
void vaultTransferAsync(const VaultTransferRequest& request);
void hip3LiquidatorTransferAsync(const Hip3LiquidatorTransferRequest& request);
void createSubAccountAsync(const CreateSubAccountRequest& request);
void subAccountTransferAsync(const SubAccountTransferRequest& request);
void borrowLendAsync(const BorrowLendRequest& request);
void spotDeployRegisterToken2Async(const SpotDeployRegisterToken2Request& request);
void spotDeployUserGenesisAsync(const SpotDeployUserGenesisRequest& request);
Expand Down
1 change: 1 addition & 0 deletions include/hyperliquid/rest/RestApiMessageParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ namespace hyperliquid
SimpleResponse parseSimpleResponse(const std::string& message);
TwapOrderResponse parseTwapOrder(const std::string& message);
TwapCancelResponse parseTwapCancel(const std::string& message);
CreateSubAccountResponse parseCreateSubAccount(const std::string& message);
DelegationsResponse parseDelegations(const std::string& message);
DelegatorSummaryResponse parseDelegatorSummary(const std::string& message);
DelegatorHistoryResponse parseDelegatorHistory(const std::string& message);
Expand Down
1 change: 1 addition & 0 deletions include/hyperliquid/rest/RestEndpointListener.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class RestEndpointListener {
virtual void onSimpleResponse(const SimpleResponse&, std::optional<uint64_t> = std::nullopt) {}
virtual void onTwapOrder(const TwapOrderResponse&, std::optional<uint64_t> = std::nullopt) {}
virtual void onTwapCancel(const TwapCancelResponse&, std::optional<uint64_t> = std::nullopt) {}
virtual void onCreateSubAccount(const CreateSubAccountResponse&, std::optional<uint64_t> = std::nullopt) {}
virtual void onDelegations(const DelegationsResponse&, std::optional<uint64_t> = std::nullopt) {}
virtual void onDelegatorSummary(const DelegatorSummaryResponse&, std::optional<uint64_t> = std::nullopt) {}
virtual void onDelegatorHistory(const DelegatorHistoryResponse&, std::optional<uint64_t> = std::nullopt) {}
Expand Down
18 changes: 18 additions & 0 deletions include/hyperliquid/types/RequestTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ namespace hyperliquid
TwapCancel,
VaultTransfer,
Hip3LiquidatorTransfer,
CreateSubAccount,
SubAccountTransfer,
BorrowLend,
SpotDeployRegisterToken2,
SpotDeployUserGenesis,
Expand Down Expand Up @@ -333,6 +335,8 @@ namespace hyperliquid
case RestEndpointType::TwapCancel: return "twapCancel";
case RestEndpointType::VaultTransfer: return "vaultTransfer";
case RestEndpointType::Hip3LiquidatorTransfer: return "hip3LiquidatorTransfer";
case RestEndpointType::CreateSubAccount: return "createSubAccount";
case RestEndpointType::SubAccountTransfer: return "subAccountTransfer";
case RestEndpointType::BorrowLend: return "borrowLend";
case RestEndpointType::SpotDeployRegisterToken2: return "spotDeploy";
case RestEndpointType::SpotDeployUserGenesis: return "spotDeploy";
Expand Down Expand Up @@ -441,6 +445,8 @@ namespace hyperliquid
case RestEndpointType::TwapCancel: return true;
case RestEndpointType::VaultTransfer: return true;
case RestEndpointType::Hip3LiquidatorTransfer: return true;
case RestEndpointType::CreateSubAccount: return true;
case RestEndpointType::SubAccountTransfer: return true;
case RestEndpointType::BorrowLend: return true;
case RestEndpointType::SpotDeployRegisterToken2: return true;
case RestEndpointType::SpotDeployUserGenesis: return true;
Expand Down Expand Up @@ -673,6 +679,18 @@ namespace hyperliquid
bool isDeposit;
};

struct CreateSubAccountRequest
{
std::string name;
};

struct SubAccountTransferRequest
{
std::string subAccountUser;
bool isDeposit;
double usd;
};

enum class BorrowLendOperation { Supply, Withdraw, Repay, Borrow };

inline std::string toString(BorrowLendOperation operation)
Expand Down
8 changes: 8 additions & 0 deletions include/hyperliquid/types/ResponseTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,14 @@ namespace hyperliquid
std::optional<std::string> error;
};

struct CreateSubAccountResponse
{
std::string status;
std::string type;
std::optional<std::string> subAccountUser;
std::optional<std::string> error;
};

// --- Staking / delegation types ---

struct Delegation
Expand Down
6 changes: 6 additions & 0 deletions include/hyperliquid/websocket/WebsocketApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ namespace hyperliquid
void hip3LiquidatorTransfer(const Hip3LiquidatorTransferRequest& request,
std::optional<uint64_t> correlationId = std::nullopt);

void createSubAccount(const CreateSubAccountRequest& request,
std::optional<uint64_t> correlationId = std::nullopt);

void subAccountTransfer(const SubAccountTransferRequest& request,
std::optional<uint64_t> correlationId = std::nullopt);

void usdClassTransfer(const UsdClassTransferRequest& request,
std::optional<uint64_t> correlationId = std::nullopt);
void sendAsset(const SendAssetRequest& request,
Expand Down
27 changes: 27 additions & 0 deletions src/messages/ExchangeRequestBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,33 @@ namespace hyperliquid
return body;
}

nlohmann::ordered_json ExchangeRequestBuilder::createSubAccount(const CreateSubAccountRequest& request) const
{
nlohmann::ordered_json action;
action["type"] = "createSubAccount";
action["name"] = request.name;

nlohmann::ordered_json body;
body["action"] = action;
return body;
}

nlohmann::ordered_json ExchangeRequestBuilder::subAccountTransfer(const SubAccountTransferRequest& request) const
{
nlohmann::ordered_json action;
action["type"] = "subAccountTransfer";
action["subAccountUser"] = request.subAccountUser;
action["isDeposit"] = request.isDeposit;
// Like vaultTransfer, this is a plain L1 action whose usd field mirrors USDC's own
// on-chain representation: raw integer units at USDC's 6 decimals, so $5 is sent as
// 5_000_000.
action["usd"] = static_cast<uint64_t>(std::llround(request.usd * 1e6));

nlohmann::ordered_json body;
body["action"] = action;
return body;
}

nlohmann::ordered_json ExchangeRequestBuilder::borrowLend(const BorrowLendRequest& request) const
{
nlohmann::ordered_json action;
Expand Down
4 changes: 4 additions & 0 deletions src/messages/ExchangeRequestBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class ExchangeRequestBuilder {

nlohmann::ordered_json hip3LiquidatorTransfer(const Hip3LiquidatorTransferRequest& request) const;

nlohmann::ordered_json createSubAccount(const CreateSubAccountRequest& request) const;

nlohmann::ordered_json subAccountTransfer(const SubAccountTransferRequest& request) const;

nlohmann::ordered_json borrowLend(const BorrowLendRequest& request) const;

nlohmann::ordered_json spotDeployRegisterToken2(const SpotDeployRegisterToken2Request& request) const;
Expand Down
26 changes: 26 additions & 0 deletions src/rest/RestApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,20 @@ SimpleResponse RestApi::hip3LiquidatorTransfer(const Hip3LiquidatorTransferReque
impl_->exchangeRequestBuilder.hip3LiquidatorTransfer(request)));
}

CreateSubAccountResponse RestApi::createSubAccount(const CreateSubAccountRequest& request)
{
return RestApiMessageParser().parseCreateSubAccount(
impl_->signAndSendSync(RestEndpointType::CreateSubAccount,
impl_->exchangeRequestBuilder.createSubAccount(request)));
}

SimpleResponse RestApi::subAccountTransfer(const SubAccountTransferRequest& request)
{
return RestApiMessageParser().parseSimpleResponse(
impl_->signAndSendSync(RestEndpointType::SubAccountTransfer,
impl_->exchangeRequestBuilder.subAccountTransfer(request)));
}

SimpleResponse RestApi::borrowLend(const BorrowLendRequest& request)
{
return RestApiMessageParser().parseSimpleResponse(
Expand Down Expand Up @@ -1218,6 +1232,18 @@ void RestApi::hip3LiquidatorTransferAsync(const Hip3LiquidatorTransferRequest& r
impl_->exchangeRequestBuilder.hip3LiquidatorTransfer(request));
}

void RestApi::createSubAccountAsync(const CreateSubAccountRequest& request)
{
impl_->signAndSend(RestEndpointType::CreateSubAccount,
impl_->exchangeRequestBuilder.createSubAccount(request));
}

void RestApi::subAccountTransferAsync(const SubAccountTransferRequest& request)
{
impl_->signAndSend(RestEndpointType::SubAccountTransfer,
impl_->exchangeRequestBuilder.subAccountTransfer(request));
}

void RestApi::borrowLendAsync(const BorrowLendRequest& request)
{
impl_->signAndSend(RestEndpointType::BorrowLend,
Expand Down
47 changes: 47 additions & 0 deletions src/rest/RestApiMessageParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ namespace hyperliquid
case RestEndpointType::AgentSetAbstraction:
case RestEndpointType::VaultTransfer:
case RestEndpointType::Hip3LiquidatorTransfer:
case RestEndpointType::SubAccountTransfer:
case RestEndpointType::BorrowLend:
case RestEndpointType::SpotDeployRegisterToken2:
case RestEndpointType::SpotDeployUserGenesis:
Expand Down Expand Up @@ -223,6 +224,9 @@ namespace hyperliquid
case RestEndpointType::TwapCancel:
listener.onTwapCancel(parseTwapCancel(message), correlationId);
break;
case RestEndpointType::CreateSubAccount:
listener.onCreateSubAccount(parseCreateSubAccount(message), correlationId);
break;
case RestEndpointType::Delegations:
listener.onDelegations(parseDelegations(message), correlationId);
break;
Expand Down Expand Up @@ -575,6 +579,44 @@ namespace hyperliquid
return response;
}

CreateSubAccountResponse parseCreateSubAccount(const std::string& message)
{
CreateSubAccountResponse 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());

std::string_view data;
if (!resp["data"].get_string().get(data))
response.subAccountUser = std::string(data);
}
catch (const simdjson::simdjson_error& err)
{
getLogger()->error("RestMessageParser: parse error in createSubAccount: {}\n raw: {}", err.what(), message);
}

return response;
}

static void parseOutcomeSpec(simdjson::ondemand::object& obj, Outcome& outcome)
{
outcome.outcome = static_cast<int>(obj["outcome"].get_int64().value());
Expand Down Expand Up @@ -3566,6 +3608,11 @@ namespace hyperliquid
return impl_->parseTwapCancel(message);
}

CreateSubAccountResponse RestApiMessageParser::parseCreateSubAccount(const std::string& message)
{
return impl_->parseCreateSubAccount(message);
}

DelegationsResponse RestApiMessageParser::parseDelegations(const std::string& message)
{
return impl_->parseDelegations(message);
Expand Down
Loading
Loading