satrush-client 0.1.14

Rust client to interact with SatRush's on-chain program.
Documentation
use super::SATRUSH_ID;
use solana_pubkey::Pubkey;

pub fn get_satrush_config_address() -> (Pubkey, u8) {
    let seeds = &[b"satrush_config".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

/// Anchor's event-CPI authority PDA — the extra signer every `#[event_cpi]`
/// instruction takes alongside the program account.
pub fn get_event_authority_address() -> (Pubkey, u8) {
    let seeds = &[b"__event_authority".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_board_address() -> (Pubkey, u8) {
    let seeds = &[b"board".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_round_address(round_id: u32) -> (Pubkey, u8) {
    let seeds = &[b"round".as_ref(), &round_id.to_le_bytes()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_public_deployment_address(authority: Pubkey, round_id: u32) -> (Pubkey, u8) {
    let seeds = &[
        b"public_deployment".as_ref(),
        &authority.to_bytes(),
        &round_id.to_le_bytes(),
    ];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_miner_address(authority: Pubkey) -> (Pubkey, u8) {
    let seeds = &[b"miner".as_ref(), &authority.to_bytes()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_sats_vault_address() -> (Pubkey, u8) {
    let seeds = &[b"sats_vault".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_token_vault_address() -> (Pubkey, u8) {
    let seeds = &[b"token_vault".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_epoch_vault_address() -> (Pubkey, u8) {
    let seeds = &[b"epoch_vault".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_epoch_vault_iteration_address(iteration_id: u32) -> (Pubkey, u8) {
    let seeds = &[b"epoch_vault_iteration".as_ref(), &iteration_id.to_le_bytes()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_epoch_vault_entry_address(iteration_id: u32, authority: Pubkey) -> (Pubkey, u8) {
    let seeds = &[
        b"epoch_vault_entry".as_ref(),
        &iteration_id.to_le_bytes(),
        &authority.to_bytes(),
    ];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_epoch_vault_page_address(iteration_id: u32, page_index: u16) -> (Pubkey, u8) {
    let seeds = &[
        b"epoch_vault_page".as_ref(),
        &iteration_id.to_le_bytes(),
        &page_index.to_le_bytes(),
    ];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_one_btc_vault_address() -> (Pubkey, u8) {
    let seeds = &[b"one_btc_vault".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_one_btc_vault_iteration_address(iteration_id: u32) -> (Pubkey, u8) {
    let seeds = &[b"one_btc_vault_iteration".as_ref(), &iteration_id.to_le_bytes()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_treasury_address() -> (Pubkey, u8) {
    let seeds = &[b"treasury".as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

pub fn get_public_automation_address(authority: Pubkey) -> (Pubkey, u8) {
    let seeds = &[b"public_automation".as_ref(), &authority.to_bytes()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

/// Affiliate tag rules, mirroring the program's `Affiliate::validate_tag`:
/// 3–16 bytes of `a-z`, `0-9`, `_` or `-`. Checking client-side turns an
/// invalid tag into an error here instead of an on-chain rejection — or, past
/// the 32-byte seed limit, a panic inside `find_program_address`.
pub const AFFILIATE_TAG_MIN_LEN: usize = 3;
pub const AFFILIATE_TAG_MAX_LEN: usize = 16;

pub fn validate_affiliate_tag(tag: &str) -> Result<(), String> {
    if tag.len() < AFFILIATE_TAG_MIN_LEN || tag.len() > AFFILIATE_TAG_MAX_LEN {
        return Err(format!(
            "affiliate tag must be {AFFILIATE_TAG_MIN_LEN}-{AFFILIATE_TAG_MAX_LEN} characters, got {}",
            tag.len()
        ));
    }
    if let Some(bad) = tag
        .chars()
        .find(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_' || *c == '-'))
    {
        return Err(format!("affiliate tag may only contain a-z, 0-9, '_' or '-'; found {bad:?}"));
    }
    Ok(())
}

/// Derive the Affiliate PDA from its authority wallet (one per wallet).
pub fn get_affiliate_address(authority: Pubkey) -> (Pubkey, u8) {
    let seeds = &[b"affiliate".as_ref(), authority.as_ref()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

/// Derive the AffiliateTag registry PDA, which resolves a referral link's tag
/// to the claiming authority. Callers should `validate_affiliate_tag` first:
/// a tag over the 32-byte seed limit panics inside `find_program_address`.
pub fn get_affiliate_tag_address(tag: &str) -> (Pubkey, u8) {
    let seeds = &[b"affiliate_tag".as_ref(), tag.as_bytes()];
    Pubkey::find_program_address(seeds, &SATRUSH_ID)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validate_affiliate_tag_accepts_the_charset_at_both_bounds() {
        validate_affiliate_tag("a_0").unwrap();
        validate_affiliate_tag("abcdefgh12345_-z").unwrap();
    }

    #[test]
    fn validate_affiliate_tag_rejects_bad_lengths_and_charsets() {
        assert!(validate_affiliate_tag("ab").is_err(), "too short");
        assert!(validate_affiliate_tag("abcdefgh12345_-zx").is_err(), "17 bytes");
        assert!(validate_affiliate_tag("ABC").is_err(), "uppercase");
        assert!(validate_affiliate_tag("a.b").is_err(), "dot");
        assert!(validate_affiliate_tag("").is_err(), "empty");
    }

    #[test]
    fn validate_affiliate_tag_rejects_tags_that_would_panic_the_derivation() {
        // Over the 32-byte seed limit `find_program_address` panics, so the
        // guard has to reject it before the derivation is ever attempted.
        assert!(validate_affiliate_tag(&"a".repeat(40)).is_err());
    }
}