Skip to content

Exchange endpoint: createSubAccount, subAccountTransfer - #153

Merged
TuxedoFish merged 1 commit into
mainfrom
endpoint/create-subaccount-transfer
Sep 8, 2026
Merged

Exchange endpoint: createSubAccount, subAccountTransfer#153
TuxedoFish merged 1 commit into
mainfrom
endpoint/create-subaccount-transfer

Conversation

@TuxedoFish

@TuxedoFish TuxedoFish commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #121.

Adds the two missing /exchange actions that complete sub-account support (the SDK already has the read-side subAccounts endpoint, but no way to create a sub-account or move funds into/out of one):

  • createSubAccount - RestApi::createSubAccount / WebsocketApi::createSubAccount. Returns a CreateSubAccountResponse with the new sub-account's address in subAccountUser.
  • subAccountTransfer - RestApi::subAccountTransfer / WebsocketApi::subAccountTransfer. Deposit/withdraw USDC into/from a sub-account (dollars in the request struct, converted to raw 1e6 USDC units in the builder, same as vaultTransfer).

Both are plain L1 actions (signed with the master/agent key directly, not EIP-712 user-signed), and neither takes a vaultAddress parameter - createSubAccount has no vault-like target at all, and subAccountTransfer already names its target via subAccountUser, same reasoning already applied to vaultTransfer/hip3LiquidatorTransfer in this codebase. Also excluded both from ApiConfig::vaultAddress fallback in Signing::prepareBodyForType for the same reason.

Field-shape verification - now run against live testnet

Ran both actions live against testnet using a real signed request from this repo's test wallet:

createSubAccount - real testnet request/response

action: {"type":"createSubAccount","name":"claude-verify-a4edd7d9"}
response: status=err, error="Cannot create sub-accounts until enough volume traded. Required: $100000. Traded: $5578.19."

This confirms the request shape is correct - the server parsed and understood it, then rejected it on a legitimate business rule (this wallet's trading volume), not a schema/deserialization error. It also confirms the error-path response parsing works: CreateSubAccountResponse::status/error were populated correctly from a real error payload. The success-path response shape (response.data = the new address) still couldn't be confirmed live, since no wallet with $100k+ traded volume was available to actually create one - it remains cross-checked against the TS SDK only, as before. Flagging this rather than overstating full coverage.

subAccountTransfer - real testnet attempt, inconclusive

Attempted a transfer to a placeholder address (no real sub-account existed to target, since createSubAccount above didn't succeed):

action: {"type":"subAccountTransfer","subAccountUser":"0x0000000000000000000000000000000000000a","isDeposit":true,"usd":1000000}
response (raw body, not JSON): "Failed to deserialize the JSON body into the target type"

This is not evidence of a bug in our request shape - independently re-confirmed usd's type against the TS SDK source (UnsignedInteger, "amount * 1e6"), which matches what we send exactly. The far more likely explanation is that 0x000...a isn't a real, registered sub-account of this wallet (it's a placeholder, since no sub-account could be created to test against), and the server's address-ownership validation appears to fail before reaching its normal JSON-wrapped error response - hence the raw-text body instead of the usual {"status":"err",...} shape. Our client-side handling of this degraded gracefully (caught, logged, returned as status="err"), which is the existing shared SimpleResponse parsing behavior, not something new to this PR.

Net effect: subAccountTransfer could not be end-to-end confirmed against a real successful transfer, for the same underlying reason as createSubAccount's success path - this test wallet can't create/access a real sub-account without $100k of trading volume. The request shape is independently confirmed correct against the TS SDK; the full round-trip (a real deposit into a real sub-account) is not.

Changes

  • include/hyperliquid/types/RequestTypes.h: RestEndpointType::CreateSubAccount/SubAccountTransfer (+ toString/isAuthenticated), CreateSubAccountRequest/SubAccountTransferRequest structs.
  • include/hyperliquid/types/ResponseTypes.h: CreateSubAccountResponse (subAccountTransfer reuses the existing generic SimpleResponse).
  • src/messages/ExchangeRequestBuilder.h/.cpp: builders for both actions.
  • include/hyperliquid/rest/RestApiMessageParser.h/.cpp: parseCreateSubAccount.
  • include/hyperliquid/rest/RestEndpointListener.h: onCreateSubAccount callback.
  • include/hyperliquid/rest/RestApi.h/src/rest/RestApi.cpp: sync + async methods.
  • include/hyperliquid/websocket/WebsocketApi.h/src/websocket/WebsocketApi.cpp: websocket wrappers via the generic signAndSend.
  • src/signing/Signing.cpp: excluded both action types from the ApiConfig::vaultAddress fallback.
  • tests/transfers_test.cpp: request-builder tests (body shape, usd scaling, reference-signature match) and response-parser tests for both actions.
  • tests/websocket_api_exchange_test.cpp: wallet-required smoke test.
  • examples/rest_sub_account.cpp, examples/ws_sub_account.cpp: new testnet examples (create a sub-account, then deposit into it).
  • README.md: marked both rows done in the exchange-actions coverage table.

Test plan

  • cmake -S . -B build -DHYPERLIQUID_WARNINGS_AS_ERRORS=ON -DHYPERLIQUID_BUILD_TESTS=ON -DHYPERLIQUID_BUILD_EXAMPLES=ON -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" - clean configure
  • cmake --build build -j "$(nproc)" - zero warnings/errors
  • ctest --test-dir build - 100% (28/28 test suites) passing
  • Rebased onto latest main (post-Info endpoints: userBorrowLendInterest, liquidatable #154/Info endpoints: twapHistory, userTwapSliceFillsByTime, activeAssetData #157) - clean rebase, no conflicts, full rebuild/retest after rebasing
  • Ran both actions live against testnet with a real signed request (see "Field-shape verification" above) - createSubAccount's request/error-path fully confirmed; both actions' success paths remain unconfirmed since this wallet can't clear the $100k trading-volume gate to create a real sub-account to test against

🤖 Generated with Claude Code

https://claude.ai/code/session_018Le73N2EZjGLsCqmCScAse

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.11382% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.02%. Comparing base (6c2c7b6) to head (1122ab2).

Files with missing lines Patch % Lines
src/rest/RestApi.cpp 0.00% 14 Missing ⚠️
src/rest/RestApiMessageParser.cpp 80.00% 5 Missing ⚠️
src/websocket/WebsocketApi.cpp 75.00% 2 Missing ⚠️
include/hyperliquid/rest/RestEndpointListener.h 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #153      +/-   ##
==========================================
+ Coverage   69.84%   70.02%   +0.17%     
==========================================
  Files          53       53              
  Lines        8517     8640     +123     
  Branches      531      533       +2     
==========================================
+ Hits         5949     6050     +101     
- Misses       2565     2587      +22     
  Partials        3        3              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added the ready for review CI passed; ready to be reviewed label Sep 8, 2026
Adds the two missing /exchange actions for sub-account management
(issue #121): createSubAccount (returns the new sub-account's
address) and subAccountTransfer (deposit/withdraw USDC into/from a
sub-account). Both are L1 actions signed with the master/agent key
directly, and neither takes a vaultAddress parameter since they
either have no vault-like target (createSubAccount) or already name
one via subAccountUser (subAccountTransfer) - same rule already
applied to vaultTransfer/hip3LiquidatorTransfer.

Field shapes were cross-checked against the official
hyperliquid-python-sdk (create_sub_account/sub_account_transfer) and
the nktkas/hyperliquid TS SDK's CreateSubAccountRequest/
SubAccountTransferRequest schemas, plus the pre-existing
CreateSubAccountAction/SubAccountTransferAction reference signature
vectors in signing_test.cpp.

Wires up RestApi::createSubAccount/subAccountTransfer (sync + async),
WebsocketApi::createSubAccount/subAccountTransfer, request-builder
and response-parser tests in transfers_test.cpp, a wallet-required
smoke test in websocket_api_exchange_test.cpp, testnet examples, and
marks both rows done in the README coverage table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Le73N2EZjGLsCqmCScAse
@TuxedoFish
TuxedoFish force-pushed the endpoint/create-subaccount-transfer branch from e810a09 to 1122ab2 Compare September 8, 2026 12:48
@TuxedoFish
TuxedoFish merged commit cec0144 into main Sep 8, 2026
12 checks passed
@TuxedoFish
TuxedoFish deleted the endpoint/create-subaccount-transfer branch September 8, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review CI passed; ready to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement createSubAccount / subAccountTransfer

1 participant