wp-solana-test-core 0.1.1

Protocol-agnostic Solana test infrastructure built on LiteSVM
Documentation
//! Fixture loader for offline test account data
//!
//! Loads Solana account data from JSON fixture files, allowing tests to run
//! without a live RPC connection. JSON format matches the output of the
//! `dump_pool_account` example tool.

use std::str::FromStr;

use anyhow::{Context, Result};
use base64::Engine;
use solana_sdk::{account::Account, pubkey::Pubkey};

/// Represents a single serialized Solana account loaded from JSON.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AccountFixture {
    /// Base-58 encoded public key of the account
    pub address: String,
    /// Account data encoded as base64 **or** hex (auto-detected)
    pub data: String,
    /// Encoding hint: "base64" or "hex". Defaults to "base64" when absent.
    #[serde(default = "default_encoding")]
    pub encoding: String,
    /// Lamports balance
    pub lamports: u64,
    /// Owner program (base-58 pubkey)
    pub owner: String,
    /// Whether the account is executable
    #[serde(default)]
    pub executable: bool,
    /// Rent epoch
    #[serde(default)]
    pub rent_epoch: u64,
}

fn default_encoding() -> String {
    "base64".to_string()
}

/// Load a single account fixture from JSON bytes.
///
/// Returns the account address and a ready-to-use `Account` struct.
///
/// # Supported encodings
/// - `"base64"` (default) -- data field is standard base64
/// - `"hex"` -- data field is hex-encoded bytes
///
/// # Example JSON
/// ```json
/// {
///   "address": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv",
///   "data": "<base64 or hex>",
///   "encoding": "base64",
///   "lamports": 6124800,
///   "owner": "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
///   "executable": false,
///   "rent_epoch": 0
/// }
/// ```
pub fn load_account_fixture(json_bytes: &[u8]) -> Result<(Pubkey, Account)> {
    let fixture: AccountFixture =
        serde_json::from_slice(json_bytes).context("Failed to parse account fixture JSON")?;

    let address = Pubkey::from_str(&fixture.address)
        .map_err(|e| anyhow::anyhow!("Invalid account address '{}': {}", fixture.address, e))?;

    let owner = Pubkey::from_str(&fixture.owner)
        .map_err(|e| anyhow::anyhow!("Invalid owner address '{}': {}", fixture.owner, e))?;

    let data = decode_data(&fixture.data, &fixture.encoding)?;

    Ok((
        address,
        Account {
            lamports: fixture.lamports,
            data,
            owner,
            executable: fixture.executable,
            rent_epoch: fixture.rent_epoch,
        },
    ))
}

/// Load multiple account fixtures from a JSON array.
///
/// The JSON must be an array of account objects (same schema as
/// [`load_account_fixture`]).
pub fn load_account_fixtures(json_bytes: &[u8]) -> Result<Vec<(Pubkey, Account)>> {
    let fixtures: Vec<AccountFixture> = serde_json::from_slice(json_bytes)
        .context("Failed to parse account fixtures JSON array")?;

    fixtures
        .into_iter()
        .map(|fixture| {
            let address = Pubkey::from_str(&fixture.address).map_err(|e| {
                anyhow::anyhow!("Invalid account address '{}': {}", fixture.address, e)
            })?;

            let owner = Pubkey::from_str(&fixture.owner)
                .map_err(|e| anyhow::anyhow!("Invalid owner address '{}': {}", fixture.owner, e))?;

            let data = decode_data(&fixture.data, &fixture.encoding)?;

            Ok((
                address,
                Account {
                    lamports: fixture.lamports,
                    data,
                    owner,
                    executable: fixture.executable,
                    rent_epoch: fixture.rent_epoch,
                },
            ))
        })
        .collect()
}

/// Decode account data from a string using the specified encoding.
fn decode_data(data_str: &str, encoding: &str) -> Result<Vec<u8>> {
    match encoding {
        "base64" => base64::engine::general_purpose::STANDARD
            .decode(data_str)
            .context("Failed to decode base64 account data"),
        "hex" => hex::decode(data_str).context("Failed to decode hex account data"),
        other => Err(anyhow::anyhow!("Unsupported encoding: '{}'. Use 'base64' or 'hex'.", other)),
    }
}

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

    #[test]
    fn test_load_account_fixture_base64() {
        let json = serde_json::json!({
            "address": "11111111111111111111111111111111",
            "data": base64::engine::general_purpose::STANDARD.encode([1, 2, 3, 4]),
            "encoding": "base64",
            "lamports": 1000,
            "owner": "11111111111111111111111111111111",
            "executable": false,
            "rent_epoch": 0
        });
        let bytes = serde_json::to_vec(&json).unwrap();
        let (addr, account) = load_account_fixture(&bytes).unwrap();
        assert_eq!(addr, Pubkey::from_str("11111111111111111111111111111111").unwrap());
        assert_eq!(account.data, vec![1, 2, 3, 4]);
        assert_eq!(account.lamports, 1000);
    }

    #[test]
    fn test_load_account_fixture_hex() {
        let json = serde_json::json!({
            "address": "11111111111111111111111111111111",
            "data": "01020304",
            "encoding": "hex",
            "lamports": 2000,
            "owner": "11111111111111111111111111111111",
            "executable": false,
            "rent_epoch": 0
        });
        let bytes = serde_json::to_vec(&json).unwrap();
        let (_, account) = load_account_fixture(&bytes).unwrap();
        assert_eq!(account.data, vec![1, 2, 3, 4]);
        assert_eq!(account.lamports, 2000);
    }

    #[test]
    fn test_load_account_fixtures_array() {
        let json = serde_json::json!([
            {
                "address": "11111111111111111111111111111111",
                "data": base64::engine::general_purpose::STANDARD.encode([1, 2]),
                "encoding": "base64",
                "lamports": 100,
                "owner": "11111111111111111111111111111111",
                "executable": false,
                "rent_epoch": 0
            },
            {
                "address": "11111111111111111111111111111111",
                "data": "0506",
                "encoding": "hex",
                "lamports": 200,
                "owner": "11111111111111111111111111111111",
                "executable": false,
                "rent_epoch": 0
            }
        ]);
        let bytes = serde_json::to_vec(&json).unwrap();
        let accounts = load_account_fixtures(&bytes).unwrap();
        assert_eq!(accounts.len(), 2);
        assert_eq!(accounts[0].1.data, vec![1, 2]);
        assert_eq!(accounts[1].1.data, vec![5, 6]);
    }
}