wp-solana-test-utils 0.1.1

Protocol-specific test fixtures for Raydium CLMM, Meteora DLMM, Orca Whirlpool
Documentation
//! Internal helpers shared by protocol fixture modules.

use std::{
    env,
    path::{Path, PathBuf},
    str::FromStr,
};

use anyhow::{Context, Result};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{account::Account, pubkey::Pubkey, signature::Signer};
use wp_solana_test_core::{
    fund_token, fund_wsol, override_mint_authority, set_account, TestContext,
};

/// Fetch program binary from RPC client.
///
/// For upgradeable programs this fetches the program-data account and extracts
/// the actual binary (skipping the 45-byte metadata header).
pub async fn fetch_program(client: &RpcClient, program_id: &Pubkey) -> Result<Vec<u8>> {
    let account = client.get_account(program_id).await?;

    const BPF_LOADER_UPGRADEABLE: &str = "BPFLoaderUpgradeab1e11111111111111111111111";
    let bpf_loader_upgradeable = Pubkey::from_str(BPF_LOADER_UPGRADEABLE)?;

    if account.owner == bpf_loader_upgradeable {
        let (program_data_address, _bump) =
            Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable);

        let program_data_account = client.get_account(&program_data_address).await?;

        const PROGRAM_DATA_HEADER_SIZE: usize = 45;
        if program_data_account.data.len() < PROGRAM_DATA_HEADER_SIZE {
            return Err(anyhow::anyhow!(
                "Program data account too small: {} bytes (expected at least {})",
                program_data_account.data.len(),
                PROGRAM_DATA_HEADER_SIZE
            ));
        }

        let program_bytes = program_data_account.data[PROGRAM_DATA_HEADER_SIZE..].to_vec();
        Ok(program_bytes)
    } else {
        Ok(account.data.to_vec())
    }
}

fn resolve_fallback_path_from(base_dir: &Path, fallback_path: &str) -> Option<PathBuf> {
    let candidate = Path::new(fallback_path);
    if candidate.is_absolute() && candidate.exists() {
        return Some(candidate.to_path_buf());
    }

    let mut dir = base_dir.to_path_buf();
    loop {
        let candidate = dir.join(fallback_path);
        if candidate.exists() {
            return Some(candidate);
        }
        if !dir.pop() {
            break;
        }
    }

    None
}

fn resolve_fallback_path(fallback_path: &str) -> Option<PathBuf> {
    let base_dir = env::current_dir().ok()?;
    resolve_fallback_path_from(&base_dir, fallback_path)
}

/// Fetch a program binary from RPC (with local `.so` fallback) and load it
/// into the [`TestContext`] SVM.
///
/// This consolidates the repeated fetch-program → fallback-to-file →
/// `svm.add_program()` pattern used across all protocol fixture files.
pub async fn fetch_and_add_program(
    ctx: &TestContext,
    client: &RpcClient,
    program_id: Pubkey,
    fallback_path: &str,
) -> Result<()> {
    let program_bytes = match fetch_program(client, &program_id).await {
        Ok(bytes) => bytes,
        Err(e) => {
            tracing::warn!(
                error = %e,
                program_id = %program_id,
                "Failed to fetch program from RPC; trying local fallback"
            );

            if let Some(resolved_path) = resolve_fallback_path(fallback_path) {
                tracing::info!(path = %resolved_path.display(), "Loading from file instead");
                std::fs::read(&resolved_path).context("Failed to read program binary from file")?
            } else {
                return Err(anyhow::anyhow!("Failed to fetch program: {}", e));
            }
        }
    };

    {
        let mut svm = ctx.lock_svm();
        svm.add_program(program_id, &program_bytes)?;
    }
    tracing::info!(program_id = %program_id, "Program added successfully");

    Ok(())
}

/// Set up a token mint from an RPC-fetched account, override its mint
/// authority to the test payer, and fund the payer with tokens.
///
/// For native SOL (wSOL) the authority override is skipped and [`fund_wsol`]
/// is used instead of [`fund_token`].
///
/// Returns the payer's associated token account (ATA) address.
pub fn setup_mint_and_fund(
    ctx: &TestContext,
    mint: Pubkey,
    mint_account: Account,
    is_native: bool,
    amount: u64,
) -> Result<Pubkey> {
    set_account(ctx, mint, mint_account)?;

    if is_native {
        let ata = fund_wsol(ctx, ctx.payer.pubkey(), amount)?;
        tracing::info!(
            mint = %mint,
            sol_amount = amount / 1_000_000_000,
            "Wrapped SOL into WSOL"
        );
        Ok(ata)
    } else {
        override_mint_authority(ctx, mint, ctx.payer.pubkey())?;
        let ata = fund_token(ctx, mint, ctx.payer.pubkey(), amount)?;
        tracing::info!(
            mint = %mint,
            amount,
            "Minted tokens to payer (authority overridden)"
        );
        Ok(ata)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::resolve_fallback_path_from;

    #[test]
    fn resolve_fallback_path_finds_hit_in_base_dir() {
        let dir = tempdir().expect("tempdir");
        let file = dir.path().join("program.so");
        fs::write(&file, b"so").expect("write fallback");

        let resolved = resolve_fallback_path_from(dir.path(), "program.so").expect("resolved path");
        assert_eq!(resolved, file);
    }

    #[test]
    fn resolve_fallback_path_finds_workspace_ancestor_hit() {
        let dir = tempdir().expect("tempdir");
        let workspace_root = dir.path();
        let crate_dir = workspace_root.join("crates/protocols/orca/whirlpool-sdk");
        fs::create_dir_all(&crate_dir).expect("create nested crate dir");

        let fallback = workspace_root.join("target/deploy/whirlpool.so");
        fs::create_dir_all(fallback.parent().expect("fallback parent"))
            .expect("create target/deploy");
        fs::write(&fallback, b"so").expect("write fallback");

        let resolved = resolve_fallback_path_from(&crate_dir, "target/deploy/whirlpool.so")
            .expect("resolved path");
        assert_eq!(resolved, fallback);
    }

    #[test]
    fn resolve_fallback_path_returns_none_when_missing() {
        let dir = tempdir().expect("tempdir");
        assert!(resolve_fallback_path_from(dir.path(), "target/deploy/missing.so").is_none());
    }
}