Skip to main content

palinurus_core/
response.rs

1//! Response shaping — keep RPC/IX outputs to ~200 tokens, not 40KB JSON.
2//!
3//! The bounty's trap #3: raw `getAccountInfo`/`getSignaturesForAddress` responses
4//! nuke the agent's context window and cost the operator money. Judges call
5//! `execute` and count tokens. Every shaper here stays within a ~200-token budget
6//! (≈ 800 chars at the `chars / 4` heuristic — see [`estimate_tokens`]).
7//!
8//! Shapers are pure string formatting — no consensus concerns, no network.
9
10use crate::base58::Pubkey;
11use crate::durable_nonce::{parse_nonce_account, NonceState, NonceVersion};
12use crate::rpc::{AccountInfo, TxSummary};
13
14/// What kind of account `shape_account_info` is summarizing.
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum AccountKind {
17    /// A durable-nonce account — parsed with `parse_nonce_account` for a real summary.
18    Nonce,
19    /// A Solana Attestation Service attestation account — owner + len + data prefix.
20    Attestation,
21    /// Anything else — owner + len + data prefix.
22    Other,
23}
24
25/// Approximate token count via the `chars / 4` heuristic. The bounty's shaping
26/// budget is ~200 tokens; [`shape_account_info`] / [`shape_signatures`] /
27/// [`shape_attestation_result`] all stay under it.
28pub fn estimate_tokens(s: &str) -> usize {
29    s.chars().count() / 4
30}
31
32/// Short pubkey form: first 4 + `…` + last 4 (e.g. `Et5C…T2p`). ~9 chars, ~2 tokens.
33pub fn short_pubkey(pk: &Pubkey) -> String {
34    let s = pk.to_string();
35    let bytes = s.as_bytes();
36    if bytes.len() <= 8 {
37        return s;
38    }
39    format!("{}…{}", &s[..4], &s[s.len() - 4..])
40}
41
42/// One-line summary of an account, tuned to the `kind`. Never dumps the raw bytes.
43pub fn shape_account_info(info: &AccountInfo, kind: AccountKind) -> String {
44    match kind {
45        AccountKind::Nonce => shape_nonce_account(info),
46        AccountKind::Attestation => format!(
47            "Attestation account owner={} len={}B lamports={} data[0:8]={}",
48            short_pubkey(&info.owner),
49            info.data.len(),
50            info.lamports,
51            hex_prefix(&info.data, 8),
52        ),
53        AccountKind::Other => format!(
54            "Account owner={} len={}B lamports={} executable={} data[0:8]={}",
55            short_pubkey(&info.owner),
56            info.data.len(),
57            info.lamports,
58            info.executable,
59            hex_prefix(&info.data, 8),
60        ),
61    }
62}
63
64fn shape_nonce_account(info: &AccountInfo) -> String {
65    match parse_nonce_account(&info.data) {
66        Ok(nonce) => match nonce.version {
67            NonceVersion::Legacy => "Nonce: legacy (unsupported for versioned tx)".to_string(),
68            NonceVersion::Current => match nonce.state {
69                NonceState::Uninitialized => "Nonce: current/uninitialized".to_string(),
70                NonceState::Initialized(d) => format!(
71                    "Nonce: current/initialized auth={} nonce={} lamports_per_sig={}",
72                    short_pubkey(&d.authority),
73                    short_pubkey(&Pubkey::from_bytes(d.durable_nonce)),
74                    d.lamports_per_signature,
75                ),
76            },
77        },
78        Err(e) => format!(
79            "Nonce: parse error ({:?}) owner={} len={}B data[0:8]={}",
80            e,
81            short_pubkey(&info.owner),
82            info.data.len(),
83            hex_prefix(&info.data, 8),
84        ),
85    }
86}
87
88/// Compact list of recent signatures for an address. Shows the first few + a
89/// "+N more" tail when the list is long.
90pub fn shape_signatures(sigs: &[TxSummary]) -> String {
91    if sigs.is_empty() {
92        return "No recent transactions.".to_string();
93    }
94    let total = sigs.len();
95    const SHOW: usize = 5;
96    let head: Vec<String> = sigs.iter().take(SHOW).map(|t| {
97        let status = match &t.err {
98            None => "ok".to_string(),
99            Some(e) => format!("FAIL:{}", truncate(e, 20)),
100        };
101        let sig_short = truncate(&t.signature, 8);
102        format!("{}(slot {}, {})", sig_short, t.slot, status)
103    }).collect();
104    let mut out = format!("{} txs: {}", total, head.join(" | "));
105    if total > SHOW {
106        out.push_str(&format!(" | +{} more", total - SHOW));
107    }
108    out
109}
110
111/// One-line result of a `create_attestation` submission: PDA + nonce + signature.
112pub fn shape_attestation_result(sig: &str, pda: &Pubkey, nonce: &Pubkey) -> String {
113    format!(
114        "Attested → PDA {} (nonce {}) sig {}",
115        short_pubkey(pda),
116        short_pubkey(nonce),
117        truncate(sig, 12),
118    )
119}
120
121// ---- helpers ----
122
123fn hex_prefix(data: &[u8], n: usize) -> String {
124    let n = n.min(data.len());
125    let mut s = String::with_capacity(n * 2);
126    for b in data.iter().take(n) {
127        s.push_str(&format!("{:02x}", b));
128    }
129    if data.len() > n {
130        s.push('…');
131    }
132    s
133}
134
135fn truncate(s: &str, max: usize) -> String {
136    if s.chars().count() <= max {
137        return s.to_string();
138    }
139    let head: String = s.chars().take(max).collect();
140    format!("{head}…")
141}