solana-wasi 0.1.0

Solana primitives that actually compile to wasm32-wasip2: pubkeys, PDAs, JSON-RPC over a swappable transport, SPL Token / Token-2022 account parsing, and unsigned v0 transaction construction. No solana-sdk, no C toolchain, no async runtime.
Documentation
//! SPL Token and Token-2022 account layouts.
//!
//! Both programs share the classic 82-byte mint and 165-byte token account;
//! Token-2022 appends a TLV extension area, which [`extensions`] decodes.

pub mod extensions;

pub use extensions::{AccountState, MintExtension, TransferFee};

use crate::error::{Error, Result};
use crate::pubkey::{ids, Pubkey};
use crate::rpc::Account;

/// Serialized length of the base mint.
pub const MINT_LEN: usize = 82;

/// Serialized length of a token account.
pub const TOKEN_ACCOUNT_LEN: usize = 165;

/// Which of the two token programs owns an account.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenProgram {
    /// `Tokenkeg…` — the original SPL Token program.
    Legacy,
    /// `TokenzQd…` — Token-2022, with extensions.
    Token2022,
}

impl TokenProgram {
    /// The program id.
    pub const fn id(self) -> Pubkey {
        match self {
            TokenProgram::Legacy => ids::TOKEN_PROGRAM,
            TokenProgram::Token2022 => ids::TOKEN_2022_PROGRAM,
        }
    }

    /// Identify a program from an account owner, or `None` if it is neither.
    pub fn from_owner(owner: &Pubkey) -> Option<Self> {
        if *owner == ids::TOKEN_PROGRAM {
            Some(TokenProgram::Legacy)
        } else if *owner == ids::TOKEN_2022_PROGRAM {
            Some(TokenProgram::Token2022)
        } else {
            None
        }
    }

    /// Short name for a summary line.
    pub const fn name(self) -> &'static str {
        match self {
            TokenProgram::Legacy => "spl-token",
            TokenProgram::Token2022 => "token-2022",
        }
    }
}

/// The 82-byte base mint, common to both programs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mint {
    /// `Some` means the supply can still be increased.
    pub mint_authority: Option<Pubkey>,
    /// Total supply in base units.
    pub supply: u64,
    /// Decimal places.
    pub decimals: u8,
    /// False for an account that has been allocated but not initialized.
    pub is_initialized: bool,
    /// `Some` means any holder's account can be frozen.
    pub freeze_authority: Option<Pubkey>,
}

impl Mint {
    /// Parse the base mint out of an account buffer.
    pub fn unpack(data: &[u8]) -> Result<Self> {
        if data.len() < MINT_LEN {
            return Err(Error::InvalidAccountData(format!(
                "mint is {} bytes, expected at least {MINT_LEN}",
                data.len()
            )));
        }
        Ok(Mint {
            mint_authority: coption_pubkey(&data[0..36])?,
            supply: u64::from_le_bytes(data[36..44].try_into().unwrap()),
            decimals: data[44],
            is_initialized: data[45] != 0,
            freeze_authority: coption_pubkey(&data[46..82])?,
        })
    }
}

/// A mint plus everything Token-2022 bolted onto it.
#[derive(Debug, Clone, PartialEq)]
pub struct MintState {
    /// The mint's own address.
    pub address: Pubkey,
    /// Which token program owns it.
    pub program: TokenProgram,
    /// The 82-byte base mint.
    pub mint: Mint,
    /// Token-2022 extensions; always empty for the legacy program.
    pub extensions: Vec<MintExtension>,
}

impl MintState {
    /// Parse a fetched account as a mint.
    ///
    /// Errors when the account is not owned by a token program at all, which is
    /// the check that catches "this address is a wallet / an AMM pool / a
    /// hallucination", not a mint.
    pub fn parse(address: Pubkey, account: &Account) -> Result<Self> {
        let program = TokenProgram::from_owner(&account.owner).ok_or_else(|| {
            Error::InvalidAccountData(format!(
                "{} is owned by {}, which is not a token program",
                address.abbreviated(),
                account.owner.abbreviated()
            ))
        })?;
        let mint = Mint::unpack(&account.data)?;
        let extensions = match program {
            TokenProgram::Legacy => Vec::new(),
            TokenProgram::Token2022 => extensions::parse_mint_extensions(&account.data)?,
        };
        Ok(MintState {
            address,
            program,
            mint,
            extensions,
        })
    }

    /// Find a decoded extension by discriminator.
    pub fn extension(&self, kind: u16) -> Option<&MintExtension> {
        self.extensions.iter().find(|e| e.kind() == kind)
    }

    /// The permanent delegate, if the mint has one.
    pub fn permanent_delegate(&self) -> Option<Pubkey> {
        match self.extension(12) {
            Some(MintExtension::PermanentDelegate { delegate }) => *delegate,
            _ => None,
        }
    }

    /// The transfer hook program, if the mint has one.
    pub fn transfer_hook_program(&self) -> Option<Pubkey> {
        match self.extension(14) {
            Some(MintExtension::TransferHook { program_id, .. }) => *program_id,
            _ => None,
        }
    }

    /// True when the mint cannot be transferred at all.
    pub fn is_non_transferable(&self) -> bool {
        self.extension(9).is_some()
    }

    /// True when newly created accounts start frozen.
    pub fn defaults_to_frozen(&self) -> bool {
        matches!(
            self.extension(6),
            Some(MintExtension::DefaultAccountState {
                state: AccountState::Frozen
            })
        )
    }

    /// True when transfers are currently paused.
    pub fn is_paused(&self) -> bool {
        matches!(
            self.extension(26),
            Some(MintExtension::PausableConfig { paused: true, .. })
        )
    }

    /// The transfer fee in effect, in basis points, if any.
    pub fn transfer_fee_bps(&self) -> Option<u16> {
        match self.extension(1) {
            Some(MintExtension::TransferFeeConfig { newer, .. }) => Some(newer.basis_points),
            _ => None,
        }
    }

    /// Render `supply` with the mint's decimals applied.
    pub fn ui_supply(&self) -> String {
        crate::shape::ui_amount(self.mint.supply as u128, self.mint.decimals)
    }
}

/// A token account (an ATA or otherwise).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenAccount {
    /// The mint this account holds.
    pub mint: Pubkey,
    /// The wallet that controls it.
    pub owner: Pubkey,
    /// Balance in base units.
    pub amount: u64,
    /// A key allowed to move up to `delegated_amount`.
    pub delegate: Option<Pubkey>,
    /// Initialized, frozen, or neither.
    pub state: AccountState,
    /// True for a wrapped-SOL account.
    pub is_native: bool,
    /// How much the delegate may still move.
    pub delegated_amount: u64,
    /// A key allowed to close the account and reclaim its rent.
    pub close_authority: Option<Pubkey>,
}

impl TokenAccount {
    /// Parse the base token account out of an account buffer.
    pub fn unpack(data: &[u8]) -> Result<Self> {
        if data.len() < TOKEN_ACCOUNT_LEN {
            return Err(Error::InvalidAccountData(format!(
                "token account is {} bytes, expected at least {TOKEN_ACCOUNT_LEN}",
                data.len()
            )));
        }
        Ok(TokenAccount {
            mint: pubkey_at(&data[0..32]),
            owner: pubkey_at(&data[32..64]),
            amount: u64::from_le_bytes(data[64..72].try_into().unwrap()),
            delegate: coption_pubkey(&data[72..108])?,
            state: AccountState::from(data[108]),
            is_native: u32::from_le_bytes(data[109..113].try_into().unwrap()) == 1,
            delegated_amount: u64::from_le_bytes(data[121..129].try_into().unwrap()),
            close_authority: coption_pubkey(&data[129..165])?,
        })
    }

    /// True when the account cannot send or receive until it is thawed.
    pub fn is_frozen(&self) -> bool {
        self.state == AccountState::Frozen
    }
}

/// Derive the associated token account for `(owner, mint)` under `program`.
///
/// The token program id is part of the seed set, so the ATA for a Token-2022
/// mint is a *different* address than the one you would get by assuming the
/// legacy program. Getting this wrong is how a payment lands in an account
/// nobody can spend from.
pub fn associated_token_address(
    owner: &Pubkey,
    mint: &Pubkey,
    program: TokenProgram,
) -> Result<Pubkey> {
    let program_id = program.id();
    let (address, _bump) = Pubkey::find_program_address(
        &[owner.as_bytes(), program_id.as_bytes(), mint.as_bytes()],
        &ids::ASSOCIATED_TOKEN_PROGRAM,
    )?;
    Ok(address)
}

fn pubkey_at(b: &[u8]) -> Pubkey {
    let mut a = [0u8; 32];
    a.copy_from_slice(&b[..32]);
    Pubkey::new_from_array(a)
}

/// SPL's `COption<Pubkey>`: a 4-byte little-endian discriminator then 32 bytes.
fn coption_pubkey(b: &[u8]) -> Result<Option<Pubkey>> {
    if b.len() < 36 {
        return Err(Error::InvalidAccountData("truncated COption".into()));
    }
    match u32::from_le_bytes(b[0..4].try_into().unwrap()) {
        0 => Ok(None),
        1 => Ok(Some(pubkey_at(&b[4..36]))),
        other => Err(Error::InvalidAccountData(format!(
            "COption discriminant {other} is neither 0 nor 1"
        ))),
    }
}