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
//! Output shaping: turning chain data into the ~200 tokens a model needs.
//!
//! Every byte a tool returns is re-sent to the model on every subsequent turn
//! of the conversation. A `getProgramAccounts` dump is not just noisy, it is a
//! recurring line item on the operator's bill. These helpers exist so a plugin
//! has no excuse to return raw JSON.

/// Rough token count for a budget assertion in a test.
///
/// Four characters per token is the usual English-text approximation; it is
/// close enough to catch "this tool returns 40KB" and nowhere near precise
/// enough to bill against.
pub fn estimate_tokens(s: &str) -> usize {
    s.len().div_ceil(4)
}

/// Render a raw base-unit amount with `decimals` applied.
///
/// Integer arithmetic throughout: a `u64` supply at 9 decimals does not survive
/// an `f64` round trip, and a payment summary that disagrees with the
/// transaction by one lamport is a summary nobody should sign.
pub fn ui_amount(raw: u128, decimals: u8) -> String {
    if decimals == 0 {
        return group_thousands(&raw.to_string());
    }
    let scale = 10u128.pow(decimals as u32);
    let whole = raw / scale;
    let frac = raw % scale;
    if frac == 0 {
        return group_thousands(&whole.to_string());
    }
    let frac_str = format!("{frac:0width$}", width = decimals as usize);
    let frac_str = frac_str.trim_end_matches('0');
    format!("{}.{}", group_thousands(&whole.to_string()), frac_str)
}

/// Short form for large numbers: `1.2M`, `934.5K`.
pub fn compact_amount(raw: u128, decimals: u8) -> String {
    let scale = 10u128.pow(decimals as u32);
    let whole = raw / scale;
    match whole {
        0..=9_999 => ui_amount(raw, decimals),
        10_000..=999_999 => format!("{:.1}K", whole as f64 / 1_000.0),
        1_000_000..=999_999_999 => format!("{:.1}M", whole as f64 / 1_000_000.0),
        _ => format!("{:.1}B", whole as f64 / 1_000_000_000.0),
    }
}

/// Parse a human decimal string into base units.
///
/// Rejects anything that is not a plain non-negative decimal, and rejects more
/// fractional digits than the mint has — silently truncating "1.005" to "1.00"
/// on a 2-decimal mint is the kind of rounding a payment tool must never do on
/// its own.
pub fn parse_amount(input: &str, decimals: u8) -> Result<u128, String> {
    let s = input.trim();
    if s.is_empty() {
        return Err("amount is empty".into());
    }
    if s.starts_with('-') {
        return Err("amount must not be negative".into());
    }
    let (whole, frac) = match s.split_once('.') {
        Some((w, f)) => (w, f),
        None => (s, ""),
    };
    if whole.is_empty() && frac.is_empty() {
        return Err("amount is empty".into());
    }
    if !whole.chars().all(|c| c.is_ascii_digit()) || !frac.chars().all(|c| c.is_ascii_digit()) {
        return Err(format!("`{}` is not a decimal number", clip(s, 24)));
    }
    if frac.len() > decimals as usize {
        return Err(format!(
            "amount has {} decimal places but the mint has {decimals}",
            frac.len()
        ));
    }
    let padded = format!("{whole}{frac}{}", "0".repeat(decimals as usize - frac.len()));
    let padded = if padded.is_empty() { "0" } else { &padded };
    padded
        .parse::<u128>()
        .map_err(|_| "amount is too large".to_string())
}

/// `part` as a percentage of `whole`, one decimal place. `0.0` when `whole` is
/// zero, so a fresh mint does not read as 100% concentrated.
pub fn percent_of(part: u128, whole: u128) -> f64 {
    if whole == 0 {
        return 0.0;
    }
    (part as f64 / whole as f64) * 100.0
}

/// Insert thousands separators into a digit string.
fn group_thousands(digits: &str) -> String {
    let mut out = String::with_capacity(digits.len() + digits.len() / 3);
    let n = digits.len();
    for (i, c) in digits.chars().enumerate() {
        if i > 0 && (n - i) % 3 == 0 {
            out.push(',');
        }
        out.push(c);
    }
    out
}

/// Truncate to `max` characters with an ellipsis.
pub fn clip(s: &str, max: usize) -> String {
    if s.chars().count() <= max {
        return s.to_string();
    }
    let mut out: String = s.chars().take(max).collect();
    out.push('');
    out
}

/// Accumulate output lines under a hard character budget.
///
/// A tool that grows its output with the number of findings will eventually
/// blow the context window on the one token that has forty of them. This makes
/// the ceiling explicit and tells the model what it lost.
pub struct Budget {
    lines: Vec<String>,
    used: usize,
    max_chars: usize,
    dropped: usize,
}

impl Budget {
    /// A budget in characters. 1200 characters is roughly 300 tokens.
    pub fn new(max_chars: usize) -> Self {
        Self {
            lines: Vec::new(),
            used: 0,
            max_chars,
            dropped: 0,
        }
    }

    /// Add a line if it fits; otherwise count it as dropped.
    pub fn push(&mut self, line: impl Into<String>) {
        let line = line.into();
        let cost = line.len() + 1;
        if self.used + cost > self.max_chars {
            self.dropped += 1;
            return;
        }
        self.used += cost;
        self.lines.push(line);
    }

    /// Add a line even if it exceeds the budget. For the verdict, which must
    /// never be the thing that gets dropped.
    pub fn push_always(&mut self, line: impl Into<String>) {
        let line = line.into();
        self.used += line.len() + 1;
        self.lines.push(line);
    }

    /// How many lines did not fit.
    pub fn dropped(&self) -> usize {
        self.dropped
    }

    /// Render, appending a note when anything was dropped.
    pub fn render(&self) -> String {
        let mut out = self.lines.join("\n");
        if self.dropped > 0 {
            out.push_str(&format!("\n(+{} more, omitted for length)", self.dropped));
        }
        out
    }
}