acta-maker-sdk 0.5.1

Rust SDK for Acta options market makers — WebSocket client, order signing, message types
Documentation
use std::time::{Duration, Instant};

use solana_sdk::pubkey::Pubkey;

use super::{
    CapitalStep, VaultPhase, VaultSafety, VaultView, WindowClock, WindowPolicy, WindowTracker,
    WithdrawalPlan, next_step, plan_withdrawals,
};
use VaultPhase::{Active, Idle, ProcessingCapital, Settling};
use VaultSafety::{EmergencyUnwind, Frozen, Normal, WithdrawOnly};

fn view(phase: VaultPhase, safety: VaultSafety) -> VaultView {
    VaultView {
        phase,
        safety,
        equity_computed: false,
        accepting_deposits: false,
        pending_deposits: 0,
        pending_withdrawals: 0,
    }
}

const POLICY: WindowPolicy = WindowPolicy {
    reopen: true,
    deposit_window: Duration::from_secs(180),
    trading_window: Duration::from_secs(600),
};
const NO_REOPEN: WindowPolicy = WindowPolicy {
    reopen: false,
    ..POLICY
};
const FRESH: WindowClock = WindowClock {
    open_for: None,
    closed_for: None,
};

fn open_for(secs: u64) -> WindowClock {
    WindowClock {
        open_for: Some(Duration::from_secs(secs)),
        closed_for: None,
    }
}

fn closed_for(secs: u64) -> WindowClock {
    WindowClock {
        open_for: None,
        closed_for: Some(Duration::from_secs(secs)),
    }
}

fn key(seed: u8) -> Pubkey {
    Pubkey::new_from_array([seed; 32])
}

#[test]
fn processing_capital_walks_equity_withdrawals_finalize_even_when_frozen() {
    for safety in [Normal, Frozen] {
        let mut v = view(ProcessingCapital, safety);
        v.pending_withdrawals = 2;
        assert_eq!(
            next_step(v, FRESH, POLICY),
            Some(CapitalStep::ComputeEquity),
            "{safety:?}"
        );
        v.equity_computed = true;
        assert_eq!(
            next_step(v, FRESH, POLICY),
            Some(CapitalStep::ProcessWithdrawals),
            "{safety:?}"
        );
        v.pending_withdrawals = 0;
        assert_eq!(
            next_step(v, FRESH, POLICY),
            Some(CapitalStep::FinalizeCycle),
            "{safety:?}"
        );
    }
}

#[test]
fn idle_pending_closes_the_window_then_prices_then_drains() {
    let mut v = view(Idle, Normal);
    v.accepting_deposits = true;
    v.pending_deposits = 3;
    assert_eq!(
        next_step(v, open_for(1), POLICY),
        Some(CapitalStep::CloseDeposits)
    );
    v.accepting_deposits = false;
    assert_eq!(
        next_step(v, closed_for(1), POLICY),
        Some(CapitalStep::ComputeEquity)
    );
    v.equity_computed = true;
    v.pending_withdrawals = 1;
    assert_eq!(
        next_step(v, closed_for(1), POLICY),
        Some(CapitalStep::ProcessDeposits)
    );
    v.pending_deposits = 0;
    assert_eq!(
        next_step(v, closed_for(1), POLICY),
        Some(CapitalStep::ProcessWithdrawals)
    );
}

#[test]
fn idle_drained_window_alternates_on_the_configured_cadence() {
    let closed = view(Idle, Normal);
    assert_eq!(next_step(closed, closed_for(599), POLICY), None);
    assert_eq!(
        next_step(closed, closed_for(600), POLICY),
        Some(CapitalStep::OpenDeposits)
    );
    assert_eq!(next_step(closed, closed_for(600), NO_REOPEN), None);
    assert_eq!(next_step(closed, FRESH, POLICY), None);
    let mut open = closed;
    open.accepting_deposits = true;
    assert_eq!(next_step(open, open_for(179), POLICY), None);
    assert_eq!(
        next_step(open, open_for(180), POLICY),
        Some(CapitalStep::CloseDeposits)
    );
}

#[test]
fn window_clock_restarts_when_the_observed_state_flips() {
    let mut state = WindowTracker::default();
    let t0 = Instant::now();
    assert_eq!(state.observe(false, t0), closed_for(0));
    let later = t0 + Duration::from_secs(700);
    assert_eq!(state.observe(false, later), closed_for(700));
    assert_eq!(state.observe(true, later), open_for(0));
    let much_later = later + Duration::from_secs(200);
    assert_eq!(state.observe(true, much_later), open_for(200));
}

#[test]
fn withdraw_only_prioritizes_payouts_then_retries_isolated_refunds() {
    let mut v = view(Idle, WithdrawOnly);
    v.pending_withdrawals = 1;
    v.pending_deposits = 1;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ProcessWithdrawals)
    );
    v.pending_withdrawals = 0;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ProcessDeposits)
    );
    v.pending_deposits = 0;
    assert_eq!(next_step(v, FRESH, POLICY), None);
}

#[test]
fn emergency_active_and_settling_are_left_alone() {
    for (phase, safety) in [
        (Idle, EmergencyUnwind),
        (ProcessingCapital, EmergencyUnwind),
        (ProcessingCapital, WithdrawOnly),
        (Active, Normal),
        (Settling, Normal),
    ] {
        let mut v = view(phase, safety);
        v.pending_withdrawals = 1;
        assert_eq!(
            next_step(v, closed_for(1000), POLICY),
            None,
            "{phase:?}/{safety:?}"
        );
    }
}

#[test]
fn frozen_idle_prices_and_processes_withdrawals_without_deposits_or_reopening() {
    let mut v = view(Idle, Frozen);
    v.pending_withdrawals = 1;
    v.pending_deposits = 1;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ProcessDeposits)
    );
    v.pending_deposits = 0;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ComputeEquity)
    );
    v.equity_computed = true;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ProcessWithdrawals)
    );
    v.pending_withdrawals = 0;
    assert_eq!(next_step(v, closed_for(1000), POLICY), None);
    v.pending_deposits = 0;
    v.equity_computed = false;
    assert_eq!(next_step(v, closed_for(1000), POLICY), None);
}

#[test]
fn sole_holder_exiting_takes_the_final_redemption_path() {
    let plan = plan_withdrawals(&[(key(1), 100)], 100);
    assert_eq!(plan.final_user, Some(key(1)));
    assert!(plan.partial.is_empty());
}

#[test]
fn draining_queue_holds_back_the_largest_holder() {
    let plan = plan_withdrawals(&[(key(1), 60), (key(2), 40)], 100);
    assert_eq!(plan.partial, vec![key(2)]);
    assert_eq!(plan.final_user, None);
    let plan = plan_withdrawals(&[(key(3), 20), (key(1), 50), (key(2), 30)], 100);
    assert_eq!(plan.partial, vec![key(2), key(3)]);
}

#[test]
fn partial_exits_are_batched_without_holdback() {
    let plan = plan_withdrawals(&[(key(1), 10), (key(2), 20)], 100);
    assert_eq!(plan.partial, vec![key(1), key(2)]);
    assert_eq!(plan.final_user, None);
    assert_eq!(plan_withdrawals(&[], 100), WithdrawalPlan::default());
}

#[test]
fn mixed_cycle_resolves_deposits_before_supply_draining_withdrawal() {
    let mut v = view(ProcessingCapital, Normal);
    v.pending_deposits = 1;
    v.pending_withdrawals = 1;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ComputeEquity)
    );
    v.equity_computed = true;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ProcessDeposits)
    );
    assert_eq!(
        next_step(v, FRESH, NO_REOPEN),
        Some(CapitalStep::ProcessDeposits)
    );
    v.pending_deposits = 0;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::ProcessWithdrawals)
    );
    v.pending_withdrawals = 0;
    assert_eq!(
        next_step(v, FRESH, POLICY),
        Some(CapitalStep::FinalizeCycle)
    );
}

#[test]
fn frozen_deposits_refund_before_other_capital_steps() {
    for phase in [Idle, Active, Settling, ProcessingCapital] {
        for computed in [false, true] {
            for withdrawals in [0, 1] {
                let mut v = view(phase, Frozen);
                v.pending_deposits = 1;
                v.pending_withdrawals = withdrawals;
                v.equity_computed = computed;
                assert_eq!(
                    next_step(v, FRESH, POLICY),
                    Some(CapitalStep::ProcessDeposits)
                );
                v.pending_deposits = 0;
                if matches!(phase, Active | Settling) {
                    assert_eq!(next_step(v, FRESH, POLICY), None);
                }
            }
        }
    }
}

#[test]
fn listing_strings_parse_into_the_view_enums() {
    assert_eq!("processing_capital".parse(), Ok(ProcessingCapital));
    assert_eq!("withdraw_only".parse(), Ok(WithdrawOnly));
    assert_eq!("closing".parse(), Ok(VaultSafety::Closing));
    assert!("unknown".parse::<VaultPhase>().is_err());
    assert_eq!("emergency_unwind".parse(), Ok(EmergencyUnwind));
}