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
//! 32-byte public keys, base58, and program-derived addresses.
//!
//! `solana-sdk`'s `Pubkey` drags in a dependency tree that does not build for
//! `wasm32-wasip2` inside a WIT component. This is the same type with the same
//! wire behaviour, in about two hundred lines and four pure-Rust dependencies.

use core::fmt;
use core::str::FromStr;

use curve25519_dalek::edwards::CompressedEdwardsY;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};

use crate::error::{Error, Result};

/// Appended to the seed set so a program address can never collide with a
/// legitimate ed25519 public key. Same constant the runtime uses.
const PDA_MARKER: &[u8] = b"ProgramDerivedAddress";

/// Maximum length of one PDA seed, enforced by the runtime.
pub const MAX_SEED_LEN: usize = 32;

/// Maximum number of PDA seeds, enforced by the runtime.
pub const MAX_SEEDS: usize = 16;

/// A Solana public key: 32 raw bytes, displayed as base58.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Pubkey(pub(crate) [u8; 32]);

impl Pubkey {
    /// Wrap 32 raw bytes.
    pub const fn new_from_array(bytes: [u8; 32]) -> Self {
        Pubkey(bytes)
    }

    /// Parse a base58 address.
    ///
    /// Rejects anything that does not decode to exactly 32 bytes, which also
    /// rejects the truncated and look-alike addresses that show up when an LLM
    /// hallucinates or echoes an address out of a chat message.
    pub fn from_base58(s: &str) -> Result<Self> {
        // Cheap length gate before the decoder: 32 bytes is 43-44 base58 chars,
        // fewer only when the key has leading zero bytes (system program, sysvars).
        if s.is_empty() || s.len() > 44 {
            return Err(Error::InvalidPubkey(truncate_for_error(s)));
        }
        let mut out = [0u8; 32];
        match bs58::decode(s).onto(&mut out) {
            Ok(32) => Ok(Pubkey(out)),
            _ => Err(Error::InvalidPubkey(truncate_for_error(s))),
        }
    }

    /// The raw bytes.
    pub const fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }

    /// The raw bytes, by value.
    pub const fn to_bytes(self) -> [u8; 32] {
        self.0
    }

    /// Base58 encoding.
    pub fn to_base58(&self) -> String {
        bs58::encode(self.0).into_string()
    }

    /// `EPjFWd…Dt1v` — for summaries a human reads in a chat window.
    pub fn abbreviated(&self) -> String {
        let full = self.to_base58();
        if full.len() <= 12 {
            return full;
        }
        format!("{}{}", &full[..6], &full[full.len() - 4..])
    }

    /// The all-zero key, which is also the System Program id.
    pub const fn zeroed() -> Self {
        Pubkey([0u8; 32])
    }

    /// True when every byte is zero.
    pub fn is_zeroed(&self) -> bool {
        self.0 == [0u8; 32]
    }

    /// True when the key is a point on the ed25519 curve, i.e. someone could
    /// hold a private key for it.
    ///
    /// The inverse is the load-bearing check: a recipient address that is *not*
    /// on the curve is a program-derived address, and a plain token transfer to
    /// one is unrecoverable. Payment builders should refuse it.
    pub fn is_on_curve(&self) -> bool {
        CompressedEdwardsY(self.0).decompress().is_some()
    }

    /// Derive an address from seeds without the bump search.
    ///
    /// Fails when the derived address lands on the curve, exactly like the
    /// runtime, so a caller cannot accidentally mint an address that a key
    /// could sign for.
    pub fn create_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Result<Pubkey> {
        if seeds.len() > MAX_SEEDS {
            return Err(Error::InvalidSeeds("too many seeds"));
        }
        if seeds.iter().any(|s| s.len() > MAX_SEED_LEN) {
            return Err(Error::InvalidSeeds("seed longer than 32 bytes"));
        }
        let mut hasher = Sha256::new();
        for seed in seeds {
            hasher.update(seed);
        }
        hasher.update(program_id.0);
        hasher.update(PDA_MARKER);
        let hash: [u8; 32] = hasher.finalize().into();
        let candidate = Pubkey(hash);
        if candidate.is_on_curve() {
            return Err(Error::InvalidSeeds("derived address is on the curve"));
        }
        Ok(candidate)
    }

    /// Find the canonical (highest bump) program address for these seeds.
    pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Result<(Pubkey, u8)> {
        for bump in (0u8..=255).rev() {
            let bump_seed = [bump];
            let mut all: Vec<&[u8]> = Vec::with_capacity(seeds.len() + 1);
            all.extend_from_slice(seeds);
            all.push(&bump_seed);
            match Pubkey::create_program_address(&all, program_id) {
                Ok(pk) => return Ok((pk, bump)),
                Err(Error::InvalidSeeds("derived address is on the curve")) => continue,
                Err(e) => return Err(e),
            }
        }
        Err(Error::InvalidSeeds("no viable bump seed"))
    }
}

/// Keep a hostile address out of log lines and error strings at full length.
fn truncate_for_error(s: &str) -> String {
    let cleaned: String = s.chars().filter(|c| !c.is_control()).take(48).collect();
    cleaned
}

impl fmt::Display for Pubkey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.to_base58())
    }
}

impl fmt::Debug for Pubkey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.to_base58())
    }
}

impl FromStr for Pubkey {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self> {
        Pubkey::from_base58(s)
    }
}

impl From<[u8; 32]> for Pubkey {
    fn from(b: [u8; 32]) -> Self {
        Pubkey(b)
    }
}

impl Serialize for Pubkey {
    fn serialize<S: Serializer>(&self, s: S) -> core::result::Result<S::Ok, S::Error> {
        s.serialize_str(&self.to_base58())
    }
}

impl<'de> Deserialize<'de> for Pubkey {
    fn deserialize<D: Deserializer<'de>>(d: D) -> core::result::Result<Self, D::Error> {
        let s = String::deserialize(d)?;
        Pubkey::from_base58(&s).map_err(serde::de::Error::custom)
    }
}

/// Well-known program and sysvar addresses.
///
/// Hardcoded as bytes so they are `const`; `program_ids::base58_round_trips`
/// in the test suite asserts every one of them re-encodes to the address in
/// its doc comment, so a typo cannot survive `cargo test`.
pub mod ids {
    use super::Pubkey;

    /// `11111111111111111111111111111111`
    pub const SYSTEM_PROGRAM: Pubkey = Pubkey([0; 32]);

    /// `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`
    pub const TOKEN_PROGRAM: Pubkey = Pubkey([
        6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133,
        237, 95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169,
    ]);

    /// `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`
    pub const TOKEN_2022_PROGRAM: Pubkey = Pubkey([
        6, 221, 246, 225, 238, 117, 143, 222, 24, 66, 93, 188, 228, 108, 205, 218, 182, 26, 252,
        77, 131, 185, 13, 39, 254, 189, 249, 40, 216, 161, 139, 252,
    ]);

    /// `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`
    pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = Pubkey([
        140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153,
        218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89,
    ]);

    /// `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`
    pub const MEMO_PROGRAM: Pubkey = Pubkey([
        5, 74, 83, 90, 153, 41, 33, 6, 77, 36, 232, 113, 96, 218, 56, 124, 124, 53, 181, 221, 188,
        146, 187, 129, 228, 31, 168, 64, 65, 5, 68, 141,
    ]);

    /// `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`
    pub const METAPLEX_METADATA_PROGRAM: Pubkey = Pubkey([
        11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, 4, 195, 205, 88, 184, 108,
        115, 26, 160, 253, 181, 73, 182, 209, 188, 3, 248, 41, 70,
    ]);

    /// `ComputeBudget111111111111111111111111111111`
    pub const COMPUTE_BUDGET_PROGRAM: Pubkey = Pubkey([
        3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187,
        197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0,
    ]);

    /// `SysvarRent111111111111111111111111111111111`
    pub const SYSVAR_RENT: Pubkey = Pubkey([
        6, 167, 213, 23, 25, 44, 92, 81, 33, 140, 201, 76, 61, 74, 241, 127, 88, 218, 238, 8, 155,
        161, 253, 68, 227, 219, 217, 138, 0, 0, 0, 0,
    ]);

    /// `SysvarRecentB1ockHashes11111111111111111111`
    pub const SYSVAR_RECENT_BLOCKHASHES: Pubkey = Pubkey([
        6, 167, 213, 23, 25, 44, 86, 142, 224, 138, 132, 95, 115, 210, 151, 136, 207, 3, 92, 49,
        69, 178, 26, 179, 68, 216, 6, 46, 169, 64, 0, 0,
    ]);

    /// `So11111111111111111111111111111111111111112` — wrapped SOL.
    pub const NATIVE_MINT: Pubkey = Pubkey([
        6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57, 220,
        26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 1,
    ]);

    /// `1nc1nerator11111111111111111111111111111111`
    pub const INCINERATOR: Pubkey = Pubkey([
        0, 51, 144, 114, 141, 52, 17, 96, 121, 189, 201, 17, 191, 255, 0, 219, 212, 77, 46, 205,
        204, 247, 156, 166, 225, 0, 56, 225, 0, 0, 0, 0,
    ]);
}