pub mod error;
pub mod http;
pub mod signing;
pub mod types;
mod utils;
pub mod ws;
use std::{
fmt,
hash::Hash,
sync::atomic::{self, AtomicU64},
};
pub use alloy::signers::local::PrivateKeySigner;
use alloy::{
dyn_abi::Eip712Domain,
primitives::{B128, U256, address},
};
use anyhow::Context;
use chrono::Utc;
use either::Either;
pub use error::{ActionError, Error};
use reqwest::IntoUrl;
use rust_decimal::{Decimal, MathematicalOps, RoundingStrategy, prelude::ToPrimitive};
use serde::{Deserialize, Serialize};
pub use types::*;
use url::Url;
use crate::{
Address,
hyperevm::{from_wei, to_wei},
};
pub type Cloid = B128;
pub type OidOrCloid = Either<u64, Cloid>;
pub use http::Client as HttpClient;
pub use ws::Connection as WebSocket;
pub struct NonceHandler {
nonce: AtomicU64,
}
impl Default for NonceHandler {
fn default() -> Self {
let now = Utc::now().timestamp_millis() as u64;
Self {
nonce: AtomicU64::new(now),
}
}
}
impl NonceHandler {
pub fn next(&self) -> u64 {
let now = Utc::now().timestamp_millis() as u64;
let prev = self.nonce.load(atomic::Ordering::Relaxed);
if prev + 300 < now {
self.nonce.fetch_max(now, atomic::Ordering::Relaxed);
}
self.nonce.fetch_add(1, atomic::Ordering::Relaxed)
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
derive_more::Display,
derive_more::FromStr,
derive_more::IsVariant,
)]
#[serde(rename_all = "PascalCase")]
pub enum Chain {
#[display("Mainnet")]
Mainnet,
#[display("Testnet")]
Testnet,
}
impl Chain {
pub fn arbitrum_id(&self) -> &'static str {
if self.is_mainnet() {
ARBITRUM_MAINNET_CHAIN_ID
} else {
ARBITRUM_TESTNET_CHAIN_ID
}
}
pub fn domain(&self) -> Eip712Domain {
if self.is_mainnet() {
ARBITRUM_MAINNET_EIP712_DOMAIN
} else {
ARBITRUM_TESTNET_EIP712_DOMAIN
}
}
}
pub const ARBITRUM_MAINNET_CHAIN_ID: &str = "0xa4b1";
pub const ARBITRUM_TESTNET_CHAIN_ID: &str = "0x66eee";
pub const USDC_CONTRACT_IN_EVM: Address = address!("0xb88339CB7199b77E23DB6E890353E22632Ba630f");
#[inline(always)]
pub fn mainnet() -> HttpClient {
HttpClient::new(Chain::Mainnet)
}
#[inline(always)]
pub fn testnet() -> HttpClient {
HttpClient::new(Chain::Testnet)
}
#[inline(always)]
pub fn mainnet_ws() -> WebSocket {
WebSocket::new(mainnet_websocket_url())
}
#[inline(always)]
pub fn mainnet_url() -> Url {
"https://api.hyperliquid.xyz".parse().unwrap()
}
#[inline(always)]
pub fn mainnet_websocket_url() -> Url {
"wss://api.hyperliquid.xyz/ws".parse().unwrap()
}
#[inline(always)]
pub fn testnet_url() -> Url {
"https://api.hyperliquid-testnet.xyz".parse().unwrap()
}
#[inline(always)]
pub fn testnet_websocket_url() -> Url {
"wss://api.hyperliquid-testnet.xyz/ws".parse().unwrap()
}
#[inline(always)]
pub fn testnet_ws() -> WebSocket {
WebSocket::new(testnet_websocket_url())
}
#[derive(Debug, Clone, Copy)]
pub struct PriceTick {
max_decimals: i64,
}
impl PriceTick {
pub fn for_spot(sz_decimals: i64) -> Self {
Self {
max_decimals: 8 - sz_decimals,
}
}
pub fn for_perp(sz_decimals: i64) -> Self {
Self {
max_decimals: 6 - sz_decimals,
}
}
pub fn tick_for(&self, price: Decimal) -> Option<Decimal> {
let sig_figs = price.log10();
let sig_figs_n = sig_figs.ceil().to_i32()? as i64;
let decimals = 5_i64 - sig_figs_n;
let max_decimals = decimals.clamp(0, self.max_decimals);
Some(Decimal::TEN.powi(-max_decimals))
}
pub fn round(&self, price: Decimal) -> Option<Decimal> {
let tick = self.tick_for(price)?;
let rounded =
(price / tick).round_dp_with_strategy(0, RoundingStrategy::MidpointTowardZero) * tick;
Some(rounded)
}
pub fn round_by_side(&self, side: Side, price: Decimal, conservative: bool) -> Option<Decimal> {
let tick = self.tick_for(price)?;
let strategy = match (side, conservative) {
(Side::Ask, true) | (Side::Bid, false) => {
RoundingStrategy::ToPositiveInfinity
}
(Side::Ask, false) | (Side::Bid, true) => {
RoundingStrategy::ToNegativeInfinity
}
};
let rounded = price.round_dp_with_strategy(tick.scale(), strategy);
Some(rounded)
}
}
#[derive(Debug, Clone)]
pub struct PerpMarket {
pub name: String,
pub index: usize,
pub sz_decimals: i64,
pub collateral: SpotToken,
pub max_leverage: u64,
pub isolated_margin: bool,
pub margin_mode: Option<MarginMode>,
pub growth_mode: bool,
pub aligned_quote_token: bool,
pub table: PriceTick,
}
impl PerpMarket {
#[must_use]
pub fn symbol(&self) -> &str {
&self.name
}
#[must_use]
pub fn tick_table(&self) -> &PriceTick {
&self.table
}
pub fn tick_for(&self, price: Decimal) -> Option<Decimal> {
self.table.tick_for(price)
}
pub fn round_price(&self, price: Decimal) -> Option<Decimal> {
self.table.round(price)
}
pub fn round_by_side(&self, side: Side, price: Decimal, conservative: bool) -> Option<Decimal> {
self.table.round_by_side(side, price, conservative)
}
}
#[derive(Debug, Clone)]
pub struct SpotMarket {
pub name: String,
pub index: usize,
pub tokens: [SpotToken; 2],
pub table: PriceTick,
}
impl SpotMarket {
#[must_use]
pub fn symbol(&self) -> String {
format!("{}/{}", self.tokens[0].name, self.tokens[1].name)
}
#[must_use]
pub fn base(&self) -> &SpotToken {
&self.tokens[0]
}
#[must_use]
pub fn quote(&self) -> &SpotToken {
&self.tokens[1]
}
#[must_use]
pub fn tick_table(&self) -> &PriceTick {
&self.table
}
pub fn tick_for(&self, price: Decimal) -> Option<Decimal> {
self.table.tick_for(price)
}
pub fn round_price(&self, price: Decimal) -> Option<Decimal> {
self.table.round(price)
}
pub fn round_by_side(&self, side: Side, price: Decimal, conservative: bool) -> Option<Decimal> {
self.table.round_by_side(side, price, conservative)
}
}
impl PartialEq for SpotMarket {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for SpotMarket {}
#[cfg(test)]
mod tick_tests {
use rust_decimal::dec;
use super::*;
#[test]
fn test_perp() {
let prices = vec![
(5, dec!(93231.23), dec!(1), dec!(93231)),
(5, dec!(108_234.23), dec!(1), dec!(108_234)),
(2, dec!(137.23025), dec!(0.01), dec!(137.23)),
(2, dec!(99.98241), dec!(0.001), dec!(99.982)),
(0, dec!(0.001234), dec!(0.000001), dec!(0.001234)),
(0, dec!(0.051618), dec!(0.000001), dec!(0.051618)),
(0, dec!(0.000829), dec!(0.000001), dec!(0.000829)),
];
for (sz_decimals, price, expected_tick, expected_price) in prices {
let table = PriceTick::for_perp(sz_decimals);
let tick = table.tick_for(price);
assert_eq!(
tick,
Some(expected_tick),
"${}: expected tick {}, got {:?}",
price,
expected_tick,
tick
);
let output_price = table.round(price).unwrap();
assert_eq!(
expected_price, output_price,
"${}: expected price {}, got {}",
price, expected_price, output_price
);
}
}
#[test]
fn test_spot() {
let prices = vec![
(5, dec!(93231.23), dec!(1), dec!(93231)),
(5, dec!(108_234.23), dec!(1), dec!(108_234)),
(2, dec!(137.23025), dec!(0.01), dec!(137.23)),
(2, dec!(99.98241), dec!(0.001), dec!(99.982)),
(0, dec!(0.0000003315), dec!(0.00000001), dec!(0.00000033)),
(2, dec!(0.00001501), dec!(0.000001), dec!(0.000015)),
(0, dec!(0.9543309), dec!(0.00001), dec!(0.95433)),
(2, dec!(15.9715981), dec!(0.001), dec!(15.972)),
];
for (sz_decimals, price, expected_tick, expected_price) in prices {
let table = PriceTick::for_spot(sz_decimals);
let tick = table.tick_for(price);
assert_eq!(
tick,
Some(expected_tick),
"${}: expected tick {}, got {:?}",
price,
expected_tick,
tick
);
let output_price = table.round(price).unwrap();
assert_eq!(
expected_price, output_price,
"${}: expected price {}, got {}",
price, expected_price, output_price
);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpotToken {
pub name: String,
pub index: u32,
pub token_id: B128,
pub evm_contract: Option<Address>,
pub cross_chain_address: Option<Address>,
pub sz_decimals: i64,
pub wei_decimals: i64,
pub evm_extra_decimals: i64,
}
impl SpotToken {
#[must_use]
pub fn to_wei(&self, size: Decimal) -> U256 {
to_wei(size, (self.wei_decimals + self.evm_extra_decimals) as u32)
}
#[must_use]
pub fn from_wei(&self, size: U256) -> Decimal {
from_wei(size, (self.wei_decimals + self.evm_extra_decimals) as u32)
}
#[must_use]
#[inline(always)]
pub fn is_evm_linked(&self) -> bool {
self.evm_contract.is_some()
}
#[must_use]
#[inline(always)]
pub fn total_evm_decimals(&self) -> i64 {
self.sz_decimals + self.evm_extra_decimals
}
#[must_use]
#[inline(always)]
pub fn bridge_address(&self) -> Option<Address> {
self.cross_chain_address
}
}
impl Hash for SpotToken {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.token_id.hash(state);
}
}
impl PartialEq for SpotToken {
fn eq(&self, other: &Self) -> bool {
self.token_id == other.token_id
}
}
impl Eq for SpotToken {}
impl fmt::Display for SpotToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Debug, Clone)]
pub struct OutcomeSideSpec {
pub name: String,
}
#[derive(Debug, Clone)]
pub struct OutcomeInfo {
pub outcome: u32,
pub name: String,
pub description: String,
pub side_specs: Vec<OutcomeSideSpec>,
}
#[derive(Debug, Clone)]
pub struct OutcomeQuestion {
pub question: u32,
pub name: String,
pub description: String,
pub fallback_outcome: Option<u32>,
pub named_outcomes: Vec<u32>,
pub settled_named_outcomes: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct OutcomeMeta {
pub outcomes: Vec<OutcomeInfo>,
pub questions: Vec<OutcomeQuestion>,
}
async fn raw_spot_markets(
core_url: impl IntoUrl,
client: reqwest::Client,
) -> anyhow::Result<SpotTokens> {
let mut url = core_url.into_url()?;
url.set_path("/info");
let resp = client.post(url).json(&InfoRequest::SpotMeta).send().await?;
Ok(resp.json().await?)
}
pub async fn spot_tokens(
core_url: impl IntoUrl,
client: reqwest::Client,
) -> anyhow::Result<Vec<SpotToken>> {
let data = raw_spot_markets(core_url, client).await?;
let spot_tokens: Vec<_> = data.tokens.iter().cloned().map(SpotToken::from).collect();
Ok(spot_tokens)
}
pub async fn spot_markets(
core_url: impl IntoUrl,
client: reqwest::Client,
) -> anyhow::Result<Vec<SpotMarket>> {
let data = raw_spot_markets(core_url, client).await?;
let mut markets = Vec::with_capacity(data.universe.len());
let spot_tokens: Vec<_> = data.tokens.iter().cloned().map(SpotToken::from).collect();
for item in data.universe {
let (_, base) = spot_tokens
.iter()
.enumerate()
.find(|(index, _)| *index as u32 == item.tokens[0])
.context("base token index not found")?;
let (_, quote) = spot_tokens
.iter()
.enumerate()
.find(|(index, _)| *index as u32 == item.tokens[1])
.context("quote token index not found")?;
markets.push(SpotMarket {
name: item.name,
index: 10_000 + item.index,
tokens: [base.clone(), quote.clone()],
table: PriceTick::for_spot(base.sz_decimals),
});
}
Ok(markets)
}
pub async fn perp_dexs(
core_url: impl IntoUrl,
client: reqwest::Client,
) -> anyhow::Result<Vec<Dex>> {
let mut url = core_url.into_url()?;
url.set_path("/info");
let resp = client
.post(url)
.json(&InfoRequest::PerpDexs)
.send()
.await
.context("info")?;
let dexes: Vec<Option<PerpDex>> = resp.json().await?;
let dex_list = dexes
.into_iter()
.enumerate()
.filter_map(|(index, dex)| {
dex.map(|dex| Dex {
name: dex.name,
index,
deployer_fee_scale: dex.deployer_fee_scale,
})
})
.collect();
Ok(dex_list)
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PerpDex {
name: String,
#[serde(default, with = "rust_decimal::serde::str_option")]
deployer_fee_scale: Option<Decimal>,
}
pub async fn perp_markets(
core_url: impl IntoUrl,
client: reqwest::Client,
dex: Option<Dex>,
) -> anyhow::Result<Vec<PerpMarket>> {
let mut url = core_url.into_url()?;
url.set_path("/info");
let spot = raw_spot_markets(url.clone(), client.clone()).await?;
let resp = client
.post(url)
.json(&InfoRequest::Meta {
dex: dex.as_ref().map(|dex| dex.name.clone()),
})
.send()
.await
.context("meta")?;
let data: PerpTokens = resp.json().await?;
let collateral = spot
.tokens
.get(data.collateral_token)
.context("collateral token index out of bounds")?;
let collateral = SpotToken::from(collateral.clone());
let dex_index = dex.as_ref().map(|dex| dex.index).unwrap_or_default();
let perps = data
.universe
.into_iter()
.enumerate()
.map(|(index, perp)| {
let index = 100_000 * usize::from(dex.is_some()) + dex_index * 10_000 + index;
PerpMarket {
name: perp.name,
index,
max_leverage: perp.max_leverage,
sz_decimals: perp.sz_decimals,
collateral: collateral.clone(),
isolated_margin: perp.only_isolated,
margin_mode: perp.margin_mode,
growth_mode: perp.growth_mode,
aligned_quote_token: perp.aligned_quote_token,
table: PriceTick::for_perp(perp.sz_decimals),
}
})
.collect();
Ok(perps)
}
pub async fn outcome_meta(
core_url: impl IntoUrl,
client: reqwest::Client,
) -> anyhow::Result<OutcomeMeta> {
let mut url = core_url.into_url()?;
url.set_path("/info");
let resp = client
.post(url)
.json(&InfoRequest::OutcomeMeta)
.send()
.await
.context("info")?;
let raw: RawOutcomeMeta = resp.json().await?;
Ok(OutcomeMeta {
outcomes: raw
.outcomes
.into_iter()
.map(|o| OutcomeInfo {
outcome: o.outcome,
name: o.name,
description: o.description,
side_specs: o
.side_specs
.into_iter()
.map(|s| OutcomeSideSpec { name: s.name })
.collect(),
})
.collect(),
questions: raw
.questions
.into_iter()
.map(|q| OutcomeQuestion {
question: q.question,
name: q.name,
description: q.description,
fallback_outcome: q.fallback_outcome,
named_outcomes: q.named_outcomes,
settled_named_outcomes: q.settled_named_outcomes,
})
.collect(),
})
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawOutcomeMeta {
#[serde(default)]
outcomes: Vec<RawOutcomeInfo>,
#[serde(default)]
questions: Vec<RawOutcomeQuestion>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawOutcomeInfo {
outcome: u32,
name: String,
description: String,
side_specs: Vec<RawOutcomeSideSpec>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawOutcomeSideSpec {
name: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawOutcomeQuestion {
question: u32,
name: String,
description: String,
fallback_outcome: Option<u32>,
#[serde(default)]
named_outcomes: Vec<u32>,
#[serde(default)]
settled_named_outcomes: Vec<u32>,
}
fn generate_evm_transfer_address(index: usize) -> Address {
let base = U256::from(0x20) << 152; let addr: U256 = base + U256::from(index);
let bytes = addr.to_be_bytes::<32>();
Address::from_slice(&bytes[12..]) }
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PerpTokens {
universe: Vec<PerpUniverseItem>,
collateral_token: usize,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PerpUniverseItem {
name: String,
max_leverage: u64,
#[serde(default)]
only_isolated: bool,
margin_mode: Option<MarginMode>,
sz_decimals: i64,
#[serde(default, deserialize_with = "deserialize_growth_mode")]
growth_mode: bool,
#[serde(default, alias = "isAlignedQuoteToken", alias = "isQuoteTokenAligned")]
aligned_quote_token: bool,
}
fn deserialize_growth_mode<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"enabled" => Ok(true),
"disabled" => Ok(false),
_ => Err(serde::de::Error::custom(format!(
"invalid growth_mode value: {}",
s
))),
}
}
#[derive(Debug, Copy, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MarginMode {
StrictIsolated,
NoCross,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SpotTokens {
universe: Vec<SpotUniverseItem>,
tokens: Vec<Token>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SpotUniverseItem {
tokens: [u32; 2],
name: String,
index: usize,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Token {
name: String,
index: usize,
token_id: B128,
sz_decimals: i64,
wei_decimals: i64,
evm_contract: Option<EvmContract>,
}
impl From<Token> for SpotToken {
fn from(token: Token) -> Self {
let (evm_contract, cross_chain_address, evm_extra_decimals) =
if let Some(contract) = token.evm_contract {
(
Some(if token.name == "USDC" {
USDC_CONTRACT_IN_EVM
} else {
contract.address
}),
Some(generate_evm_transfer_address(token.index)),
contract.evm_extra_wei_decimals,
)
} else if token.name == "HYPE" {
(
Some(Address::repeat_byte(85)),
Some(Address::repeat_byte(34)),
10,
)
} else {
(None, None, 0)
};
Self {
name: token.name.clone(),
token_id: token.token_id,
index: token.index as u32,
evm_contract,
evm_extra_decimals,
wei_decimals: token.wei_decimals,
cross_chain_address: if token.name == "HYPE" {
Some(Address::repeat_byte(34))
} else {
cross_chain_address
},
sz_decimals: token.sz_decimals,
}
}
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
struct EvmContract {
address: Address,
evm_extra_wei_decimals: i64,
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, sync::Arc, thread};
use alloy::primitives::address;
use super::*;
use crate::hypercore;
#[tokio::test]
async fn test_spot_markets() {
let client = reqwest::Client::new();
let markets = spot_markets("https://api.hyperliquid.xyz", client)
.await
.unwrap();
assert!(!markets.is_empty());
}
#[tokio::test]
async fn test_evm_send_addresses() {
let expected_addresses = HashMap::from([
(
"PURR/USDC",
address!("0x2000000000000000000000000000000000000001"),
),
("@1", address!("0x2000000000000000000000000000000000000002")),
(
"@166",
address!("0x200000000000000000000000000000000000010C"),
),
("@4", address!("0x2000000000000000000000000000000000000005")),
(
"@107",
address!("0x2222222222222222222222222222222222222222"),
),
(
"@250",
address!("0x2000000000000000000000000000000000000079"),
),
(
"@142",
address!("0x20000000000000000000000000000000000000c5"),
),
]);
let spot = hypercore::spot_markets(mainnet_url(), reqwest::Client::new())
.await
.unwrap();
for (key, value) in expected_addresses {
let market = spot.iter().find(|market| market.name == key).unwrap();
let address = market.tokens[0].cross_chain_address.unwrap();
assert_eq!(address, value, "unexpected {address} <> {value}");
}
}
#[tokio::test]
async fn test_http_clearinghouse_state() {
let client = hypercore::mainnet();
let user = address!("0x162cc7c861ebd0c06b3d72319201150482518185");
let state = client.clearinghouse_state(user, None).await.unwrap();
assert!(state.time > 0);
assert!(state.margin_summary.account_value >= rust_decimal::Decimal::ZERO);
}
#[tokio::test]
async fn test_http_user_balances() {
let client = hypercore::mainnet();
let user = address!("0xdfc24b077bc1425ad1dea75bcb6f8158e10df303");
let _balances = client.user_balances(user).await.unwrap();
}
#[tokio::test]
async fn test_http_user_fees() {
let client = hypercore::mainnet();
let user = address!("0xdfc24b077bc1425ad1dea75bcb6f8158e10df303");
let _fees = client.user_fees(user).await.unwrap();
}
#[tokio::test]
async fn test_http_all_mids() {
let client = hypercore::mainnet();
let mids = client.all_mids(None).await.unwrap();
assert!(mids.contains_key("BTC"));
assert!(mids.contains_key("ETH"));
assert!(*mids.get("BTC").unwrap() > rust_decimal::Decimal::ZERO);
}
#[tokio::test]
async fn test_http_open_orders() {
let client = hypercore::mainnet();
let user = address!("0xdfc24b077bc1425ad1dea75bcb6f8158e10df303");
let _orders = client.open_orders(user, None).await.unwrap();
}
#[tokio::test]
async fn test_http_perps() {
let client = hypercore::mainnet();
let perps = client.perps().await.unwrap();
assert!(!perps.is_empty());
assert!(perps.iter().any(|m| m.name == "BTC"));
assert!(perps.iter().any(|m| m.name == "ETH"));
}
#[tokio::test]
async fn test_http_spot() {
let client = hypercore::mainnet();
let spots = client.spot().await.unwrap();
assert!(!spots.is_empty());
}
#[test]
fn test_nonce_handler_uniqueness_single_thread() {
let handler = NonceHandler::default();
let mut nonces = std::collections::HashSet::new();
for _ in 0..10_000 {
let nonce = handler.next();
assert!(nonces.insert(nonce), "Duplicate nonce detected: {nonce}");
}
}
#[test]
fn test_nonce_handler_uniqueness_concurrent() {
let handler = Arc::new(NonceHandler::default());
let num_threads = 32;
let nonces_per_thread = 1_000_000;
let barrier = Arc::new(std::sync::Barrier::new(num_threads));
let handles: Vec<_> = (0..num_threads)
.map(|_| {
let handler = Arc::clone(&handler);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
let mut nonces = Vec::with_capacity(nonces_per_thread);
for _ in 0..nonces_per_thread {
nonces.push(handler.next());
}
nonces
})
})
.collect();
let mut all_nonces = std::collections::HashSet::new();
for handle in handles {
let nonces = handle.join().unwrap();
for nonce in nonces {
assert!(
all_nonces.insert(nonce),
"Duplicate nonce detected in concurrent test: {nonce}"
);
}
}
assert_eq!(all_nonces.len(), num_threads * nonces_per_thread);
}
#[test]
fn test_nonce_handler_stale_nonce_race_condition() {
use std::sync::atomic::Ordering;
let handler = Arc::new(NonceHandler::default());
let old_nonce = 1000u64;
handler.nonce.store(old_nonce, Ordering::SeqCst);
let num_threads = 16;
let nonces_per_thread = 1000;
let barrier = Arc::new(std::sync::Barrier::new(num_threads));
let handles: Vec<_> = (0..num_threads)
.map(|_| {
let handler = Arc::clone(&handler);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
let mut nonces = Vec::with_capacity(nonces_per_thread);
for _ in 0..nonces_per_thread {
nonces.push(handler.next());
}
nonces
})
})
.collect();
let mut all_nonces = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for handle in handles {
let nonces = handle.join().unwrap();
for nonce in nonces {
if !all_nonces.insert(nonce) {
duplicates.push(nonce);
}
}
}
assert!(
duplicates.is_empty(),
"Found {} duplicate nonces when triggering stale nonce reset: {:?}",
duplicates.len(),
&duplicates[..duplicates.len().min(10)]
);
}
#[tokio::test]
async fn test_http_outcome_meta_mainnet() {
let client = hypercore::mainnet();
let meta = client.outcome_meta().await.unwrap();
let _ = meta.outcomes.len();
}
#[tokio::test]
async fn test_http_outcome_meta_testnet() {
let client = hypercore::testnet();
let meta = client.outcome_meta().await.unwrap();
assert!(!meta.outcomes.is_empty());
for o in &meta.outcomes {
assert_eq!(o.side_specs.len(), 2, "outcome {} should have 2 sides", o.outcome);
}
}
#[test]
fn outcome_meta_deserialize() {
let json = r#"{
"outcomes": [
{
"outcome": 1273,
"name": "Recurring",
"description": "class:priceBinary|underlying:BTC|expiry:20260317-0300|targetPrice:74212|period:1d",
"sideSpecs": [{"name": "Yes"}, {"name": "No"}]
},
{
"outcome": 9,
"name": "Who will win the HL 100 meter dash?",
"description": "This race is yet to be scheduled.",
"sideSpecs": [{"name": "Hypurr"}, {"name": "Usain Bolt"}]
}
],
"questions": [
{
"question": 1,
"name": "What will Hypurr eat?",
"description": "Food journal.",
"fallbackOutcome": 13,
"namedOutcomes": [10, 11, 12],
"settledNamedOutcomes": []
}
]
}"#;
let meta: RawOutcomeMeta = serde_json::from_str(json).unwrap();
assert_eq!(meta.outcomes.len(), 2);
assert_eq!(meta.outcomes[0].outcome, 1273);
assert_eq!(meta.outcomes[0].name, "Recurring");
assert_eq!(meta.outcomes[0].side_specs.len(), 2);
assert_eq!(meta.outcomes[0].side_specs[0].name, "Yes");
assert_eq!(meta.outcomes[1].side_specs[1].name, "Usain Bolt");
assert_eq!(meta.questions.len(), 1);
assert_eq!(meta.questions[0].question, 1);
assert_eq!(meta.questions[0].fallback_outcome, Some(13));
assert_eq!(meta.questions[0].named_outcomes, vec![10, 11, 12]);
}
#[test]
fn outcome_meta_empty() {
let json = r#"{"outcomes": [], "questions": []}"#;
let meta: RawOutcomeMeta = serde_json::from_str(json).unwrap();
assert!(meta.outcomes.is_empty());
assert!(meta.questions.is_empty());
}
}