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
//! Metaplex Token Metadata, decoded far enough to answer one question:
//! **can the team still change what this token claims to be?**
//!
//! An immutable metadata account is a real signal. A mutable one means the
//! symbol reading `USDC` today can read something else tomorrow, under the same
//! mint address your agent allowlisted.
//!
//! `name`, `symbol` and `uri` are attacker-controlled. They are carried raw in
//! [`TokenMetadata`]; run them through [`crate::sanitize`] before they reach an
//! output.

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

/// The account key discriminator for `MetadataV1`.
const KEY_METADATA_V1: u8 = 4;

/// Metaplex metadata for a mint.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenMetadata {
    /// `None` when the metadata has been made immutable by clearing it.
    pub update_authority: Option<Pubkey>,
    /// The mint this metadata describes.
    pub mint: Pubkey,
    /// Attacker-controlled. Sanitize before display.
    pub name: String,
    /// Attacker-controlled. Sanitize before display.
    pub symbol: String,
    /// Attacker-controlled. Sanitize before display.
    pub uri: String,
    /// Royalty in basis points, for NFTs.
    pub seller_fee_basis_points: u16,
    /// False means name, symbol and URI are frozen forever.
    pub is_mutable: bool,
    /// Creator addresses, if the list is present.
    pub creators: Vec<Pubkey>,
}

/// Derive the metadata PDA for a mint: `["metadata", program_id, mint]`.
pub fn metadata_address(mint: &Pubkey) -> Result<Pubkey> {
    let program = ids::METAPLEX_METADATA_PROGRAM;
    let (address, _bump) = Pubkey::find_program_address(
        &[b"metadata", program.as_bytes(), mint.as_bytes()],
        &program,
    )?;
    Ok(address)
}

impl TokenMetadata {
    /// Parse a fetched metadata account.
    ///
    /// Metaplex has shipped several account versions and pads its strings to a
    /// fixed width with NUL bytes. The parser reads only the prefix every
    /// version shares and stops, so a newer layout degrades to "we know the
    /// authority and the name", never to a wrong answer.
    pub fn parse(account: &Account) -> Result<Self> {
        if account.owner != ids::METAPLEX_METADATA_PROGRAM {
            return Err(Error::InvalidAccountData(
                "account is not owned by the Metaplex metadata program".into(),
            ));
        }
        let d = &account.data;
        if d.is_empty() || d[0] != KEY_METADATA_V1 {
            return Err(Error::InvalidAccountData(format!(
                "metadata account key is {}, expected {KEY_METADATA_V1}",
                d.first().copied().unwrap_or(0)
            )));
        }

        let mut c = 1usize;
        let update_authority = read_pubkey(d, &mut c)?;
        let mint = read_pubkey(d, &mut c)?;
        let name = read_padded_string(d, &mut c)?;
        let symbol = read_padded_string(d, &mut c)?;
        let uri = read_padded_string(d, &mut c)?;
        let seller_fee_basis_points = read_u16(d, &mut c)?;

        // Option<Vec<Creator>>
        let mut creators = Vec::new();
        let has_creators = read_u8(d, &mut c)? == 1;
        if has_creators {
            let count = read_u32(d, &mut c)?;
            for _ in 0..count.min(16) {
                let address = read_pubkey(d, &mut c)?;
                let _verified = read_u8(d, &mut c)?;
                let _share = read_u8(d, &mut c)?;
                creators.push(address);
            }
        }

        let _primary_sale_happened = read_u8(d, &mut c)?;
        let is_mutable = read_u8(d, &mut c)? == 1;

        Ok(TokenMetadata {
            // Metaplex writes the zero key rather than removing the field.
            update_authority: if update_authority.is_zeroed() {
                None
            } else {
                Some(update_authority)
            },
            mint,
            name,
            symbol,
            uri,
            seller_fee_basis_points,
            is_mutable,
            creators,
        })
    }
}

fn read_u8(d: &[u8], c: &mut usize) -> Result<u8> {
    let v = *d.get(*c).ok_or_else(short)?;
    *c += 1;
    Ok(v)
}

fn read_u16(d: &[u8], c: &mut usize) -> Result<u16> {
    let end = c.checked_add(2).ok_or_else(short)?;
    let slice = d.get(*c..end).ok_or_else(short)?;
    *c = end;
    Ok(u16::from_le_bytes([slice[0], slice[1]]))
}

fn read_u32(d: &[u8], c: &mut usize) -> Result<u32> {
    let end = c.checked_add(4).ok_or_else(short)?;
    let slice = d.get(*c..end).ok_or_else(short)?;
    *c = end;
    Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
}

fn read_pubkey(d: &[u8], c: &mut usize) -> Result<Pubkey> {
    let end = c.checked_add(32).ok_or_else(short)?;
    let slice = d.get(*c..end).ok_or_else(short)?;
    *c = end;
    let mut b = [0u8; 32];
    b.copy_from_slice(slice);
    Ok(Pubkey::new_from_array(b))
}

/// A borsh string, minus the NUL padding Metaplex writes to a fixed width.
fn read_padded_string(d: &[u8], c: &mut usize) -> Result<String> {
    let len = read_u32(d, c)? as usize;
    // The length prefix is attacker-adjacent; bound it by the account itself.
    if len > d.len() {
        return Err(Error::InvalidAccountData(
            "metadata string length exceeds the account".into(),
        ));
    }
    let end = c.checked_add(len).ok_or_else(short)?;
    let slice = d.get(*c..end).ok_or_else(short)?;
    *c = end;
    Ok(String::from_utf8_lossy(slice)
        .trim_end_matches('\0')
        .to_string())
}

fn short() -> Error {
    Error::InvalidAccountData("metadata account is truncated".into())
}