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
//! Durable nonce accounts.
//!
//! Trap number one in an approval-gated agent flow: the plugin builds a
//! transaction, drops it into a Telegram approval queue, and the human is at
//! lunch. A `recent_blockhash` is valid for 150 slots — about a minute — so by
//! the time they tap approve, the transaction is dead, and the agent has to
//! rebuild it, which means the human approved bytes that no longer exist.
//!
//! A durable nonce replaces the blockhash with a value stored in an account.
//! It stays valid until the nonce is advanced, which happens exactly once, in
//! the transaction itself. Build it now, sign it tomorrow, and replay is still
//! impossible.
//!
//! Setup is one-time and belongs to the operator, not the agent:
//!
//! ```text
//! solana-keygen new -o nonce.json
//! solana create-nonce-account nonce.json 0.0015 --nonce-authority <session-key>
//! ```

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

/// Serialized length of a nonce account.
pub const NONCE_ACCOUNT_LEN: usize = 80;

/// The initialized contents of a nonce account.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonceState {
    /// The only key allowed to advance or withdraw from this nonce.
    pub authority: Pubkey,
    /// The value to use as the transaction's `recent_blockhash`.
    pub durable_nonce: [u8; 32],
    /// Lamports per signature at the time the nonce was stored.
    pub lamports_per_signature: u64,
}

impl NonceState {
    /// Parse a fetched nonce account.
    ///
    /// Rejects anything not owned by the System Program, and anything still in
    /// the `Uninitialized` state: using a stale or foreign account's bytes as a
    /// blockhash produces a transaction that silently never lands.
    pub fn parse(account: &Account) -> Result<Self> {
        if !account.owner.is_zeroed() {
            return Err(Error::InvalidAccountData(format!(
                "nonce account is owned by {}, expected the system program",
                account.owner.abbreviated()
            )));
        }
        if account.data.len() < NONCE_ACCOUNT_LEN {
            return Err(Error::InvalidAccountData(format!(
                "nonce account is {} bytes, expected {NONCE_ACCOUNT_LEN}",
                account.data.len()
            )));
        }
        let version = u32::from_le_bytes(account.data[0..4].try_into().unwrap());
        match version {
            0 => return Err(Error::InvalidAccountData("nonce account is uninitialized".into())),
            1 => {}
            other => {
                return Err(Error::InvalidAccountData(format!(
                    "unknown nonce account version {other}"
                )))
            }
        }
        let mut authority = [0u8; 32];
        authority.copy_from_slice(&account.data[4..36]);
        let mut durable_nonce = [0u8; 32];
        durable_nonce.copy_from_slice(&account.data[36..68]);
        Ok(NonceState {
            authority: Pubkey::new_from_array(authority),
            durable_nonce,
            lamports_per_signature: u64::from_le_bytes(account.data[68..76].try_into().unwrap()),
        })
    }

    /// The nonce as a base58 string, the form an explorer shows.
    pub fn nonce_base58(&self) -> String {
        bs58::encode(self.durable_nonce).into_string()
    }
}