wp-solana-core 0.1.2

Cross-protocol shared core for waterpump-solana: token planning compatibility, error types, system constants.
Documentation
//! Mint-account decoding helpers shared by every protocol's fetch layer.
//!
//! These are pure byte-decoders over an already-fetched account (no I/O); the
//! RPC round-trips that produce the [`solana_account::Account`] live in the
//! SDK `fetch/` layer.

use solana_account::Account;
use solana_pubkey::Pubkey;
use wp_solana_pool_types::metadata::{MintInfo, TokenProgram};

use crate::plan_error::FetchError;

/// Decode a fetched mint account into [`MintInfo`].
///
/// `account` is `None` when the RPC `get_multiple_accounts` slot for `address`
/// came back empty, which is surfaced as [`FetchError::AccountNotFound`].
pub fn decode_mint_info(address: Pubkey, account: Option<Account>) -> Result<MintInfo, FetchError> {
    let account = account.ok_or(FetchError::AccountNotFound(address))?;
    let program = TokenProgram::from_program_id(&account.owner).ok_or_else(|| {
        FetchError::InvalidInput(format!(
            "mint {} owner is neither SPL Token nor Token-2022",
            address
        ))
    })?;
    let decimals = read_mint_decimals(&account.data, address)?;
    Ok(MintInfo { address, decimals, program })
}

/// Read the `decimals` byte from a raw mint account's `data`.
pub fn read_mint_decimals(data: &[u8], address: Pubkey) -> Result<u8, FetchError> {
    // The `decimals` byte sits at the same offset for both account formats:
    //   SPL Token   (`spl_token::state::Mint`, 82 bytes total) — offset 44
    //   Token-2022  (base `Mint` struct prefix is identical to SPL Token,
    //                followed by optional extension TLVs)              — offset 44
    // We avoid an `unpack_unchecked` call to dodge the SPL Token / Token-2022
    // crate version mismatch that the workspace would otherwise need to
    // resolve. If the upstream Mint layout ever changes, switch to
    // `spl_token_interface::state::Mint::unpack` (gated on owner program).
    const DECIMALS_OFFSET: usize = 44;
    if data.len() <= DECIMALS_OFFSET {
        return Err(FetchError::AccountDecode(format!(
            "mint account {} too short ({} bytes)",
            address,
            data.len()
        )));
    }
    Ok(data[DECIMALS_OFFSET])
}

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

    // A minimal SPL Mint layout: bytes [0..44) are mint_authority/supply etc.,
    // byte 44 is `decimals`. We only assert on offset 44, so the prefix bytes
    // are arbitrary.
    fn mint_data(decimals: u8) -> Vec<u8> {
        let mut v = vec![0u8; 82];
        v[44] = decimals;
        v
    }

    fn account_owned_by(owner: Pubkey, data: Vec<u8>) -> Account {
        Account { lamports: 1, data, owner, executable: false, rent_epoch: 0 }
    }

    #[test]
    fn reads_decimals_at_offset_44() {
        let addr = Pubkey::new_unique();
        assert_eq!(read_mint_decimals(&mint_data(9), addr).unwrap(), 9);
        assert_eq!(read_mint_decimals(&mint_data(0), addr).unwrap(), 0);
        assert_eq!(read_mint_decimals(&mint_data(6), addr).unwrap(), 6);
    }

    #[test]
    fn rejects_short_account_data() {
        let addr = Pubkey::new_unique();
        let err = read_mint_decimals(&[0u8; 44], addr).unwrap_err();
        assert!(matches!(err, FetchError::AccountDecode(_)), "got {err:?}");
    }

    #[test]
    fn decode_mint_info_spl_token() {
        let addr = Pubkey::new_unique();
        let acc = account_owned_by(spl_token_interface::ID, mint_data(9));
        let info = decode_mint_info(addr, Some(acc)).unwrap();
        assert_eq!(info, MintInfo { address: addr, decimals: 9, program: TokenProgram::SplToken });
    }

    #[test]
    fn decode_mint_info_token_2022() {
        let addr = Pubkey::new_unique();
        let acc = account_owned_by(spl_token_2022_interface::ID, mint_data(6));
        let info = decode_mint_info(addr, Some(acc)).unwrap();
        assert_eq!(
            info,
            MintInfo { address: addr, decimals: 6, program: TokenProgram::SplToken2022 }
        );
    }

    #[test]
    fn decode_mint_info_missing_account_is_not_found() {
        let addr = Pubkey::new_unique();
        let err = decode_mint_info(addr, None).unwrap_err();
        assert!(matches!(err, FetchError::AccountNotFound(a) if a == addr), "got {err:?}");
    }

    #[test]
    fn decode_mint_info_unknown_owner_is_invalid_input() {
        let addr = Pubkey::new_unique();
        let acc = account_owned_by(Pubkey::new_unique(), mint_data(9));
        let err = decode_mint_info(addr, Some(acc)).unwrap_err();
        assert!(matches!(err, FetchError::InvalidInput(_)), "got {err:?}");
    }
}