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
use crate::client::Network;
use std::{
    cell::RefCell,
    future::{Future, ready},
    pin::pin,
    task::{Context, Poll, Waker},
};

use crate::account::LocalKey;
use crate::client::TransportError;
use crate::utils::hex;
use crate::utils::sign::L1Action;

use super::*;
use crate::earn::vaults::transfer::{
    HLP_LOCKUP_MS, MAINNET_HLP as HLP, MAINNET_VAULT_LOCKUP_MS, VaultTransfer, lockup_ms,
};

const KEY: &str = "0x0123456789012345678901234567890123456789012345678901234567890123";
const NONCE: u64 = 1_790_412_105_001;

/// Live, mainnet, 2026-10-06: HLP asked about one follower, trimmed to one
/// follower, one portfolio row and two children.
const HLP_DETAILS: &str = r#"{"name":"Hyperliquidity Provider (HLP)","vaultAddress":"0xdfc24b077bc1425ad1dea75bcb6f8158e10df303","leader":"0x677d831aef5328190852e24f13c46cac05f984e7","description":"This community-owned vault provides liqu","portfolio":[["day",{"accountValueHistory":[[1791216735605,"181550588.8688080013"]],"pnlHistory":[[1791216735605,"0.0"]],"vlm":"0.0"}]],"apr":0.037569521048262375,"followerState":{"user":"0x03e161499870b0a37549b16a5d33e0582fe1255e","vaultEquity":"604934.1646616657","pnl":"80799.3608496657","allTimePnl":"156824.7213636657","daysFollowing":655,"vaultEntryTime":1734650428028,"lockupUntil":1770680880422},"leaderFraction":0.002002618331064799,"leaderCommission":0.0,"followers":[{"user":"Leader","vaultEquity":"361964.4018696983","pnl":"273234.2946016983","allTimePnl":"273238.1857936983","daysFollowing":1252,"vaultEntryTime":1683243596849,"lockupUntil":1687702696528},{"user":"0x03e161499870b0a37549b16a5d33e0582fe1255e","vaultEquity":"604934.1646616657","pnl":"80799.3608496657","allTimePnl":"156824.7213636657","daysFollowing":655,"vaultEntryTime":1734650428028,"lockupUntil":1770680880422}],"maxDistributable":41085359.552966,"maxWithdrawable":604934.164661,"isClosed":false,"relationship":{"type":"parent","data":{"childAddresses":["0x010461c14e146ac35fe42271bdc1134ee31c703a","0x2e3d94f0562703b25c83308a05046ddaf9a8dd14"]}},"allowDeposits":true,"alwaysCloseOnWithdraw":false}"#;
/// Live, the same follower.
const EQUITIES: &str = r#"[{"vaultAddress":"0xdfc24b077bc1425ad1dea75bcb6f8158e10df303","equity":"604931.6453119999","lockedUntilTimestamp":1770680880422}]"#;
/// Live, HLP's leader.
const LEADING: &str =
    r#"[{"address":"0xdfc24b077bc1425ad1dea75bcb6f8158e10df303","name":"Hyperliquidity Provider (HLP)"}]"#;

fn dec(text: &str) -> Dec {
    Dec::parse(text).unwrap()
}

/// The vector `@nktkas/hyperliquid` 0.33.2 made: 10 USDC into HLP. Mainnet.
#[test]
fn a_vault_deposit_is_the_sdk_s_byte_for_byte() {
    let key = LocalKey::from_hex(KEY).unwrap();
    let action = VaultTransfer::deposit(HLP, dec("10")).unwrap();
    let json = r#"{"type":"vaultTransfer","vaultAddress":"0xdfc24b077bc1425ad1dea75bcb6f8158e10df303","isDeposit":true,"usd":10000000}"#;
    assert_eq!(action.wire(), json);
    assert_eq!(
        hex::encode_prefixed(&action.connection_id(NONCE, None)),
        "0x58a49fb2e400641875c0df3c98fd48f61210198012623c24f1ce8cc748c6e57a"
    );
    let signature = key.sign_digest(&action.digest(NONCE, None, Network::Mainnet)).unwrap();
    let (r, s) = (
        "0xd7b57bb7650ca10752a733ed62df2a5a8931e1c4f7290e0a143bcf96b22ad1fb",
        "0x63045ef3dfa4f14c26ac85dc03a76b4c332bbca837fb52ea4f9cf84bfcabe450",
    );
    assert_eq!((signature.r_hex().as_str(), signature.s_hex().as_str(), signature.v()), (r, s, 28));
    assert_eq!(
        action.body(NONCE, &signature, None),
        format!(
            r#"{{"action":{json},"nonce":{NONCE},"signature":{{"r":"{r}","s":"{s}","v":28}},"vaultAddress":null}}"#
        )
    );
}

#[test]
fn usd_goes_out_in_millionths_and_finer_is_refused() {
    let out = VaultTransfer::withdraw(HLP, dec("604934.164661")).unwrap();
    assert_eq!(out.usd(), 604_934_164_661);
    assert!(out.wire().contains(r#""isDeposit":false,"usd":604934164661"#));
    assert!(VaultTransfer::withdraw(HLP, dec("604931.6453119999")).is_err(), "an equity, uncut");
    assert!(VaultTransfer::deposit(HLP, Dec::ZERO).is_err());
    let cut = crate::utils::cut_units(dec("604931.6453119999"), 6).unwrap();
    assert_eq!(VaultTransfer::from_micros(HLP, false, cut).usd(), 604_931_645_311);
}

#[test]
fn hlp_locks_a_deposit_four_days_and_any_other_vault_one() {
    assert_eq!(lockup_ms(Network::Mainnet, HLP), HLP_LOCKUP_MS);
    assert_eq!(HLP_LOCKUP_MS, 4 * MAINNET_VAULT_LOCKUP_MS);
    assert_eq!(lockup_ms(Network::Mainnet, "0x010461c14e146ac35fe42271bdc1134ee31c703a".parse().unwrap()), 86_400_000);
}

#[test]
fn vault_details_read_with_their_floats_kept_as_written() {
    let hlp: VaultDetails = serde_json::from_str(HLP_DETAILS).unwrap();
    assert_eq!(hlp.vault_address, HLP);
    assert_eq!(hlp.apr.as_ref().map(Figure::as_str), Some("0.037569521048262375"));
    assert_eq!(hlp.apr.as_ref().and_then(Figure::to_dec), Some(dec("0.037569521048262375")));
    assert_eq!(hlp.max_withdrawable.as_ref().and_then(Figure::to_dec), Some(dec("604934.164661")));
    assert_eq!(hlp.leader_commission.as_ref().and_then(Figure::to_dec), Some(Dec::ZERO));
    let mine = hlp.follower_state.as_ref().unwrap();
    assert_eq!((mine.vault_equity, mine.lockup_until), (dec("604934.1646616657"), Some(1_770_680_880_422)));
    // The leader's own row names no address.
    let users: Vec<Option<Address>> = hlp.followers.iter().map(|f| f.user).collect();
    assert_eq!(users, [None, Some(Address::parse_const("0x03e161499870b0a37549b16a5d33e0582fe1255e").unwrap())]);
    assert!(hlp.takes_deposits());
    assert_eq!(serde_json::from_str::<Option<VaultDetails>>("null").unwrap(), None, "no vault at that address");
}

#[test]
fn equities_and_leading_vaults_read() {
    let equities: Vec<VaultEquity> = serde_json::from_str(EQUITIES).unwrap();
    assert_eq!((equities[0].vault_address, equities[0].equity), (HLP, dec("604931.6453119999")));
    assert!(!equities[0].is_locked(1_791_305_840_205));
    assert!(serde_json::from_str::<Vec<VaultEquity>>("[]").unwrap().is_empty());
    let leading: Vec<LeadingVault> = serde_json::from_str(LEADING).unwrap();
    assert_eq!((leading[0].address, leading[0].name.as_str()), (HLP, "Hyperliquidity Provider (HLP)"));
}

struct Recorded {
    sent: RefCell<Vec<String>>,
}

impl Transport for Recorded {
    fn post(&self, _: &str, body: String) -> impl Future<Output = Result<String, TransportError>> {
        self.sent.borrow_mut().push(body);
        ready(Ok(HLP_DETAILS.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;
        }
    }
}

/// Through `info`: the floats survive the transport.
#[test]
fn a_details_read_names_the_vault_and_the_follower() {
    let transport = Recorded { sent: RefCell::new(Vec::new()) };
    let follower: Address = "0x03e161499870b0a37549b16a5d33e0582fe1255e".parse().unwrap();
    let hlp = block_on(vault_details(&transport, HLP, Some(follower))).unwrap().unwrap();
    assert_eq!(hlp.apr.as_ref().map(Figure::as_str), Some("0.037569521048262375"));
    assert_eq!(
        transport.sent.borrow()[0],
        r#"{"type":"vaultDetails","vaultAddress":"0xdfc24b077bc1425ad1dea75bcb6f8158e10df303","user":"0x03e161499870b0a37549b16a5d33e0582fe1255e"}"#
    );
    assert_eq!(
        vault_details_request(HLP, None),
        r#"{"type":"vaultDetails","vaultAddress":"0xdfc24b077bc1425ad1dea75bcb6f8158e10df303"}"#
    );
}