df-hyperliquid-core 0.0.9-beta

Hyperliquid SDK for Rust: markets, account, trading, transfers, earn and deploy, with exact decimals and signing. Native and wasm.
Documentation
//! What each constructor refuses, and settlements checked against an `outcomeMeta` read. A whole
//! venue run through a client is in `tests/mocked/deploy.rs`.

use super::*;
use crate::deploy::outcomes::actions::support::{instance, listing, row, venue};
use crate::markets::outcomes::meta::{Question, SideSpec};

/// The venue `out` as live ones are: a standalone outcome, and question 350 of three named
/// outcomes and a fallback, the first named one settled (still listed here, as a read taken
/// mid-settlement can show it).
fn out() -> OutcomeMeta {
    OutcomeMeta {
        questions: vec![Question {
            question: 350,
            name: "template:sportsContestResult".to_owned(),
            description: "participantA:Denmark|participantB:Portugal".to_owned(),
            fallback_outcome: Some(1472),
            named_outcomes: vec![1473, 1474, 1475],
            settled_named_outcomes: vec![1473],
        }],
        ..listing(vec![
            row(
                1209,
                "template:priceTouch",
                "perp:HYPE|target:100|time:20261001-0000",
                ["template:Yes", "template:No"],
            ),
            row(1472, "template fallback", "other", ["Yes", "No"]),
            row(1473, "template:sportsContestParticipant2", "participant:Denmark", ["Yes", "No"]),
            row(1474, "template:sportsContestParticipant2", "participant:Portugal", ["Yes", "No"]),
            row(1475, "template:sportsContestDraw2", "", ["Yes", "No"]),
        ])
    }
}

/// An outcome settles with its name, description and side names copied exactly as listed, trailing
/// spaces and all; one no question holds settles to any fraction. Testnet, 2026-10-08: question
/// 823's named outcomes were settled one at a time at 0 as each team went out, the question staying
/// live; a question's fallback never settles alone.
#[test]
fn an_outcome_settles_alone_as_listed_and_a_question_s_leg_only_at_0_or_1() {
    let listed = listing(vec![row(7, "Chicken  ", " N/A \t", ["Yes ", " No"])]);
    let copied = OutcomeDeploy::settle_outcome(venue("ab"), &listed, 7, "0.25".parse().unwrap()).unwrap();
    assert_eq!(
        copied.wire(),
        r#"{"type":"outcomeDeploy","venue":"ab","operation":{"settleOutcome":{"outcome":7,"settleFraction":"0.25","details":"","nameAndDescription":["Chicken  "," N/A \t"],"sideNames":["Yes "," No"]}}}"#
    );
    let meta = out();
    let settle = |outcome, fraction: &str| {
        OutcomeDeploy::settle_outcome(venue("out"), &meta, outcome, fraction.parse().unwrap())
    };
    let leg = settle(1474, "0").unwrap();
    assert!(leg.wire().contains(r#""outcome":1474,"settleFraction":"0""#));
    assert_eq!(settle(1474, "0.5"), Err(DeployError::NotWhole(1474)));
    assert_eq!(settle(1472, "0"), Err(DeployError::Fallback { outcome: 1472, question: 350 }));
    assert_eq!(
        DeployError::Fallback { outcome: 1472, question: 350 }.to_string(),
        "outcome 1472 is question 350's fallback, which settles with its question"
    );
    assert!(settle(1209, "0.25").is_ok(), "an outcome no question holds settles to any fraction");
    assert_eq!(settle(4242, "0"), Err(DeployError::NotListed(4242)));
    let one_sided =
        listing(vec![Outcome { side_specs: vec![SideSpec { name: "Yes".to_owned() }], ..row(9, "x", "", ["", ""]) }]);
    assert_eq!(
        OutcomeDeploy::settle_outcome(venue("out"), &one_sided, 9, Fraction::ONE),
        Err(DeployError::NotTwoSided(9))
    );
}

#[test]
fn a_question_settles_its_open_legs_one_winning_the_settled_one_left_out() {
    let meta = out();
    let settle = |question, winner| OutcomeDeploy::settle_question(venue("out"), &meta, question, winner);
    assert_eq!(
        settle(350, 1475).unwrap().wire(),
        r#"{"type":"outcomeDeploy","venue":"out","operation":{"settleQuestion2":{"question":350,"outcomeSettlements":[{"outcome":1474,"settleFraction":"0","details":"","nameAndDescription":["template:sportsContestParticipant2","participant:Portugal"],"sideNames":["Yes","No"]},{"outcome":1475,"settleFraction":"1","details":"","nameAndDescription":["template:sportsContestDraw2",""],"sideNames":["Yes","No"]}],"nameAndDescription":["template:sportsContestResult","participantA:Denmark|participantB:Portugal"]}}}"#
    );
    assert_eq!(settle(350, 1473), Err(DeployError::NotOpen { outcome: 1473, question: 350 }), "already settled");
    assert_eq!(settle(350, 1472), Err(DeployError::NotOpen { outcome: 1472, question: 350 }), "the fallback");
    assert_eq!(settle(351, 1475), Err(DeployError::NoQuestion(351)));
    let mut gone = out();
    gone.outcomes.retain(|row| row.outcome != 1474);
    assert_eq!(
        OutcomeDeploy::settle_question(venue("out"), &gone, 350, 1475),
        Err(DeployError::NotListed(1474)),
        "a leg the read does not list"
    );
    let mut twice = out();
    twice.questions[0].named_outcomes.push(1474);
    assert_eq!(OutcomeDeploy::settle_question(venue("out"), &twice, 350, 1475), Err(DeployError::SettledTwice(1474)));
}

#[test]
fn a_question_needs_a_named_outcome_and_a_grant_list_a_grant() {
    assert_eq!(
        OutcomeDeploy::register_question(venue("zzz"), instance("sportsContestResult", &[]), FeeScale::ZERO, vec![]),
        Err(DeployError::NoNamedOutcomes)
    );
    assert_eq!(OutcomeDeploy::set_sub_deployers(venue("zzz"), vec![]), Err(DeployError::NoGrants));
}