df-hyperliquid-core 0.0.8-beta.1

Hyperliquid SDK for Rust: markets, account, trading, transfers, earn and deploy, with exact decimals and signing. Native and wasm.
Documentation
//! Live answers, mainnet, 2026-10-06, some trimmed as marked.

use std::{
    cell::RefCell,
    future::{Future, ready},
    pin::pin,
    task::{Context, Poll, Waker},
};

use crate::account::agent::{Agent, Role, extra_agents};
use crate::account::builder::{max_builder_fee, max_builder_fee_request};
use crate::account::referral::Referral;
use crate::account::sub_account::{SubAccount, sub_accounts};
use crate::client::{Transport, TransportError};
use crate::trade::builder_fee::BuilderFee;
use crate::utils::{Address, Dec};

/// HLP's leader's named agents, trimmed to two of six.
const AGENTS: &str = r#"[{"name":"a3","address":"0xeeb5fb0d32a403733ebbf6e1c694f9253b37cf4c","validUntil":1793721457998},{"name":"a2","address":"0x208c65ee65efc668e1d9960ffa6fe57c7e633079","validUntil":1793721404275}]"#;
const SUBS: &str = r#"[{"name":"ATLAS-SUPERVISED","subAccountUser":"0x1a16ca90bc961b6bce46f400be2afb0f9f71b93f","master":"0x114dc2c90b61ef68b6ccb45d8e9902609cb08f9d","clearinghouseState":{"marginSummary":{"accountValue":"0.0","totalNtlPos":"0.0","totalRawUsd":"0.0","totalMarginUsed":"0.0"},"crossMarginSummary":{"accountValue":"0.0","totalNtlPos":"0.0","totalRawUsd":"0.0","totalMarginUsed":"0.0"},"crossMaintenanceMarginUsed":"0.0","withdrawable":"0.0","assetPositions":[],"time":1791305840519},"spotState":{"balances":[{"coin":"USDC","token":0,"total":"0.0","hold":"0.0","entryNtl":"0.0"}]}}]"#;
/// A code's owner, its referral rows and reward history trimmed to one each.
const REFERRER: &str = r#"{"referredBy":null,"cumVlm":"7049.82","unclaimedRewards":"1.39565981","claimedRewards":"1.799226","builderRewards":"0.0","referrerState":{"stage":"ready","data":{"code":"TESTCODE","nReferrals":3,"referralStates":[{"cumVlm":"2547126.0","cumRewardedFeesSinceReferred":"15.83428","cumFeesRewardedToReferrer":"0.874721","timeJoined":1775271678880,"user":"0xb6a6123aca000befa2b18fb65e7ec489ee1dde92","tokenToState":[[0,{"cumVlm":"2547126.0","cumRewardedFeesSinceReferred":"15.83428","cumFeesRewardedToReferrer":"0.874721"}]]}]}},"rewardHistory":[{"earned":"0.24564","vlm":"3541.88913","referralVlm":"0.0","time":1686181525632}],"tokenToState":[[0,{"cumVlm":"7049.82","unclaimedRewards":"1.39565981","claimedRewards":"1.799226","builderRewards":"0.0"}]]}"#;
const REFERRED: &str = r#"{"referredBy":{"referrer":"0x677d831aef5328190852e24f13c46cac05f984e7","code":"TESTCODE"},"cumVlm":"2547126.0","unclaimedRewards":"0.0","claimedRewards":"0.0","builderRewards":"0.0","referrerState":{"stage":"needToCreateCode"},"rewardHistory":[],"tokenToState":[[0,{"cumVlm":"2547126.0","unclaimedRewards":"0.0","claimedRewards":"0.0","builderRewards":"0.0"}]]}"#;
const UNTRADED: &str = r#"{"referredBy":null,"cumVlm":"0.0","unclaimedRewards":"0.0","claimedRewards":"0.0","builderRewards":"0.0","referrerState":{"stage":"needToTrade","data":{"required":"10000.0"}},"rewardHistory":[],"tokenToState":[]}"#;

fn address(text: &str) -> Address {
    text.parse().unwrap()
}

#[test]
fn named_agents_read_with_when_each_expires() {
    let agents: Vec<Agent> = serde_json::from_str(AGENTS).unwrap();
    assert_eq!(agents[0].name, "a3");
    assert_eq!(agents[0].address, address("0xeeb5fb0d32a403733ebbf6e1c694f9253b37cf4c"));
    assert_eq!(agents[0].valid_until, Some(1_793_721_457_998));
}

#[test]
fn every_role_reads_and_an_agent_names_its_master() {
    for (said, role) in [
        (r#"{"role":"user"}"#, Role::User),
        (r#"{"role":"missing"}"#, Role::Missing),
        (r#"{"role":"vault"}"#, Role::Vault),
        (
            r#"{"role":"agent","data":{"user":"0x677d831aef5328190852e24f13c46cac05f984e7"}}"#,
            Role::Agent { user: address("0x677d831aef5328190852e24f13c46cac05f984e7") },
        ),
        (
            r#"{"role":"subAccount","data":{"master":"0xbaff87940cb148c5e2529c0a5944b0a3386fb015"}}"#,
            Role::SubAccount { master: address("0xbaff87940cb148c5e2529c0a5944b0a3386fb015") },
        ),
        (r#"{"role":"marketMaker"}"#, Role::Other("marketMaker".to_owned())),
        (r#"{"role":"agent","data":{"user":"not an address"}}"#, Role::Other("agent".to_owned())),
        (r#"{"role":"agent"}"#, Role::Other("agent".to_owned())),
    ] {
        assert_eq!(serde_json::from_str::<Role>(said).unwrap(), role, "{said}");
    }
}

#[test]
fn a_subaccount_reads_with_its_own_states() {
    let subs: Vec<SubAccount> = serde_json::from_str(SUBS).unwrap();
    let desk = &subs[0];
    assert_eq!(desk.name, "ATLAS-SUPERVISED");
    assert_eq!(desk.master, address("0x114dc2c90b61ef68b6ccb45d8e9902609cb08f9d"));
    assert_eq!(desk.clearinghouse_state.as_ref().map(|perp| perp.withdrawable), Some(Dec::ZERO));
    assert_eq!(desk.spot_state.as_ref().and_then(|spot| spot.usdc()).map(|usdc| usdc.total), Some(Dec::ZERO));
}

#[test]
fn referral_says_who_referred_whom_and_the_account_s_own_code() {
    let owner: Referral = serde_json::from_str(REFERRER).unwrap();
    assert_eq!((owner.own_code(), owner.referred_code()), (Some("TESTCODE"), None));
    assert_eq!(owner.referrer_state.data.as_ref().and_then(|data| data.n_referrals), Some(3));
    assert_eq!(owner.unclaimed_rewards, Dec::parse("1.39565981").unwrap());
    let referred: Referral = serde_json::from_str(REFERRED).unwrap();
    assert_eq!(referred.referred_code(), Some("TESTCODE"));
    assert_eq!(
        referred.referred_by.as_ref().map(|by| by.referrer),
        Some(address("0x677d831aef5328190852e24f13c46cac05f984e7"))
    );
    assert_eq!((referred.referrer_state.stage.as_str(), referred.own_code()), ("needToCreateCode", None));
    let untraded: Referral = serde_json::from_str(UNTRADED).unwrap();
    assert_eq!(untraded.referrer_state.data.and_then(|data| data.required), Some(Dec::from_int(10_000)));
}

/// Answers by `type`, keeping what was asked. Nothing leaves the process.
struct Recorded {
    sent: RefCell<Vec<String>>,
}

impl Transport for Recorded {
    fn post(&self, _: &str, body: String) -> impl Future<Output = Result<String, TransportError>> {
        let answer = match serde_json::from_str::<serde_json::Value>(&body).unwrap()["type"].as_str() {
            Some("maxBuilderFee") => "880",
            _ => "null",
        };
        self.sent.borrow_mut().push(body);
        ready(Ok(answer.to_owned()))
    }
}

fn block_on<F: Future>(future: F) -> F::Output {
    let mut future = pin!(future);
    let mut context = Context::from_waker(Waker::noop());
    loop {
        if let Poll::Ready(value) = future.as_mut().poll(&mut context) {
            return value;
        }
    }
}

#[test]
fn none_reads_as_empty_and_a_builder_fee_as_tenths() {
    let transport = Recorded { sent: RefCell::new(Vec::new()) };
    let user = address("0x0D1d9635D0640821d15e323ac8AdADfA9c111414");
    let builder = address("0x6967d7AcD1EC5f210E9590666498e0cC5D13a843");
    assert_eq!(block_on(extra_agents(&transport, user)).unwrap(), [], "null is no agents");
    assert_eq!(block_on(sub_accounts(&transport, user)).unwrap(), [], "null is no subaccounts");
    assert_eq!(block_on(max_builder_fee(&transport, user, builder)).unwrap(), BuilderFee::from_tenths(880));
    assert_eq!(serde_json::from_str::<Option<Vec<Agent>>>("[]").unwrap(), Some(vec![]), "the live answer for none");
    assert_eq!(
        max_builder_fee_request(user, builder),
        r#"{"type":"maxBuilderFee","user":"0x0d1d9635d0640821d15e323ac8adadfa9c111414","builder":"0x6967d7acd1ec5f210e9590666498e0cc5d13a843"}"#
    );
    assert_eq!(
        transport.sent.borrow()[0],
        r#"{"type":"extraAgents","user":"0x0d1d9635d0640821d15e323ac8adadfa9c111414"}"#
    );
}

/// A live unified account, read as a standing: its mode, spot, and each dex
/// asked for, in that order, at its own address.
#[test]
fn a_standing_reads_the_mode_spot_and_each_dex_asked_for() {
    use crate::account::fixtures::{self, PERP_UNIFIED, PERP_UNIFIED_XYZ, SPOT_UNIFIED, UNIFIED};
    use crate::account::state::Abstraction;
    let at = |kind: &str, rest: &str| format!(r#"{{"type":"{kind}","user":"{UNIFIED}"{rest}}}"#);
    let asked = [
        at("userAbstraction", ""),
        at("spotClearinghouseState", ""),
        at("clearinghouseState", ""),
        at("clearinghouseState", r#","dex":"xyz""#),
    ];
    let transport = fixtures::Recorded::new(&[
        (&asked[0], "\"unifiedAccount\""),
        (&asked[1], SPOT_UNIFIED),
        (&asked[2], PERP_UNIFIED),
        (&asked[3], PERP_UNIFIED_XYZ),
    ]);
    let standing = fixtures::block_on(crate::account::standing(&transport, address(UNIFIED), &["", "xyz"])).unwrap();
    assert_eq!(standing.mode, Some(Abstraction::UnifiedAccount));
    assert_eq!(standing.dexes.iter().map(|dex| dex.dex.as_str()).collect::<Vec<_>>(), ["", "xyz"]);
    assert_eq!(standing.dex("").map(|perp| perp.withdrawable), Some(Dec::parse("7046.148664").unwrap()));
    assert_eq!(standing.spot.as_ref().map(|spot| spot.balances.len()), Some(7));
    assert_eq!(transport.asked(), asked);
}

#[test]
fn a_role_is_read_at_the_address() {
    use crate::account::fixtures::{self, ROLE_AGENT, UNIFIED};
    let agent = address("0xac325205a75ad9695c1499bff980c74539f7515a");
    let body = r#"{"type":"userRole","user":"0xac325205a75ad9695c1499bff980c74539f7515a"}"#;
    let transport = fixtures::Recorded::new(&[(body, ROLE_AGENT)]);
    assert_eq!(fixtures::block_on(crate::account::role(&transport, agent)), Ok(Role::Agent { user: address(UNIFIED) }));
}