hadron-sdk 0.2.1

Rust client SDK for the Hadron protocol
Documentation
use solana_sdk::pubkey::Pubkey;

use crate::constants::*;
use crate::types::*;

fn read_pubkey(data: &[u8], offset: usize) -> Pubkey {
    Pubkey::new_from_array(data[offset..offset + 32].try_into().unwrap())
}

fn read_u64_le(data: &[u8], offset: usize) -> u64 {
    u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap())
}

fn read_u32_le(data: &[u8], offset: usize) -> u32 {
    u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
}

fn read_u16_le(data: &[u8], offset: usize) -> u16 {
    u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap())
}

// ============================================================================
// Config (248 bytes)
// ============================================================================

pub fn decode_config(data: &[u8]) -> Result<DecodedConfig, HadronSdkError> {
    if data.len() < CONFIG_SIZE {
        return Err(HadronSdkError::DataTooShort {
            expected: CONFIG_SIZE,
            actual: data.len(),
        });
    }

    let mut offset = 0;

    let state = PoolState::try_from(data[offset])?;
    offset += 1;

    let seed = read_u64_le(data, offset);
    offset += 8;

    let authority = read_pubkey(data, offset);
    offset += 32;

    let mint_x = read_pubkey(data, offset);
    offset += 32;

    let mint_y = read_pubkey(data, offset);
    offset += 32;

    let config_bump = data[offset];
    offset += 1;

    let curve_meta = read_pubkey(data, offset);
    offset += 32;

    let spread_config_initialized = data[offset] != 0;
    offset += 1;

    let delta_staleness = data[offset];
    offset += 1;

    let oracle_mode = OracleMode::try_from(data[offset])?;
    offset += 1;

    let has_pool_fee = data[offset] != 0;
    offset += 1;

    // _padding [u8; 2]
    offset += 2;

    let pending_authority = read_pubkey(data, offset);
    offset += 32;

    let nomination_expiry = read_u64_le(data, offset);
    offset += 8;

    let token_program_x = read_pubkey(data, offset);
    offset += 32;

    let token_program_y = read_pubkey(data, offset);
    let _ = offset; // suppress unused warning

    Ok(DecodedConfig {
        state,
        seed,
        authority,
        mint_x,
        mint_y,
        config_bump,
        curve_meta,
        spread_config_initialized,
        delta_staleness,
        oracle_mode,
        has_pool_fee,
        pending_authority,
        nomination_expiry,
        token_program_x,
        token_program_y,
    })
}

// ============================================================================
// MidpriceOracle (64 bytes)
// ============================================================================

pub fn decode_midprice_oracle(data: &[u8]) -> Result<DecodedMidpriceOracle, HadronSdkError> {
    if data.len() < MIDPRICE_ORACLE_SIZE {
        return Err(HadronSdkError::DataTooShort {
            expected: MIDPRICE_ORACLE_SIZE,
            actual: data.len(),
        });
    }

    Ok(DecodedMidpriceOracle {
        authority: read_pubkey(data, 0),
        sequence: read_u64_le(data, 32),
        midprice_q32: read_u64_le(data, 40),
        spread_factor_q32: read_u64_le(data, 48),
        last_update_slot: read_u64_le(data, 56),
    })
}

// ============================================================================
// CurveMeta (48 bytes)
// ============================================================================

pub fn decode_curve_meta(data: &[u8]) -> Result<DecodedCurveMeta, HadronSdkError> {
    if data.len() < CURVE_META_SIZE {
        return Err(HadronSdkError::DataTooShort {
            expected: CURVE_META_SIZE,
            actual: data.len(),
        });
    }

    let mut initialized_slots = [0u8; 8];
    initialized_slots.copy_from_slice(&data[36..44]);

    Ok(DecodedCurveMeta {
        authority: read_pubkey(data, 0),
        active_price_bid_slot: data[32],
        active_price_ask_slot: data[33],
        active_risk_bid_slot: data[34],
        active_risk_ask_slot: data[35],
        initialized_slots,
        max_prefab_slots: data[44],
        max_curve_points: data[45],
    })
}

/// Check if a specific slot is initialized in the CurveMeta bitmap.
pub fn is_slot_initialized(
    initialized_slots: &[u8; 8],
    curve_type: CurveType,
    slot: u8,
    max_slots: u8,
) -> bool {
    let bit_index = (curve_type as u32) * (max_slots as u32) + (slot as u32);
    let byte_index = (bit_index / 8) as usize;
    let bit_offset = bit_index % 8;
    if byte_index >= 8 {
        return false;
    }
    (initialized_slots[byte_index] & (1 << bit_offset)) != 0
}

// ============================================================================
// FeeConfig (72 bytes)
// ============================================================================

pub fn decode_fee_config(data: &[u8]) -> Result<DecodedFeeConfig, HadronSdkError> {
    if data.len() < FEE_CONFIG_SIZE {
        return Err(HadronSdkError::DataTooShort {
            expected: FEE_CONFIG_SIZE,
            actual: data.len(),
        });
    }

    Ok(DecodedFeeConfig {
        initialized: data[0] != 0,
        fee_ppm: read_u32_le(data, 1),
        bump: data[5],
        // skip 2 bytes padding at offset 6-7
        fee_admin: read_pubkey(data, 8),
        fee_recipient: read_pubkey(data, 40),
    })
}

// ============================================================================
// SpreadConfig (variable size)
// ============================================================================

const SPREAD_TRIGGER_LEN: usize = 40; // 32 (account) + 2 (spread_bps) + 6 (padding)

pub fn decode_spread_config(data: &[u8]) -> Result<DecodedSpreadConfig, HadronSdkError> {
    if data.len() < 72 {
        return Err(HadronSdkError::DataTooShort {
            expected: 72,
            actual: data.len(),
        });
    }

    let initialized = data[0] != 0;
    let bump = data[1];
    let num_triggers = data[2];
    // skip 5 bytes padding at offset 3-7
    let admin = read_pubkey(data, 8);
    let config = read_pubkey(data, 40);

    let mut triggers = Vec::with_capacity(num_triggers as usize);
    let triggers_start = 72;
    for i in 0..num_triggers as usize {
        let off = triggers_start + i * SPREAD_TRIGGER_LEN;
        if off + SPREAD_TRIGGER_LEN > data.len() {
            break;
        }
        triggers.push(SpreadTriggerInput {
            account: read_pubkey(data, off),
            spread_bps: read_u16_le(data, off + 32),
        });
    }

    Ok(DecodedSpreadConfig {
        initialized,
        bump,
        num_triggers,
        admin,
        config,
        triggers,
    })
}

// ============================================================================
// CurveUpdates (258 bytes)
// ============================================================================

pub fn decode_curve_updates(data: &[u8]) -> Result<DecodedCurveUpdates, HadronSdkError> {
    if data.len() < CURVE_UPDATES_SIZE {
        return Err(HadronSdkError::DataTooShort {
            expected: CURVE_UPDATES_SIZE,
            actual: data.len(),
        });
    }

    let authority = read_pubkey(data, 0);
    let curve_meta = read_pubkey(data, 32);
    let num_ops = data[64];
    // skip 1 byte padding at offset 65

    let ops_start = 66;
    let mut ops = Vec::with_capacity(num_ops.min(MAX_CURVE_UPDATE_OPS) as usize);
    for i in 0..num_ops.min(MAX_CURVE_UPDATE_OPS) as usize {
        let off = ops_start + i * CURVE_UPDATE_OP_SIZE;
        ops.push(CurveUpdateOp {
            curve_type: CurveType::try_from(data[off])?,
            op_kind: CurveUpdateOpKind::try_from(data[off + 1])?,
            point_index: data[off + 2],
            interpolation: Interpolation::try_from(data[off + 3])?,
            amount_in: read_u64_le(data, off + 4),
            price_factor_q32: read_u64_le(data, off + 12),
            params: data[off + 20..off + 24].try_into().unwrap(),
        });
    }

    Ok(DecodedCurveUpdates {
        authority,
        curve_meta,
        num_ops,
        ops,
    })
}

// ============================================================================
// CurvePrefabs — decode individual curve sides
// ============================================================================

/// Decode a single curve side from a CurvePrefabs account.
pub fn decode_curve_side(
    data: &[u8],
    curve_type: CurveType,
    slot: u8,
    max_slots: u8,
    max_points: u8,
) -> Result<CurveSide, HadronSdkError> {
    let side_size = CURVE_SIDE_HEADER + (max_points as usize) * CURVE_POINT_LEN;
    let offset =
        32 + ((curve_type as usize) * (max_slots as usize) + (slot as usize)) * side_size;

    let end = offset + side_size;
    if data.len() < end {
        return Err(HadronSdkError::DataTooShort {
            expected: end,
            actual: data.len(),
        });
    }

    let num_points = data[offset];
    let default_interpolation = Interpolation::try_from(data[offset + 1])?;
    let x_mode = CurveXMode::try_from(data[offset + 2])?;
    let risk_mode = RiskMode::try_from(data[offset + 3])?;

    let points_start = offset + CURVE_SIDE_HEADER;
    let n = (num_points as usize).min(max_points as usize);
    let mut points = Vec::with_capacity(n);
    for i in 0..n {
        let p_off = points_start + i * CURVE_POINT_LEN;
        points.push(DecodedCurvePoint {
            amount_in: read_u64_le(data, p_off),
            price_factor_q32: read_u64_le(data, p_off + 8),
            interpolation: Interpolation::try_from(data[p_off + 16])?,
            params: data[p_off + 17..p_off + 21].try_into().unwrap(),
        });
    }

    Ok(CurveSide {
        num_points,
        default_interpolation,
        x_mode,
        risk_mode,
        points,
    })
}

/// Decode all active curve slots from a CurvePrefabs account using CurveMeta info.
pub fn decode_active_curves(
    prefabs_data: &[u8],
    meta: &DecodedCurveMeta,
) -> Result<ActiveCurves, HadronSdkError> {
    let ms = meta.max_prefab_slots;
    let mp = meta.max_curve_points;
    Ok(ActiveCurves {
        price_bid: decode_curve_side(
            prefabs_data,
            CurveType::PriceBid,
            meta.active_price_bid_slot,
            ms,
            mp,
        )?,
        price_ask: decode_curve_side(
            prefabs_data,
            CurveType::PriceAsk,
            meta.active_price_ask_slot,
            ms,
            mp,
        )?,
        risk_bid: decode_curve_side(
            prefabs_data,
            CurveType::RiskBid,
            meta.active_risk_bid_slot,
            ms,
            mp,
        )?,
        risk_ask: decode_curve_side(
            prefabs_data,
            CurveType::RiskAsk,
            meta.active_risk_ask_slot,
            ms,
            mp,
        )?,
    })
}