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
2 changes: 1 addition & 1 deletion skills/hyperliquid-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hyperliquid-plugin",
"description": "Hyperliquid on-chain perpetuals DEX — check positions, get market prices, place and cancel perpetual orders on Hyperliquid L1 (chain_id 999).",
"version": "0.4.4",
"version": "0.4.5",
"author": {
"name": "GeoGu360",
"github": "GeoGu360"
Expand Down
2 changes: 1 addition & 1 deletion skills/hyperliquid-plugin/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skills/hyperliquid-plugin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hyperliquid-plugin"
version = "0.4.4"
version = "0.4.5"
edition = "2021"

[[bin]]
Expand Down
35 changes: 25 additions & 10 deletions skills/hyperliquid-plugin/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: hyperliquid-plugin
description: Hyperliquid DEX — trade perps & spot, deposit from Arbitrum, withdraw to Arbitrum, transfer between perp and spot accounts, manage gas on HyperEVM.
version: "0.4.4"
version: "0.4.5"
author: GeoGu360
tags:
- perps
Expand All @@ -26,7 +26,7 @@ tags:
# Check for skill updates (1-hour cache)
UPDATE_CACHE="$HOME/.plugin-store/update-cache/hyperliquid-plugin"
CACHE_MAX=3600
LOCAL_VER="0.4.4"
LOCAL_VER="0.4.5"
DO_CHECK=true

if [ -f "$UPDATE_CACHE" ]; then
Expand All @@ -37,7 +37,7 @@ if [ -f "$UPDATE_CACHE" ]; then
fi

if [ "$DO_CHECK" = true ]; then
REMOTE_VER=$(curl -sf --max-time 3 "https://raw.githubusercontent.com/mig-pre/plugin-store/main/skills/hyperliquid-plugin/plugin.yaml" | grep '^version' | head -1 | tr -d '"' | awk '{print $2}')
REMOTE_VER=$(curl -sf --max-time 3 "https://raw.githubusercontent.com/okx/plugin-store/main/skills/hyperliquid-plugin/plugin.yaml" | grep '^version' | head -1 | tr -d '"' | awk '{print $2}')
if [ -n "$REMOTE_VER" ]; then
mkdir -p "$HOME/.plugin-store/update-cache"
echo "$REMOTE_VER" > "$UPDATE_CACHE"
Expand All @@ -47,7 +47,7 @@ fi
REMOTE_VER=$(cat "$UPDATE_CACHE" 2>/dev/null || echo "$LOCAL_VER")
if [ "$REMOTE_VER" != "$LOCAL_VER" ]; then
echo "Update available: hyperliquid-plugin v$LOCAL_VER -> v$REMOTE_VER. Updating..."
npx skills add mig-pre/plugin-store --skill hyperliquid-plugin --yes --global 2>/dev/null || true
npx skills add okx/plugin-store --skill hyperliquid-plugin --yes --global 2>/dev/null || true
echo "Updated hyperliquid-plugin to v$REMOTE_VER. Please re-read this SKILL.md."
fi
```
Expand Down Expand Up @@ -98,7 +98,7 @@ fi
npx skills add okx/onchainos-skills --yes --global

# 3. Install plugin-store skills (enables plugin discovery and management)
npx skills add mig-pre/plugin-store --skill plugin-store --yes --global
npx skills add okx/plugin-store --skill plugin-store --yes --global
```

### Install hyperliquid-plugin binary + launcher (auto-injected)
Expand All @@ -109,11 +109,11 @@ LAUNCHER="$HOME/.plugin-store/launcher.sh"
CHECKER="$HOME/.plugin-store/update-checker.py"
if [ ! -f "$LAUNCHER" ]; then
mkdir -p "$HOME/.plugin-store"
curl -fsSL "https://raw.githubusercontent.com/mig-pre/plugin-store/main/scripts/launcher.sh" -o "$LAUNCHER" 2>/dev/null || true
curl -fsSL "https://raw.githubusercontent.com/okx/plugin-store/main/scripts/launcher.sh" -o "$LAUNCHER" 2>/dev/null || true
chmod +x "$LAUNCHER"
fi
if [ ! -f "$CHECKER" ]; then
curl -fsSL "https://raw.githubusercontent.com/mig-pre/plugin-store/main/scripts/update-checker.py" -o "$CHECKER" 2>/dev/null || true
curl -fsSL "https://raw.githubusercontent.com/okx/plugin-store/main/scripts/update-checker.py" -o "$CHECKER" 2>/dev/null || true
fi

# Clean up old installation
Expand All @@ -138,12 +138,12 @@ mkdir -p ~/.local/bin

# Download binary + checksums to a sandbox, verify SHA256 before installing.
BIN_TMP=$(mktemp -d)
RELEASE_BASE="https://github.com/mig-pre/plugin-store/releases/download/plugins/hyperliquid-plugin@0.4.4"
RELEASE_BASE="https://github.com/okx/plugin-store/releases/download/plugins/hyperliquid-plugin@0.4.5"
curl -fsSL "${RELEASE_BASE}/hyperliquid-plugin-${TARGET}${EXT}" -o "$BIN_TMP/hyperliquid-plugin${EXT}" || {
echo "ERROR: failed to download hyperliquid-plugin-${TARGET}${EXT}" >&2
rm -rf "$BIN_TMP"; exit 1; }
curl -fsSL "${RELEASE_BASE}/checksums.txt" -o "$BIN_TMP/checksums.txt" || {
echo "ERROR: failed to download checksums.txt for hyperliquid-plugin@0.4.4" >&2
echo "ERROR: failed to download checksums.txt for hyperliquid-plugin@0.4.5" >&2
rm -rf "$BIN_TMP"; exit 1; }

EXPECTED=$(awk -v b="hyperliquid-plugin-${TARGET}${EXT}" '$2 == b {print $1; exit}' "$BIN_TMP/checksums.txt")
Expand All @@ -167,7 +167,7 @@ ln -sf "$LAUNCHER" ~/.local/bin/hyperliquid-plugin

# Register version
mkdir -p "$HOME/.plugin-store/managed"
echo "0.4.4" > "$HOME/.plugin-store/managed/hyperliquid-plugin"
echo "0.4.5" > "$HOME/.plugin-store/managed/hyperliquid-plugin"
```

---
Expand Down Expand Up @@ -1654,6 +1654,21 @@ Found and patched during integration:

## Changelog

### v0.4.5 (2026-05-10)

Seven UX / safety / correctness fixes surfaced during a HIP-3 NVDA-perp end-to-end reproduction. Each fix is independent; combined diff is 12 files / +317 / -82.

All write commands continue to require explicit `--confirm`; pre-flight checks added below run before any signing or submission, so risky inputs are caught before user authorization, never after.

- **fix**: `markets --coin <bare> --type tradfi|hip3` was silently routed to spot lookup, ignoring `--type` entirely (`lookup_single` ignored mode). Now searches every builder DEX in parallel and returns the first match; bare-symbol queries against tradfi correctly resolve `NVDA → xyz:NVDA`. (Bug #1)
- **fix**: `order` auto-bump for $10 minimum notional only bumped one tick (`if`, not `while`); for sz_decimals=3 markets like NVDA at $217.5 a `0.010 → 0.011` bump still left $2.39 < $10 and the order would link-revert post-sign. Replaced with `ceil(10 / mid * sz_factor) / sz_factor` — single-shot guaranteed convergence. (Bug #2)
- **fix**: `order` insufficient-perp-balance tip pointed users at `deposit --amount <shortfall>` even on HIP-3 builder DEX coins; (a) deposit funds the **default DEX** clearinghouse, not the builder dex, so the user's next order would fail again with the same error; (b) the suggested amount could be < $5, the HL bridge minimum (smaller deposits are silently dropped). Now emits `error_code: BUILDER_DEX_UNFUNDED` with two actionable paths: `abstraction --set unified` (one-time, all DEXs share margin) OR explicit dex-transfer chain. Default-DEX path now caps deposit suggestions at $5. (Bug #3)
- **fix**: `deposit` amounts < $5 used to print only an `eprintln!` warning then proceed to sign and broadcast; HL bridge silently drops these (funds lost on Arbitrum side). Now hard-rejected with `error_code: DEPOSIT_BELOW_MIN` — the rejection runs before user `--confirm` is honored, so no signing occurs. (Bug #3 sister bug)
- **fix**: `spot-order --coin` and `spot-prices --token` accepted only those names respectively, despite both referring to the same concept; users typing the "wrong" flag got `unexpected argument`. Added bidirectional clap aliases — both names work in both commands. (Bug #4)
- **fix**: `spot-order` only validated $10 minimum notional when both `--price` and `--size` were supplied (limit orders); market orders below $10 were signed and submitted, then rejected on-chain with `Order must have minimum value of 10 USDC`. Added mid-based notional pre-flight + auto-bump for market, hard-reject (`ORDER_BELOW_MIN_NOTIONAL`) for limit. Both run before sign/submit. (Bug #5)
- **fix**: `round_px` used `sig_figs = sz_decimals.max(1)` per Python SDK comment, but HL spec is **5 sig figs AND ≤ (MAX_DECIMALS - sz_decimals) decimal places**. For sz_decimals=3 markets (NVDA at $217.495) that yielded 3 sig figs → integer round → "217", losing 2pp risk-management precision in TP/SL bracket prices. Fixed to use 5 sig figs with the decimal-place cap; NVDA inputs of `212.06` / `229.46` are now preserved verbatim. (Bug #6)
- **fix**: `orders --coin xyz:NVDA` correctly extracted the dex prefix (per `--help` doc), but the post-fetch filter compared `coin.to_uppercase() != filter` — uppercasing the dex prefix too while the filter kept the prefix lowercase, so reduce-only TP/SL trigger orders on builder DEX coins were never returned. Switched to case-insensitive comparison. (Bug #7)

### v0.4.3 (2026-05-05)

- **fix**: HIP-4 outcome buy/sell missing OKX attribution reporting — `outcome-buy` and `outcome-sell` now invoke `report-plugin-info` after every successful order (filled OR resting) with the same payload shape as perp `order` (`market_id` = trade-context `#N` coin, `symbol` = `USDH`, `asset_id` = the 100M+ outcome asset id, `side` = BUY/SELL). `--strategy-id` flag added (optional attribution tag, empty string when omitted; consistent with v0.4.1 perp behavior). Fixes attribution gap discovered during v0.4.2 post-merge audit — outcome trades placed via v0.4.2 binary are unattributed at the backend.
Expand Down
2 changes: 1 addition & 1 deletion skills/hyperliquid-plugin/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version: 1
name: hyperliquid-plugin
version: "0.4.4"
version: "0.4.5"
description: "Trade perpetuals on Hyperliquid - check positions, get prices, place market/limit orders with TP/SL brackets, close positions, deposit USDC"
author:
name: GeoGu360
Expand Down
13 changes: 12 additions & 1 deletion skills/hyperliquid-plugin/src/commands/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,19 @@ pub async fn run(args: DepositArgs) -> anyhow::Result<()> {
println!("{}", super::error_response("Amount must be greater than 0", "INVALID_ARGUMENT", "Provide a positive USDC amount with --amount."));
return Ok(());
}
// HL bridge has a hard minimum of $5 USDC. Smaller deposits are silently
// dropped (funds lost on the source chain). Reject up-front so we never
// sign-and-broadcast a transaction that loses user money.
if args.amount < 5.0 {
eprintln!("WARNING: Minimum recommended deposit is $5 USDC. Amounts below $5 may not arrive.");
println!(
"{}",
super::error_response(
&format!("Deposit amount ${:.4} is below HL bridge minimum of $5 USDC", args.amount),
"DEPOSIT_BELOW_MIN",
"Hyperliquid's L1 bridge requires at least $5 USDC; smaller deposits are dropped (funds lost). Increase --amount to 5 or more.",
)
);
return Ok(());
}

// USDC has 6 decimals
Expand Down
118 changes: 107 additions & 11 deletions skills/hyperliquid-plugin/src/commands/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub async fn run(args: MarketsArgs) -> anyhow::Result<()> {

// Single-coin lookup short-circuits filters
if let Some(coin) = args.coin.as_deref() {
return lookup_single(info, coin).await;
return lookup_single(info, coin, &mode).await;
}

match mode {
Expand Down Expand Up @@ -142,26 +142,122 @@ fn resolve_mode(ty: Option<&str>, dex: Option<&str>) -> Result<Mode, String> {

// ---------- Single-coin lookup ----------

async fn lookup_single(info: &str, coin: &str) -> anyhow::Result<()> {
// Spot first if no colon and not in any perp universe
async fn lookup_single(info: &str, coin: &str, mode: &Mode) -> anyhow::Result<()> {
// Builder DEX market with explicit prefix ("xyz:CL") always wins over --type:
// user gave a fully-qualified coin, route directly.
if coin.contains(':') {
// Builder DEX market: "xyz:CL"
let (dex, _) = crate::api::parse_coin(coin);
let dex = dex.unwrap_or_default();
return lookup_perp(info, coin, Some(&dex)).await;
}

// Try default perp first
let default_meta = get_meta_and_asset_ctxs_for_dex(info, None).await;
if let Ok(meta) = default_meta {
if let Some(entry) = find_perp_market(&meta, coin, None) {
print_perp_single("default", entry);
// No prefix → search where --type / --dex tells us to look. Previous behavior
// (always default-perp then spot) silently dropped builder-DEX matches when
// user passed `--type tradfi --coin NVDA`.
match mode {
Mode::Spot => lookup_spot(info, coin).await,
Mode::PerpDefault => {
// Default DEX → fallback to spot for backward compat
let default_meta = get_meta_and_asset_ctxs_for_dex(info, None).await;
if let Ok(meta) = default_meta {
if let Some(entry) = find_perp_market(&meta, coin, None) {
print_perp_single("default", entry);
return Ok(());
}
}
lookup_spot(info, coin).await
}
Mode::PerpDex(dex_name) => lookup_perp(info, coin, Some(dex_name)).await,
Mode::PerpBuilders { dedupe_crypto } => {
lookup_across_builders(info, coin, *dedupe_crypto).await
}
Mode::Outcome => {
// Outcome markets are looked up by their full outcome id, not by
// bare token names. Fall back to existing single-asset error path.
println!("{}", super::error_response(
&format!("Outcome lookup by --coin '{}' not supported; use `outcome-list` to discover markets", coin),
"MARKET_NOT_FOUND",
"Use `hyperliquid-plugin outcome-list` to enumerate outcomes, then trade with `outcome-buy --outcome <id>`.",
));
Ok(())
}
}
}

/// Search across all HIP-3 builder DEXs in parallel for the first matching coin.
/// `dedupe_crypto = true` (--type tradfi) skips a builder match if the bare
/// symbol also exists on default DEX (e.g. xyz:BTC dedup'd because BTC is on
/// default). When false (--type hip3) every builder match counts.
async fn lookup_across_builders(
info: &str,
coin: &str,
dedupe_crypto: bool,
) -> anyhow::Result<()> {
let registry = match fetch_perp_dexs(info).await {
Ok(r) => r,
Err(e) => return print_api_err(&e),
};

// crypto symbol set on default DEX, used only when dedupe_crypto is on.
let crypto_set: HashSet<String> = if dedupe_crypto {
get_meta_and_asset_ctxs_for_dex(info, None)
.await
.ok()
.as_ref()
.and_then(|m| m.as_array())
.and_then(|a| a.first())
.and_then(|m| m["universe"].as_array())
.map(|u| {
u.iter()
.filter_map(|x| x["name"].as_str())
.map(|s| s.to_uppercase())
.collect()
})
.unwrap_or_default()
} else {
HashSet::new()
};

if dedupe_crypto && crypto_set.contains(&coin.to_uppercase()) {
// user said --type tradfi --coin BTC: BTC is a default-DEX crypto, not RWA
println!("{}", super::error_response(
&format!("'{}' is a crypto perp on default DEX, not a tradfi/RWA market. Use --type crypto (or no --type) to look up default-DEX coins.", coin),
"MARKET_NOT_FOUND",
"Run without --type for default DEX, or specify --coin <dex>:<symbol> to force a builder DEX lookup.",
));
return Ok(());
}

// Parallel fetch every builder DEX's meta, take first match.
let futs: Vec<_> = registry
.iter()
.map(|d: &BuilderDex| {
let name = d.name.clone();
async move {
let meta = get_meta_and_asset_ctxs_for_dex(info, Some(&name))
.await
.ok();
(name, meta)
}
})
.collect();
let results = futures::future::join_all(futs).await;

for (dex_name, meta_opt) in results {
let Some(meta) = meta_opt else { continue };
if let Some(entry) = find_perp_market(&meta, coin, Some(dex_name.clone())) {
print_perp_single(&dex_name, entry);
return Ok(());
}
}

// Fall back to spot
lookup_spot(info, coin).await
let preset = if dedupe_crypto { "tradfi" } else { "hip3" };
println!("{}", super::error_response(
&format!("Symbol '{}' not found on any builder DEX (--type {})", coin, preset),
"MARKET_NOT_FOUND",
"Run `hyperliquid-plugin markets --type tradfi` (no --coin) to list all RWA/equity markets, or use `--coin <dex>:<symbol>` if you know the DEX prefix.",
));
Ok(())
}

async fn lookup_perp(info: &str, coin: &str, dex_opt: Option<&str>) -> anyhow::Result<()> {
Expand Down
Loading
Loading