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
//! Building and serializing an unsigned v0 transaction.
//!
//! No `solana-sdk`, no `bincode`, no `borsh`. The wire format is small enough
//! to write out by hand, and writing it out by hand is what makes this compile
//! for `wasm32-wasip2` at all.
//!
//! Everything here is deliberately signature-free. A T1 plugin builds a
//! message, serializes it with empty signature slots, and hands the base64 to a
//! human or to a multisig. Nothing in this crate can sign, because nothing in
//! this crate ever holds a key.

pub mod instructions;

use base64::Engine;
use sha2::{Digest, Sha256};

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

/// One account referenced by an instruction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountMeta {
    /// The account's address.
    pub pubkey: Pubkey,
    /// True when a signature from this key is required.
    pub is_signer: bool,
    /// True when the instruction may modify it.
    pub is_writable: bool,
}

impl AccountMeta {
    /// Writable, not a signer.
    pub const fn writable(pubkey: Pubkey) -> Self {
        Self {
            pubkey,
            is_signer: false,
            is_writable: true,
        }
    }

    /// Read-only, not a signer.
    pub const fn readonly(pubkey: Pubkey) -> Self {
        Self {
            pubkey,
            is_signer: false,
            is_writable: false,
        }
    }

    /// Writable signer.
    pub const fn writable_signer(pubkey: Pubkey) -> Self {
        Self {
            pubkey,
            is_signer: true,
            is_writable: true,
        }
    }

    /// Read-only signer.
    pub const fn readonly_signer(pubkey: Pubkey) -> Self {
        Self {
            pubkey,
            is_signer: true,
            is_writable: false,
        }
    }
}

/// An instruction, before compilation into a message.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Instruction {
    /// The program to invoke.
    pub program_id: Pubkey,
    /// Accounts passed to it, in the order the program expects.
    pub accounts: Vec<AccountMeta>,
    /// Opaque instruction data.
    pub data: Vec<u8>,
}

/// A compiled v0 message, ready to serialize.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
    /// How many leading account keys must sign.
    pub num_required_signatures: u8,
    /// How many of the signers are read-only.
    pub num_readonly_signed: u8,
    /// How many of the non-signers are read-only.
    pub num_readonly_unsigned: u8,
    /// The account table, ordered by the runtime's privilege rules.
    pub account_keys: Vec<Pubkey>,
    /// A recent blockhash, or a durable nonce value.
    pub recent_blockhash: [u8; 32],
    /// Instructions with their accounts resolved to table indices.
    pub instructions: Vec<CompiledInstruction>,
}

/// An instruction with its accounts resolved to message indices.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompiledInstruction {
    /// Index of the program in the account table.
    pub program_id_index: u8,
    /// Indices of the instruction's accounts in the account table.
    pub accounts: Vec<u8>,
    /// Opaque instruction data.
    pub data: Vec<u8>,
}

/// Maximum size of a serialized transaction on the wire.
pub const PACKET_DATA_SIZE: usize = 1232;

impl Message {
    /// Compile instructions into a v0 message.
    ///
    /// Account ordering follows the runtime's rules exactly: the fee payer
    /// first, then writable signers, read-only signers, writable non-signers,
    /// and read-only non-signers. Duplicate references to the same key are
    /// merged with their privileges unioned, which is what lets a caller build
    /// instructions independently without tracking the account table.
    ///
    /// No address lookup tables. A payment or attestation transaction does not
    /// need them, and resolving a table would mean a second RPC round trip and
    /// a class of failure — a table entry changing between build and sign —
    /// that a human-approved flow should not carry.
    pub fn compile(
        payer: &Pubkey,
        instructions: &[Instruction],
        recent_blockhash: [u8; 32],
    ) -> Result<Self> {
        if instructions.is_empty() {
            return Err(Error::Encode("no instructions".into()));
        }

        // Merge every referenced account, unioning privileges.
        let mut keys: Vec<AccountMeta> = Vec::new();
        let push = |meta: AccountMeta, keys: &mut Vec<AccountMeta>| {
            if let Some(existing) = keys.iter_mut().find(|k| k.pubkey == meta.pubkey) {
                existing.is_signer |= meta.is_signer;
                existing.is_writable |= meta.is_writable;
            } else {
                keys.push(meta);
            }
        };

        push(AccountMeta::writable_signer(*payer), &mut keys);
        for ix in instructions {
            for meta in &ix.accounts {
                push(*meta, &mut keys);
            }
        }
        // Program ids are always read-only non-signers. Adding them last means
        // a program that is also an instruction account keeps its privileges.
        for ix in instructions {
            push(AccountMeta::readonly(ix.program_id), &mut keys);
        }

        // Stable partition into the four required buckets, payer pinned first.
        let payer_meta = keys
            .iter()
            .find(|k| k.pubkey == *payer)
            .copied()
            .ok_or_else(|| Error::Encode("payer vanished during compilation".into()))?;
        let rest: Vec<AccountMeta> = keys.into_iter().filter(|k| k.pubkey != *payer).collect();

        let mut ordered = vec![payer_meta];
        ordered.extend(rest.iter().filter(|k| k.is_signer && k.is_writable));
        ordered.extend(rest.iter().filter(|k| k.is_signer && !k.is_writable));
        ordered.extend(rest.iter().filter(|k| !k.is_signer && k.is_writable));
        ordered.extend(rest.iter().filter(|k| !k.is_signer && !k.is_writable));

        if ordered.len() > u8::MAX as usize {
            return Err(Error::Encode("more than 255 accounts".into()));
        }

        let num_required_signatures = ordered.iter().filter(|k| k.is_signer).count() as u8;
        let num_readonly_signed = ordered.iter().filter(|k| k.is_signer && !k.is_writable).count() as u8;
        let num_readonly_unsigned =
            ordered.iter().filter(|k| !k.is_signer && !k.is_writable).count() as u8;

        let account_keys: Vec<Pubkey> = ordered.iter().map(|k| k.pubkey).collect();
        let index_of = |pk: &Pubkey| -> Result<u8> {
            account_keys
                .iter()
                .position(|k| k == pk)
                .map(|i| i as u8)
                .ok_or_else(|| Error::Encode(format!("account {} not in table", pk.abbreviated())))
        };

        let mut compiled = Vec::with_capacity(instructions.len());
        for ix in instructions {
            let mut accounts = Vec::with_capacity(ix.accounts.len());
            for meta in &ix.accounts {
                accounts.push(index_of(&meta.pubkey)?);
            }
            compiled.push(CompiledInstruction {
                program_id_index: index_of(&ix.program_id)?,
                accounts,
                data: ix.data.clone(),
            });
        }

        Ok(Message {
            num_required_signatures,
            num_readonly_signed,
            num_readonly_unsigned,
            account_keys,
            recent_blockhash,
            instructions: compiled,
        })
    }

    /// Serialize to the v0 wire format.
    pub fn serialize(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(256);
        // v0 message prefix: high bit set, version in the low seven bits.
        out.push(0x80);
        out.push(self.num_required_signatures);
        out.push(self.num_readonly_signed);
        out.push(self.num_readonly_unsigned);

        encode_len(&mut out, self.account_keys.len());
        for key in &self.account_keys {
            out.extend_from_slice(key.as_bytes());
        }
        out.extend_from_slice(&self.recent_blockhash);

        encode_len(&mut out, self.instructions.len());
        for ix in &self.instructions {
            out.push(ix.program_id_index);
            encode_len(&mut out, ix.accounts.len());
            out.extend_from_slice(&ix.accounts);
            encode_len(&mut out, ix.data.len());
            out.extend_from_slice(&ix.data);
        }

        // Zero address table lookups.
        encode_len(&mut out, 0);
        out
    }

    /// SHA-256 of the serialized message, hex-encoded.
    ///
    /// The point of a build-then-approve flow is that the human approves the
    /// same bytes the tool described. This digest is what makes that checkable:
    /// print it next to the summary, and print it again in the wallet.
    pub fn digest(&self) -> String {
        let mut hasher = Sha256::new();
        hasher.update(self.serialize());
        let out: [u8; 32] = hasher.finalize().into();
        out.iter().map(|b| format!("{b:02x}")).collect()
    }

    /// The signers this message requires, in order. Index 0 is the fee payer.
    pub fn required_signers(&self) -> &[Pubkey] {
        &self.account_keys[..self.num_required_signatures as usize]
    }
}

/// A transaction carrying empty signature slots.
///
/// This is the artifact a T1 plugin returns: correctly framed, simulatable
/// with `sigVerify: false`, and useless to anyone who cannot sign it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnsignedTransaction {
    /// The compiled message. Signature slots are generated at serialization.
    pub message: Message,
}

impl UnsignedTransaction {
    /// Wrap a compiled message.
    pub fn new(message: Message) -> Self {
        Self { message }
    }

    /// Serialize with one all-zero signature per required signer.
    pub fn serialize(&self) -> Result<Vec<u8>> {
        let mut out = Vec::with_capacity(512);
        encode_len(&mut out, self.message.num_required_signatures as usize);
        for _ in 0..self.message.num_required_signatures {
            out.extend_from_slice(&[0u8; 64]);
        }
        out.extend_from_slice(&self.message.serialize());
        if out.len() > PACKET_DATA_SIZE {
            return Err(Error::Encode(format!(
                "transaction is {} bytes, over the {PACKET_DATA_SIZE}-byte limit",
                out.len()
            )));
        }
        Ok(out)
    }

    /// Standard base64, the encoding `sendTransaction` and `simulateTransaction`
    /// accept.
    pub fn to_base64(&self) -> Result<String> {
        Ok(base64::engine::general_purpose::STANDARD.encode(self.serialize()?))
    }
}

/// Solana's `ShortVec` length prefix: seven bits per byte, high bit continues.
pub fn encode_len(out: &mut Vec<u8>, mut n: usize) {
    loop {
        let mut byte = (n & 0x7f) as u8;
        n >>= 7;
        if n == 0 {
            out.push(byte);
            break;
        }
        byte |= 0x80;
        out.push(byte);
    }
}

/// Decode a `ShortVec` length prefix, returning the value and bytes consumed.
pub fn decode_len(input: &[u8]) -> Result<(usize, usize)> {
    let mut value = 0usize;
    for (i, byte) in input.iter().take(3).enumerate() {
        value |= ((byte & 0x7f) as usize) << (i * 7);
        if byte & 0x80 == 0 {
            return Ok((value, i + 1));
        }
    }
    Err(Error::Encode("short vec length prefix is too long".into()))
}

/// Decode a base58 blockhash into raw bytes.
pub fn blockhash_from_base58(s: &str) -> Result<[u8; 32]> {
    let mut out = [0u8; 32];
    match bs58::decode(s).onto(&mut out) {
        Ok(32) => Ok(out),
        _ => Err(Error::InvalidArgument(format!(
            "`{}` is not a 32-byte blockhash",
            crate::shape::clip(s, 24)
        ))),
    }
}