diff --git a/skills/hyperliquid-plugin/.claude-plugin/plugin.json b/skills/hyperliquid-plugin/.claude-plugin/plugin.json index 1dd456707..72a6b38ac 100644 --- a/skills/hyperliquid-plugin/.claude-plugin/plugin.json +++ b/skills/hyperliquid-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hyperliquid", "description": "Hyperliquid on-chain perpetuals DEX — check positions, get market prices, place and cancel perpetual orders on Hyperliquid L1 (chain_id 999).", - "version": "0.3.2", + "version": "0.3.6", "author": { "name": "GeoGu360", "github": "GeoGu360" diff --git a/skills/hyperliquid-plugin/Cargo.lock b/skills/hyperliquid-plugin/Cargo.lock index d8d2a9b2b..8cc0731da 100644 --- a/skills/hyperliquid-plugin/Cargo.lock +++ b/skills/hyperliquid-plugin/Cargo.lock @@ -542,7 +542,7 @@ dependencies = [ [[package]] name = "hyperliquid-plugin" -version = "0.3.2" +version = "0.3.6" dependencies = [ "anyhow", "clap", diff --git a/skills/hyperliquid-plugin/Cargo.toml b/skills/hyperliquid-plugin/Cargo.toml index f393658d6..85f96934b 100644 --- a/skills/hyperliquid-plugin/Cargo.toml +++ b/skills/hyperliquid-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyperliquid-plugin" -version = "0.3.4" +version = "0.3.6" edition = "2021" [[bin]] diff --git a/skills/hyperliquid-plugin/SKILL.md b/skills/hyperliquid-plugin/SKILL.md index 428e90c1e..4f7c909b0 100644 --- a/skills/hyperliquid-plugin/SKILL.md +++ b/skills/hyperliquid-plugin/SKILL.md @@ -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.3.4" +version: "0.3.6" author: GeoGu360 tags: - perps @@ -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.3.4" +LOCAL_VER="0.3.6" DO_CHECK=true if [ -f "$UPDATE_CACHE" ]; then @@ -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" @@ -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 ``` @@ -62,7 +62,7 @@ onchainos --version 2>/dev/null || curl -fsSL https://raw.githubusercontent.com/ 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) @@ -73,11 +73,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 @@ -99,7 +99,7 @@ case "${OS}_${ARCH}" in mingw*_aarch64|msys*_aarch64|cygwin*_aarch64) TARGET="aarch64-pc-windows-msvc"; EXT=".exe" ;; esac mkdir -p ~/.local/bin -curl -fsSL "https://github.com/mig-pre/plugin-store/releases/download/plugins/hyperliquid-plugin@0.3.4/hyperliquid-plugin-${TARGET}${EXT}" -o ~/.local/bin/.hyperliquid-plugin-core${EXT} +curl -fsSL "https://github.com/okx/plugin-store/releases/download/plugins/hyperliquid-plugin@0.3.6/hyperliquid-plugin-${TARGET}${EXT}" -o ~/.local/bin/.hyperliquid-plugin-core${EXT} chmod +x ~/.local/bin/.hyperliquid-plugin-core${EXT} # Symlink CLI name to universal launcher @@ -107,7 +107,7 @@ ln -sf "$LAUNCHER" ~/.local/bin/hyperliquid-plugin # Register version mkdir -p "$HOME/.plugin-store/managed" -echo "0.3.4" > "$HOME/.plugin-store/managed/hyperliquid-plugin" +echo "0.3.6" > "$HOME/.plugin-store/managed/hyperliquid-plugin" ``` ### Report install (auto-injected, runs once) @@ -127,7 +127,7 @@ if [ ! -f "$REPORT_FLAG" ]; then # Report to Vercel stats curl -s -X POST "https://plugin-store-dun.vercel.app/install" \ -H "Content-Type: application/json" \ - -d '{"name":"hyperliquid-plugin","version":"0.3.4"}' >/dev/null 2>&1 || true + -d '{"name":"hyperliquid-plugin","version":"0.3.6"}' >/dev/null 2>&1 || true # Report to OKX API (with HMAC-signed device token) curl -s -X POST "https://www.okx.com/priapi/v1/wallet/plugins/download/report" \ -H "Content-Type: application/json" \ @@ -230,6 +230,60 @@ The binary `hyperliquid` must be in your PATH. --- +### 0. `quickstart` — Check Assets & Get Guided Next Step + +Detects wallet state across Arbitrum and Hyperliquid in one call, then recommends the right next action. Use this when a user says "I want to start trading on Hyperliquid" or "what should I do first" without knowing their current status. + +**Trigger phrases:** +- "帮我看下 Hyperliquid 状态" / "我要开始用 Hyperliquid" +- "我有多少资产在 HL" / "quickstart hyperliquid" +- "Hyperliquid 怎么用" / "I want to trade on Hyperliquid" +- "check my hyperliquid balance" / "what should I do on HL" + +**Parameters:** + +| Flag | Required | Description | +|------|----------|-------------| +| `--address` | No | EVM wallet address (defaults to onchainos wallet) | + +**Output fields:** `wallet`, `assets.arb_usdc_balance`, `assets.hl_account_value_usd`, `assets.hl_withdrawable_usd`, `assets.hl_open_positions`, `positions[]`, `status`, `suggestion`, `next_command` + +**Status values and flow:** + +| `status` | Condition | `next_command` | +|----------|-----------|----------------| +| `active` | Has open HL positions | `hyperliquid positions` | +| `ready` | HL account ≥ $1, no positions | `hyperliquid order ...` | +| `needs_deposit` | Arbitrum USDC ≥ $5, HL empty | `hyperliquid deposit --amount X --confirm` | +| `low_balance` | Arbitrum USDC < $5 | `hyperliquid address` | +| `no_funds` | No USDC anywhere | `hyperliquid address` | + +**Example:** +``` +hyperliquid quickstart +``` + +```json +{ + "ok": true, + "wallet": "0x87fb0647...", + "assets": { + "arb_usdc_balance": 1.63, + "hl_account_value_usd": 9.89, + "hl_withdrawable_usd": 8.77, + "hl_open_positions": 1 + }, + "positions": [ + { "coin": "BTC", "side": "long", "size": "0.00015", "entryPrice": "74633.0", "unrealizedPnl": "0.0015" } + ], + "status": "active", + "suggestion": "You have open positions on Hyperliquid. Review them below.", + "next_command": "hyperliquid positions" +} +``` + +--- + ### 1. `positions` — Check Open Perp Positions Shows open perpetual positions, unrealized PnL, margin usage, and account summary for a wallet. @@ -904,6 +958,10 @@ All data returned by `hyperliquid positions`, `hyperliquid prices`, and exchange ## Changelog +### v0.3.6 (2026-04-17) + +- **feat**: `quickstart` — new command; checks Arbitrum USDC balance + Hyperliquid account value + open positions in parallel via onchainos, returns structured JSON with `status` and `next_command` to guide first-time users from zero to first trade + ### v0.3.2 (2026-04-13) - **fix**: `order` — balance pre-flight: queries Perp + Spot + Arbitrum USDC in parallel before every order; stops early with fund landscape + deposit/transfer tip if perp balance is insufficient diff --git a/skills/hyperliquid-plugin/SKILL_SUMMARY.md b/skills/hyperliquid-plugin/SKILL_SUMMARY.md new file mode 100644 index 000000000..2c6e3a8c4 --- /dev/null +++ b/skills/hyperliquid-plugin/SKILL_SUMMARY.md @@ -0,0 +1,22 @@ + +# hyperliquid -- Skill Summary + +## Overview +This skill enables trading perpetual futures on Hyperliquid, a high-performance on-chain perpetuals exchange built on its own L1 blockchain. It provides comprehensive trading functionality including position management, order placement with stop-loss/take-profit brackets, market data retrieval, and USDC deposits from Arbitrum. All operations use USDC as the margin token and settle on Hyperliquid L1 with CEX-like speed but full on-chain transparency. + +## Usage +Install the hyperliquid binary and ensure onchainos CLI is configured with your wallet. For write operations, first run commands without `--confirm` to preview, then add `--confirm` to sign and execute via EIP-712 signatures. + +## Commands +| Command | Description | +|---------|-------------| +| `hyperliquid positions` | Check open perpetual positions and account summary | +| `hyperliquid prices` | Get current mid prices for all or specific markets | +| `hyperliquid order` | Place market/limit orders with optional TP/SL brackets | +| `hyperliquid close` | Market-close an open position | +| `hyperliquid tpsl` | Set stop-loss/take-profit on existing positions | +| `hyperliquid cancel` | Cancel open orders by order ID | +| `hyperliquid deposit` | Deposit USDC from Arbitrum to Hyperliquid | + +## Triggers +Activate when users mention trading on Hyperliquid, checking Hyperliquid positions, placing perp orders, or managing stop-loss/take-profit levels. Also triggers for Hyperliquid-specific terms like "HL order", "HYPE perps", or phrases about opening/closing positions on the platform. diff --git a/skills/hyperliquid-plugin/plugin.yaml b/skills/hyperliquid-plugin/plugin.yaml index d2e383a24..5a86a7caf 100644 --- a/skills/hyperliquid-plugin/plugin.yaml +++ b/skills/hyperliquid-plugin/plugin.yaml @@ -1,6 +1,6 @@ schema_version: 1 name: hyperliquid-plugin -version: "0.3.4" +version: "0.3.6" description: "Trade perpetuals on Hyperliquid — check positions, get prices, place market/limit orders with TP/SL brackets, close positions, deposit USDC" author: name: GeoGu360 diff --git a/skills/hyperliquid-plugin/src/commands/cancel.rs b/skills/hyperliquid-plugin/src/commands/cancel.rs index 292385405..3078307e5 100644 --- a/skills/hyperliquid-plugin/src/commands/cancel.rs +++ b/skills/hyperliquid-plugin/src/commands/cancel.rs @@ -63,11 +63,11 @@ pub async fn run(args: CancelArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Cancel not signed or submitted."); + eprintln!("\n[DRY RUN] Cancel not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and submit this cancellation."); + eprintln!("\n[PREVIEW] Add --confirm to sign and submit this cancellation."); return Ok(()); } @@ -187,11 +187,11 @@ pub async fn run(args: CancelArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Cancel not signed or submitted."); + eprintln!("\n[DRY RUN] Cancel not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and submit this batch cancellation."); + eprintln!("\n[PREVIEW] Add --confirm to sign and submit this batch cancellation."); return Ok(()); } diff --git a/skills/hyperliquid-plugin/src/commands/close.rs b/skills/hyperliquid-plugin/src/commands/close.rs index 2718d7f3f..0c6437611 100644 --- a/skills/hyperliquid-plugin/src/commands/close.rs +++ b/skills/hyperliquid-plugin/src/commands/close.rs @@ -118,13 +118,13 @@ pub async fn run(args: CloseArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Not signed or submitted."); + eprintln!("\n[DRY RUN] Not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and market-close this position."); - println!("WARNING: Market orders execute immediately at prevailing price."); + eprintln!("\n[PREVIEW] Add --confirm to sign and market-close this position."); + eprintln!("WARNING: Market orders execute immediately at prevailing price."); return Ok(()); } diff --git a/skills/hyperliquid-plugin/src/commands/deposit.rs b/skills/hyperliquid-plugin/src/commands/deposit.rs index ae9b8ff56..4ca18f6d4 100644 --- a/skills/hyperliquid-plugin/src/commands/deposit.rs +++ b/skills/hyperliquid-plugin/src/commands/deposit.rs @@ -173,7 +173,7 @@ pub async fn run(args: DepositArgs) -> anyhow::Result<()> { }); // Step 2: Sign the permit via onchainos - println!("Signing USDC permit for {} USDC...", args.amount); + eprintln!("Signing USDC permit for {} USDC...", args.amount); let sig_hex = onchainos_sign_eip712(&permit_typed_data, &wallet)?; // Parse r, s, v from the 65-byte hex signature @@ -189,7 +189,7 @@ pub async fn run(args: DepositArgs) -> anyhow::Result<()> { let calldata = build_batched_deposit_calldata(&wallet, usdc_units, deadline, r, s, v); // Step 4: Submit the transaction - println!("Depositing {:.6} USDC to Hyperliquid via Arbitrum bridge...", args.amount); + eprintln!("Depositing {:.6} USDC to Hyperliquid via Arbitrum bridge...", args.amount); let deposit_result = wallet_contract_call( ARBITRUM_CHAIN_ID, HL_BRIDGE_ARBITRUM, diff --git a/skills/hyperliquid-plugin/src/commands/evm_send.rs b/skills/hyperliquid-plugin/src/commands/evm_send.rs index 273377891..dd401259d 100644 --- a/skills/hyperliquid-plugin/src/commands/evm_send.rs +++ b/skills/hyperliquid-plugin/src/commands/evm_send.rs @@ -1,8 +1,9 @@ use clap::Args; use sha3::{Digest, Keccak256}; -use crate::config::{CHAIN_ID, info_url}; +use crate::config::{CHAIN_ID, HYPER_EVM_RPC, info_url}; use crate::onchainos::{resolve_wallet, wallet_contract_call}; use crate::api::get_clearinghouse_state; +use crate::rpc::wait_tx_mined; /// CoreWriter precompile on HyperEVM — executes HyperCore actions via msg.sender const CORE_WRITER: &str = "0x3333333333333333333333333333333333333333"; @@ -170,19 +171,23 @@ pub async fn run(args: EvmSendArgs) -> anyhow::Result<()> { // ── Execute ─────────────────────────────────────────────────────────── // Step 1: Move perp → spot - println!("Step 1/2 Transferring {} USDC from perp → spot via CoreWriter...", args.amount); - wallet_contract_call(CHAIN_ID, CORE_WRITER, &calldata_perp_to_spot, None, false)?; - - // Give HyperCore time to settle the transfer (~2 HyperEVM blocks) - println!(" Waiting for HyperCore to process..."); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + eprintln!("Step 1/2 Transferring {} USDC from perp → spot via CoreWriter...", args.amount); + let result1 = wallet_contract_call(CHAIN_ID, CORE_WRITER, &calldata_perp_to_spot, None, false)?; + + // Wait for step 1 to be mined before submitting step 2 (HyperCore needs the tx on-chain) + eprintln!(" Waiting for HyperCore to process..."); + let tx1_hash = result1["data"]["txHash"].as_str().unwrap_or(""); + if !tx1_hash.is_empty() { + let confirmed = wait_tx_mined(tx1_hash, HYPER_EVM_RPC).await; + if !confirmed { + eprintln!(" Warning: step 1 tx confirmation timed out. Proceeding with step 2."); + } + } // Step 2: Spot → HyperEVM address - println!("Step 2/2 Sending {} USDC from spot → HyperEVM {}...", args.amount, &destination[..10]); + eprintln!("Step 2/2 Sending {} USDC from spot → HyperEVM {}...", args.amount, &destination[..10]); wallet_contract_call(CHAIN_ID, CORE_WRITER, &calldata_spot_to_evm, None, false)?; - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - println!("{}", serde_json::json!({ "ok": true, "action": "evm-send", diff --git a/skills/hyperliquid-plugin/src/commands/get_gas.rs b/skills/hyperliquid-plugin/src/commands/get_gas.rs index 5316b49b3..7965735b9 100644 --- a/skills/hyperliquid-plugin/src/commands/get_gas.rs +++ b/skills/hyperliquid-plugin/src/commands/get_gas.rs @@ -155,7 +155,7 @@ pub async fn run(args: GetGasArgs) -> anyhow::Result<()> { .unwrap_or(0); if existing < usdc_units as u128 { - println!("Approving USDC to relay solver..."); + eprintln!("Approving USDC to relay solver..."); let approve_value_opt = if approve_value > 0 { Some(approve_value) } else { None }; let result = wallet_contract_call( ARBITRUM_CHAIN_ID, approve_to, approve_calldata, approve_value_opt, false @@ -164,12 +164,12 @@ pub async fn run(args: GetGasArgs) -> anyhow::Result<()> { // Wait for the approve tx to be mined so deposit simulation succeeds let tx_hash = result["data"]["txHash"].as_str().unwrap_or(""); if !tx_hash.is_empty() { - print!(" Waiting for approve tx {} to confirm...", tx_hash); + eprint!(" Waiting for approve tx {} to confirm...", tx_hash); let confirmed = wait_tx_mined(tx_hash, ARBITRUM_RPC).await; - println!(" {}", if confirmed { "confirmed" } else { "timed out (proceeding anyway)" }); + eprintln!(" {}", if confirmed { "confirmed" } else { "timed out (proceeding anyway)" }); } } else { - println!("USDC allowance already sufficient, skipping approve."); + eprintln!("USDC allowance already sufficient, skipping approve."); } } @@ -192,12 +192,12 @@ pub async fn run(args: GetGasArgs) -> anyhow::Result<()> { .as_str() .unwrap_or(request_id); - println!("Depositing {} USDC to relay solver...", args.amount); + eprintln!("Depositing {} USDC to relay solver...", args.amount); let deposit_value_opt = if deposit_value > 0 { Some(deposit_value) } else { None }; wallet_contract_call(ARBITRUM_CHAIN_ID, deposit_to, deposit_calldata, deposit_value_opt, false)?; // Poll relay.link status until HYPE arrives (max ~40s) - println!("Waiting for HYPE to arrive on HyperEVM (~{} seconds)...", time_est); + eprintln!("Waiting for HYPE to arrive on HyperEVM (~{} seconds)...", time_est); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build()?; diff --git a/skills/hyperliquid-plugin/src/commands/mod.rs b/skills/hyperliquid-plugin/src/commands/mod.rs index 736aa1431..16cdbde23 100644 --- a/skills/hyperliquid-plugin/src/commands/mod.rs +++ b/skills/hyperliquid-plugin/src/commands/mod.rs @@ -16,3 +16,4 @@ pub mod spot_prices; pub mod tpsl; pub mod transfer; pub mod withdraw; +pub mod quickstart; diff --git a/skills/hyperliquid-plugin/src/commands/order.rs b/skills/hyperliquid-plugin/src/commands/order.rs index f1bb62d6c..073f3afa1 100644 --- a/skills/hyperliquid-plugin/src/commands/order.rs +++ b/skills/hyperliquid-plugin/src/commands/order.rs @@ -334,14 +334,14 @@ pub async fn run(args: OrderArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Order not signed or submitted."); + eprintln!("\n[DRY RUN] Order not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and submit this order."); - println!("WARNING: This will place a real perpetual order on Hyperliquid."); - println!(" Perpetuals trading involves significant risk including total loss."); + eprintln!("\n[PREVIEW] Add --confirm to sign and submit this order."); + eprintln!("WARNING: This will place a real perpetual order on Hyperliquid."); + eprintln!(" Perpetuals trading involves significant risk including total loss."); return Ok(()); } @@ -363,7 +363,7 @@ pub async fn run(args: OrderArgs) -> anyhow::Result<()> { lev_result["response"].as_str().unwrap_or("unknown error") ); } - println!( + eprintln!( "Leverage set to {}x ({}) for {}", lev, if is_cross { "cross" } else { "isolated" }, coin ); diff --git a/skills/hyperliquid-plugin/src/commands/quickstart.rs b/skills/hyperliquid-plugin/src/commands/quickstart.rs new file mode 100644 index 000000000..79bc65605 --- /dev/null +++ b/skills/hyperliquid-plugin/src/commands/quickstart.rs @@ -0,0 +1,187 @@ +use clap::Args; +use crate::api::get_clearinghouse_state; +use crate::config::{info_url, ARBITRUM_CHAIN_ID, USDC_ARBITRUM}; +use crate::onchainos::resolve_wallet; +use crate::rpc::{ARBITRUM_RPC, erc20_balance}; + +const ABOUT: &str = "Hyperliquid is a high-performance on-chain perpetuals DEX — trade BTC, ETH and 100+ assets with up to 50x leverage at CEX speed, with full on-chain transparency and no KYC."; + +#[derive(Args)] +pub struct QuickstartArgs { + /// Wallet address to query. Defaults to the connected onchainos wallet. + #[arg(long)] + pub address: Option, +} + +pub async fn run(args: QuickstartArgs) -> anyhow::Result<()> { + // 1. Resolve wallet (EVM address shared by Arbitrum + Hyperliquid) + let wallet = match args.address { + Some(addr) => addr, + None => resolve_wallet(ARBITRUM_CHAIN_ID)?, + }; + + eprintln!("Checking assets for {}...", &wallet[..std::cmp::min(10, wallet.len())]); + + // 2. Fetch in parallel: Arbitrum USDC balance + HL perp clearinghouse state + let url = info_url(); + let (arb_result, hl_result) = tokio::join!( + erc20_balance(USDC_ARBITRUM, &wallet, ARBITRUM_RPC), + get_clearinghouse_state(url, &wallet), + ); + + let arb_usdc_units = arb_result.unwrap_or(0); + let arb_usdc = arb_usdc_units as f64 / 1_000_000.0; + + // Parse HL clearinghouse state + let (hl_account_value, hl_withdrawable, open_positions, positions_detail) = match hl_result { + Ok(ref state) => { + let margin = &state["marginSummary"]; + let account_value: f64 = margin["accountValue"] + .as_str() + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0); + let withdrawable: f64 = state["withdrawable"] + .as_str() + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0); + let empty = vec![]; + let asset_positions = state["assetPositions"].as_array().unwrap_or(&empty); + let coins: Vec = asset_positions + .iter() + .filter_map(|p| p["position"]["coin"].as_str().map(|s| s.to_string())) + .collect(); + let detail: Vec = asset_positions + .iter() + .map(|p| { + let pos = &p["position"]; + let szi = pos["szi"].as_str().unwrap_or("0"); + serde_json::json!({ + "coin": pos["coin"].as_str().unwrap_or("?"), + "side": if szi.starts_with('-') { "short" } else { "long" }, + "size": szi, + "entryPrice": pos["entryPx"].as_str().unwrap_or("0"), + "unrealizedPnl": pos["unrealizedPnl"].as_str().unwrap_or("0"), + }) + }) + .collect(); + (account_value, withdrawable, coins, detail) + } + Err(_) => (0.0, 0.0, vec![], vec![]), + }; + + // 3. Build guidance based on account state + let (status, suggestion, onboarding_steps, next_command) = + build_suggestion(&wallet, arb_usdc, hl_account_value, &open_positions); + + let mut out = serde_json::json!({ + "ok": true, + "about": ABOUT, + "wallet": wallet, + "assets": { + "arb_usdc_balance": arb_usdc, + "hl_account_value_usd": hl_account_value, + "hl_withdrawable_usd": hl_withdrawable, + "hl_open_positions": open_positions.len(), + }, + "positions": positions_detail, + "status": status, + "suggestion": suggestion, + "next_command": next_command, + }); + + if !onboarding_steps.is_empty() { + out["onboarding_steps"] = serde_json::json!(onboarding_steps); + } + + println!("{}", serde_json::to_string_pretty(&out)?); + + Ok(()) +} + +/// Returns (status, human-readable suggestion, onboarding_steps, ready-to-run command). +fn build_suggestion( + wallet: &str, + arb_usdc: f64, + hl_account_value: f64, + open_positions: &[String], +) -> (&'static str, &'static str, Vec, String) { + // Case 1: active trader — has open positions + if !open_positions.is_empty() { + return ( + "active", + "You have open positions on Hyperliquid. Review them below.", + vec![], + "hyperliquid positions".to_string(), + ); + } + + // Case 2: funded and ready — USDC on HL, no positions yet + if hl_account_value >= 1.0 { + return ( + "ready", + "Your Hyperliquid perp account is funded. Place your first trade.", + vec![ + "1. Check available markets: hyperliquid prices".to_string(), + "2. Preview a trade (no --confirm = preview only):".to_string(), + " hyperliquid order --coin BTC --side long --size 10 --leverage 5".to_string(), + "3. When ready, add --confirm to execute on-chain:".to_string(), + " hyperliquid order --coin BTC --side long --size 10 --leverage 5 --confirm".to_string(), + ], + "hyperliquid order --coin BTC --side long --size 10 --leverage 5".to_string(), + ); + } + + // Case 3: has enough Arbitrum USDC to deposit (minimum $5) + if arb_usdc >= 5.0 { + let suggest = ((arb_usdc * 0.9 * 100.0).floor() / 100.0).max(5.0); + let suggest = suggest.min(arb_usdc); + return ( + "needs_deposit", + "You have USDC on Arbitrum. Deposit to Hyperliquid to start trading perps (minimum $5).", + vec![ + format!("1. Deposit USDC from Arbitrum to Hyperliquid (minimum $5):"), + format!(" hyperliquid deposit --amount {:.2} --confirm", suggest), + "2. Run quickstart again to confirm your HL account is funded:".to_string(), + " hyperliquid quickstart".to_string(), + "3. Check available markets: hyperliquid prices".to_string(), + "4. Place your first trade:".to_string(), + " hyperliquid order --coin BTC --side long --size 10 --leverage 5 --confirm".to_string(), + ], + format!("hyperliquid deposit --amount {:.2} --confirm", suggest), + ); + } + + // Case 4: some Arbitrum USDC but below $5 minimum + if arb_usdc > 0.0 { + return ( + "low_balance", + "You have some USDC on Arbitrum but below the $5 deposit minimum. Add more USDC to your Arbitrum wallet.", + vec![ + format!("1. Send at least $5 USDC to your Arbitrum wallet:"), + format!(" {}", wallet), + "2. Run quickstart again to check your balance:".to_string(), + " hyperliquid quickstart".to_string(), + "3. Then deposit to Hyperliquid:".to_string(), + " hyperliquid deposit --amount 5 --confirm".to_string(), + ], + "hyperliquid address".to_string(), + ); + } + + // Case 5: new user — no funds anywhere + ( + "no_funds", + "No USDC found on Arbitrum or Hyperliquid. Transfer USDC to your Arbitrum wallet, then deposit (minimum $5).", + vec![ + "1. Send USDC to your Arbitrum wallet (minimum $5):".to_string(), + format!(" {}", wallet), + "2. Run quickstart again to confirm your balance:".to_string(), + " hyperliquid quickstart".to_string(), + "3. Deposit USDC to Hyperliquid:".to_string(), + " hyperliquid deposit --amount --confirm".to_string(), + "4. Place your first trade:".to_string(), + " hyperliquid order --coin BTC --side long --size 10 --leverage 5 --confirm".to_string(), + ], + "hyperliquid address".to_string(), + ) +} diff --git a/skills/hyperliquid-plugin/src/commands/spot_cancel.rs b/skills/hyperliquid-plugin/src/commands/spot_cancel.rs index af38d6390..6cac7388b 100644 --- a/skills/hyperliquid-plugin/src/commands/spot_cancel.rs +++ b/skills/hyperliquid-plugin/src/commands/spot_cancel.rs @@ -64,11 +64,11 @@ pub async fn run(args: SpotCancelArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Cancel not signed or submitted."); + eprintln!("\n[DRY RUN] Cancel not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and submit this cancellation."); + eprintln!("\n[PREVIEW] Add --confirm to sign and submit this cancellation."); return Ok(()); } @@ -206,11 +206,11 @@ pub async fn run(args: SpotCancelArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Cancel not signed or submitted."); + eprintln!("\n[DRY RUN] Cancel not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and submit this batch cancellation."); + eprintln!("\n[PREVIEW] Add --confirm to sign and submit this batch cancellation."); return Ok(()); } diff --git a/skills/hyperliquid-plugin/src/commands/spot_order.rs b/skills/hyperliquid-plugin/src/commands/spot_order.rs index 4872a7cc5..5a629c40d 100644 --- a/skills/hyperliquid-plugin/src/commands/spot_order.rs +++ b/skills/hyperliquid-plugin/src/commands/spot_order.rs @@ -139,13 +139,13 @@ pub async fn run(args: SpotOrderArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Not signed or submitted."); + eprintln!("\n[DRY RUN] Not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to sign and submit this spot order."); - println!("WARNING: This will place a real spot order on Hyperliquid."); + eprintln!("\n[PREVIEW] Add --confirm to sign and submit this spot order."); + eprintln!("WARNING: This will place a real spot order on Hyperliquid."); return Ok(()); } diff --git a/skills/hyperliquid-plugin/src/commands/tpsl.rs b/skills/hyperliquid-plugin/src/commands/tpsl.rs index 3356274eb..42a595b10 100644 --- a/skills/hyperliquid-plugin/src/commands/tpsl.rs +++ b/skills/hyperliquid-plugin/src/commands/tpsl.rs @@ -193,13 +193,13 @@ pub async fn run(args: TpslArgs) -> anyhow::Result<()> { ); if args.dry_run { - println!("\n[DRY RUN] Not signed or submitted."); + eprintln!("\n[DRY RUN] Not signed or submitted."); return Ok(()); } if !args.confirm { - println!("\n[PREVIEW] Add --confirm to place these TP/SL orders."); - println!("NOTE: Both orders are sent independently (grouping: na). \ + eprintln!("\n[PREVIEW] Add --confirm to place these TP/SL orders."); + eprintln!("NOTE: Both orders are sent independently (grouping: na). \ The first one to trigger closes your position; cancel the \ other manually afterward."); return Ok(()); diff --git a/skills/hyperliquid-plugin/src/main.rs b/skills/hyperliquid-plugin/src/main.rs index 96cf9cfdc..914d811c4 100644 --- a/skills/hyperliquid-plugin/src/main.rs +++ b/skills/hyperliquid-plugin/src/main.rs @@ -25,6 +25,7 @@ use commands::{ tpsl::TpslArgs, transfer::TransferArgs, withdraw::WithdrawArgs, + quickstart::QuickstartArgs, }; #[derive(Parser)] @@ -76,6 +77,8 @@ enum Commands { SpotOrder(SpotOrderArgs), /// Cancel an open spot order by order ID or cancel all for a token (requires --confirm) SpotCancel(SpotCancelArgs), + /// Check wallet assets and get a recommended next step for Hyperliquid + Quickstart(QuickstartArgs), } #[tokio::main] @@ -100,5 +103,6 @@ async fn main() -> anyhow::Result<()> { Commands::SpotPrices(args) => commands::spot_prices::run(args).await, Commands::SpotOrder(args) => commands::spot_order::run(args).await, Commands::SpotCancel(args) => commands::spot_cancel::run(args).await, + Commands::Quickstart(args) => commands::quickstart::run(args).await, } }