Refactor/ux api#1
Open
Simon-Busch wants to merge 112 commits into
Open
Conversation
- UpdateLeverage -> SetLeverage with MarginMode enum (Cross/Isolated)
- UpdateIsolatedMargin -> AdjustMargin
- ScheduleCancel -> ScheduleCancelAll taking *time.Time
- ApproveAgent returns typed Agent{Address, PrivateKey *ecdsa.PrivateKey}
…quirks
Tighten the integration suite so it passes on a funded testnet/mainnet
account without hardcoded magic constants:
* testSizeForLimit derives a coin-unit size from HL_TEST_NOTIONAL and a
caller-supplied limit price, snapping UP to MinSize so the order
always clears Hyperliquid's $10 minimum notional. Used by every test
that places far from mid.
* snapPrice now rounds to five significant figures BEFORE snapping to
tick, so tick-aligned prices like 1250.25 (6 sig figs) no longer
smuggle past validateSignificantFigures.
* awaitPosition polls until a freshly-opened position propagates, so
ClosePosition / Validation scenarios don't race against API latency.
* Modify reads the price from the live order book instead of a magic
fraction of mid.
* Skip cleanly on two environment quirks instead of failing:
- 'Order price cannot be more than 95% away from the reference
price' on testnet (oracle/index not anchored to quotes).
- 'Too many extra agents for cumulative volume traded' once the
account exhausts its agent cap.
* PostInfo response peeling: try the direct shape first, then peel a
layer of data/payload/meta/response to find the universe array. Logs
the raw response truncated on mismatch.
* ApproveAgent name fits Hyperliquid's 16-char cap.
TestHIP4_YesFlow exercises the full HIP-4 trading lifecycle on a single outcome: find a live YES-side market (SideSpecs[0]), buy via market IOC, hold for 10 seconds, then sell the exact filled size back to flatten. HIP-4 contracts are tracked outside UserState.AssetPositions in a way that the SDK currently does not expose; the test logs every candidate state field (Info.Position by canonical and friendly name, all AssetPositions, SpotBalances, raw UserState JSON) so the next run makes clear where the position actually lands. The test budget is capped at $5 USDH via hip4MaxNotional. Hyperliquid enforces a $10 USDH minimum order value on outcome markets; when the venue minimum exceeds the budget at the current YES price the test skips with a clear top-up-or-pick-cheaper-outcome message rather than failing. Adds requireYesOutcomeOrSkip helper alongside the existing requireOutcomeOrSkip — same shape but restricted to SideSpecs[0] (documented as the YES side).
Adds a Convert subgroup mirroring Transfer/SubAccount/Stake/MultiSig:
c.Trade.Convert.USDCToUSDH(usdcAmount float64) (Result, error)
c.Trade.Convert.USDHToUSDC(usdhAmount float64) (Result, error)
The conversion is an IOC market trade on the USDH/USDC spot pair —
Hyperliquid does not expose a dedicated token-swap endpoint, so the
implementation uses the existing PlaceMarket path with a 5% slippage
budget.
* USDCToUSDH takes a QUOTE-side amount (the USDC notional to spend)
and derives the base-side size from the current mid.
* USDHToUSDC takes a BASE-side amount (USDH to liquidate) — natural
when the caller knows the holding they want to sell.
* Both routes snap the computed size DOWN to the pair's MinSize step
via the new snapSpotSize helper, so the validator's
size_step_violation rule does not fire when a balance like
19.645621 USDH is passed verbatim.
* findSpotPair looks the pair up via SpotMeta tokens, so the helper
works for any base/quote combination the venue exposes — USDH/USDC
today, other pairs later.
Adds TestConvert_USDCToUSDH and TestConvert_USDHToUSDC integration
scenarios. The buy side skips when spot USDC is below the venue's $10
minimum; the sell side runs whenever the account holds any USDH and
caps the liquidation at $20 to guard against fat-fingered configs.
…d, sell
End-to-end scenario that exercises the entire HIP-4 trading lifecycle
described in the user flow:
1. Convert $15 USDC -> USDH via the new Trader.Convert helper.
2. Find an outcome with live mids on BOTH SideSpecs[0] (YES) and
SideSpecs[1] (NO).
3. Buy YES at market for ~$10 USDH (sized up to clear the venue's
$10 minimum at the current mid).
4. Hold 10s.
5. Sell the exact filled YES size back to flatten.
6. Buy NO at market on the same outcome for ~$10 USDH.
7. Hold 10s.
8. Sell the exact filled NO size back.
The scenario is intentionally expensive (two round-trips at venue
minimum). It skips cleanly when neither USDC nor USDH covers the
cycle, when no outcome has both sides live, or when the venue rejects
a leg. Defensive cleanups flatten any side that survives a mid-cycle
fatal.
TestHIP3_ShortFullCycle exercises the user-described HIP-3 flow on a
builder-deployed perp:
1. Build a client pinned to the HIP-3 dex (HL_HIP3_DEX, e.g. "xyz").
2. Resolve the coin under test (HL_HIP3_COIN, e.g. "COPPER").
3. Ensure the dex-pinned perp wallet has ~$11 of headroom; otherwise
move USDC in from the main perp account via Transfer.MoveToDex.
4. SHORT (Sell at market) for ~$10 notional, sized up to clear the
venue's $10 minimum order value.
5. Wait for the position to surface in UserState(addr, dex).
6. Hold 10 seconds.
7. ClosePosition — auto-direction picks Buy to flatten the short.
8. Assert the dex-pinned position is flat.
Cleanups defensively flatten any leftover position and move the seed
USDC back out of the dex if the test moved it in. Skips cleanly when
HL_HIP3_DEX is unset, when no coin can be picked, when neither the
dex wallet nor the main perp wallet has the budget, or when the IOC
short does not fill against a thin book.
Adds awaitPositionOnDex helper alongside awaitPosition: the default
perp variant does not pass the dex parameter to Info.UserState and
therefore cannot see HIP-3 positions.
…in order action
Multiple SDK fixes uncovered while bringing TestHIP3_ShortFullCycle
end-to-end against the xyz / xyz:COPPER builder dex on mainnet:
* sendAsset replaces the broken perpDexClassTransfer action. The
upstream wire shape Hyperliquid actually accepts is a user-signed
sendAsset with fields {destination, sourceDex, destinationDex,
token, amount, fromSubAccount, nonce}; sourceDex/destinationDex use
the empty string for the default perp class. Transfer.MoveToDex and
MoveFromDex now compose this action correctly.
* Info.Mid auto-routes by the "<dex>:" prefix on the coin name.
HIP-3 builder coins ("xyz:COPPER") only appear in AllMids when the
dex is supplied; without auto-routing every HIP-3 placement and
test-helper mid lookup failed with "no mid for ...".
* Trader.SlippagePrice applies the same auto-routing, so PlaceMarket
on a HIP-3 coin no longer fails inside the slippage calculation.
* NewInfo fetches Meta(perpDexName) instead of the default-dex Meta
when the client is pinned to a builder dex. Without this the asset
registration loop iterated the default universe, leaving the
builder coins absent from coinToAsset and producing 'unknown_coin'
validation errors.
* Trader.RefreshState fetches UserState(addr, dex) when pinned to a
builder dex. Without it the cached state was always empty for HIP-3
positions, so the long/short reduce-only checks and ClosePosition's
auto-direction silently no-op'd.
* OrderAction / ModifyAction / CancelAction / CancelByCloidAction no
longer carry a 'dex' field on the wire. HIP-3 routing is encoded in
the asset id itself; the extra field made Hyperliquid reject the
envelope with 'Failed to deserialize the JSON body into the target
type'.
* The test helper 'mid' delegates to Info.Mid so it inherits the
prefix routing automatically.
* http_api.post writes the outbound JSON to stderr when
HL_DEBUG_HTTP=true. The flag is off by default; useful when a
future action shape needs another wire-level inspection.
The cancel APIs were returning (CancelResult{Status: '{"error":...}'}, nil)
when the venue reported a per-order failure (already cancelled, filled,
or never placed). Callers relying on err == nil to mean success silently
treated dead-order cancels as no-ops.
cancelResultOrError now decodes the first per-order status: a plain
string ('success') keeps the previous shape, an object with an 'error'
field returns the venue's message wrapped as a Go error and populates
CancelResult.Error so both errors.Is/As style branching and direct
field inspection work.
OutcomeMeta.Questions was typed []any, which made multi-bucket markets
like "BTC price range on May 20 at 8:00 AM" — three mutually-exclusive
price buckets exposed as separate binary outcomes (67/68/69) grouped
under question 12 — opaque from Go.
New surface:
* type Question { Question, Name, Description, FallbackOutcome,
NamedOutcomes, SettledNamedOutcomes } replaces []any in OutcomeMeta.
* ParseOutcomeDescription decodes the pipe-delimited "k:v|k:v" form
Hyperliquid uses in OutcomeInfo and Question descriptions.
* Question.BucketLabels derives ["Below T1", "T1 to T2", ...,
"Above Tn"] for class:priceBucket questions whose threshold count
+ 1 matches the NamedOutcomes count.
* Question.Buckets returns Bucket{Outcome, Label, YesCanonical,
NoCanonical} for every named outcome, ready for PlaceMarket.
* OutcomeMeta.FindQuestion / BucketLabel / QuestionByName navigate
from an outcome id back to its parent question.
* Info caches OutcomeMeta at construction; Info.OutcomeMetaCached
exposes it without an extra HTTP round-trip.
* NewInfo's outcome registration now writes an extra friendly entry
"<question name>:<bucket label>:<Yes|No>" for outcomes owned by
a question. Previously all three BTC price-range buckets registered
the same colliding friendly name ("Recurring Named Outcome:Yes"),
so two of the three were unreachable by friendly lookup.
Tests:
* TestHIP4_MultiBucketDiagnostic walks OutcomeMeta.Questions, dumps
every bucket's YES/NO canonical, friendly registration, mid, and
L2 top of book. Verified live against the BTC May-20 question
(#670 / #680 / #690).
* TestHIP4_MultiBucketRoundTrip picks the highest-mid bucket and
runs a buy-hold-sell cycle. Per-test USDH budget is bumped to
$11 because every HIP-4 order must clear the $10 venue minimum.
Unit-test fixture follow-up: result_test.go's three TestFirstCancelResult
cases are renamed and adapted for cancelResultOrError's new
(CancelResult, error) signature, plus a new case asserts the typed
error path for {"error": ...} per-order statuses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.