txline 0.1.0

Devnet-only Rust SDK for TxLINE.
Documentation
use txline::http::models::UpdateStats;
use txline::validation::legacy::{ScoreStat, ScoresBatchSummary};
use txline::validation::proof::Hash32;
use txline::validation::strategy::{
    BinaryExpression, Comparison, NDimensionalStrategy, TraderPredicate,
};
use txline::validation::v2::{ScoresStatValidationV2, ScoresStatValidationV2Response};
use txline::{ApiToken, GuestJwt, TxlineClient, TxlineConfig};

#[tokio::test]
async fn rejects_seq_zero_before_auth_or_network() {
    let client = TxlineClient::new(TxlineConfig::devnet()).unwrap();
    let err = client
        .scores()
        .stat_validation_legacy(17952170, 0, 1002, None)
        .await
        .unwrap_err();
    assert!(err.to_string().contains("seq must be greater than zero"));
}

#[test]
fn v2_preserves_requested_stat_key_order() {
    let validation =
        ScoresStatValidationV2::from_response(vec![1001, 1002], response_with(2)).unwrap();
    assert_eq!(validation.requested_stat_keys(), &[1001, 1002]);
    assert_eq!(validation.stats_to_prove()[0].key, 1001);
    assert_eq!(validation.stats_to_prove()[1].key, 1002);
    assert_eq!(validation.to_validation_input().stats.len(), 2);
}

#[test]
fn v2_rejects_length_mismatch() {
    let err = ScoresStatValidationV2::from_response(vec![1001, 1002, 1007], response_with(2))
        .unwrap_err();
    assert!(err.to_string().contains("statsToProve length"));
}

#[test]
fn v2_rejects_stat_key_order_mismatch() {
    let mut response = response_with(2);
    response.stats_to_prove.swap(0, 1);

    let err = ScoresStatValidationV2::from_response(vec![1001, 1002], response).unwrap_err();
    assert!(err.to_string().contains("statsToProve[0].key"));
    assert!(err.to_string().contains("requested statKeys[0]"));
}

#[test]
fn v2_validation_input_ts_uses_summary_min_timestamp() {
    let response_ts = 1_781_200_000_000;
    let min_timestamp = 1_781_123_456_789;
    let validation = ScoresStatValidationV2::from_response(
        vec![1001, 1002],
        response_with_timestamps(2, response_ts, min_timestamp),
    )
    .unwrap();

    assert_ne!(
        validation.response().ts,
        validation.response().summary.update_stats.min_timestamp
    );
    assert_eq!(validation.target_ts(), min_timestamp);
    assert_eq!(validation.to_validation_input().ts, min_timestamp);
}

#[test]
fn v2_epoch_day_and_validation_input_ts_use_same_timestamp() {
    let min_timestamp = 20_624_i64 * 86_400_000 + 12_345;
    let response_ts = min_timestamp + 86_400_000;
    let validation = ScoresStatValidationV2::from_response(
        vec![1001],
        response_with_timestamps(1, response_ts, min_timestamp),
    )
    .unwrap();

    let input_epoch_day = (validation.to_validation_input().ts / 86_400_000) as u16;
    assert_eq!(validation.epoch_day().unwrap(), input_epoch_day);
}

#[test]
fn strategy_builder_rejects_out_of_bounds_indices() {
    let predicate = TraderPredicate::new(0, Comparison::equal_to());
    let err = NDimensionalStrategy::builder(2)
        .binary(0, 2, BinaryExpression::subtract(), predicate)
        .unwrap_err();
    assert!(err.to_string().contains("out of bounds"));
}

#[test]
fn client_activation_preimage_uses_stored_jwt() {
    let client = TxlineClient::new(TxlineConfig::devnet()).unwrap();
    client.set_guest_jwt(GuestJwt::new("jwt").unwrap());
    client.set_api_token(ApiToken::new("api").unwrap());
    assert_eq!(
        client.activation_preimage("abc", &[1, 2]).unwrap(),
        "abc:1,2:jwt"
    );
}

fn response_with(count: usize) -> ScoresStatValidationV2Response {
    response_with_timestamps(count, 1, 1)
}

fn response_with_timestamps(
    count: usize,
    response_ts: i64,
    min_timestamp: i64,
) -> ScoresStatValidationV2Response {
    let hash = Hash32::from_bytes([9u8; 32]).unwrap();
    ScoresStatValidationV2Response {
        ts: response_ts,
        stats_to_prove: (0..count)
            .map(|idx| ScoreStat {
                key: 1001 + idx as u32,
                value: idx as i32,
                period: 0,
            })
            .collect(),
        event_stat_root: hash,
        summary: ScoresBatchSummary {
            fixture_id: 1,
            update_stats: UpdateStats {
                update_count: 1,
                min_timestamp,
                max_timestamp: response_ts.max(min_timestamp),
            },
            event_stats_sub_tree_root: hash,
        },
        stat_proofs: vec![Vec::new(); count],
        sub_tree_proof: Vec::new(),
        main_tree_proof: Vec::new(),
    }
}