Skip to main content

ifx_sdk/
decode.rs

1//! On-chain Frame account decode and tape readback (tests / local debug only).
2
3use ifx_core::layout::{decode_typed, TypedValue};
4use solana_sdk::pubkey::Pubkey;
5
6use crate::constants::ACCOUNT_DISC_FRAME;
7use crate::error::ScratchError;
8use crate::typed::ScratchValue;
9
10const OFF_AUTHORITY: usize = 1;
11const OFF_CURSOR: usize = 33;
12const OFF_INDEX_COUNT: usize = 37;
13const OFF_INDEX_CAP: usize = 39;
14const OFF_GENERATION: usize = 41;
15const OFF_PAYLOAD_AT_LEN: usize = 49;
16const OFF_PAYLOAD_AT: usize = 53;
17
18/// Snapshot of on-chain Frame account data.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct DecodedFrame {
21    pub authority: Pubkey,
22    pub cursor: u32,
23    pub index_count: u16,
24    pub index_cap: u16,
25    pub generation: u64,
26    pub payload_at: Vec<u16>,
27    pub tape: Vec<u8>,
28}
29
30impl DecodedFrame {
31    pub fn read_u64(&self, sv: &ScratchValue) -> Result<u64, ScratchError> {
32        match self.read_value(sv)? {
33            TypedValue::U64(v) => Ok(v),
34            other => Err(ScratchError::FrameRead(format!(
35                "expected u64 at index {}, got {:?}",
36                sv.index, other
37            ))),
38        }
39    }
40
41    pub fn read_value(&self, sv: &ScratchValue) -> Result<TypedValue, ScratchError> {
42        let ty = sv.ty;
43        let off = self.payload_offset(sv.index)?;
44        let end = off as usize + ty.size();
45        if end > self.tape.len() {
46            return Err(ScratchError::FrameRead(format!(
47                "read past frame tape ({end} > {}) at byte {off}",
48                self.tape.len()
49            )));
50        }
51        decode_typed(ty, &self.tape[off as usize..end])
52            .map_err(|e| ScratchError::FrameRead(format!("decode payload: {e:?}")))
53    }
54
55    fn payload_offset(&self, index: u8) -> Result<u16, ScratchError> {
56        if index as usize >= self.index_count as usize {
57            return Err(ScratchError::FrameRead(format!(
58                "binding index {index} out of range (index_count={})",
59                self.index_count
60            )));
61        }
62        self.payload_at
63            .get(index as usize)
64            .copied()
65            .ok_or_else(|| ScratchError::FrameRead(format!("missing payload_at[{index}]")))
66    }
67}
68
69/// Parse Frame account bytes (including 1-byte discriminator).
70pub fn decode_frame_account(data: &[u8]) -> Result<DecodedFrame, ScratchError> {
71    if data.len() < OFF_PAYLOAD_AT + 4 + 4 {
72        return Err(ScratchError::FrameDecode("frame account data too short".into()));
73    }
74    if data[0] != ACCOUNT_DISC_FRAME {
75        return Err(ScratchError::FrameDecode(
76            "invalid Frame account discriminator".into(),
77        ));
78    }
79    let authority = Pubkey::try_from(&data[OFF_AUTHORITY..OFF_AUTHORITY + 32])
80        .map_err(|_| ScratchError::FrameDecode("invalid authority pubkey".into()))?;
81    let cursor = u32::from_le_bytes(data[OFF_CURSOR..OFF_CURSOR + 4].try_into().unwrap());
82    let index_count = u16::from_le_bytes(data[OFF_INDEX_COUNT..OFF_INDEX_COUNT + 2].try_into().unwrap());
83    let index_cap = u16::from_le_bytes(data[OFF_INDEX_CAP..OFF_INDEX_CAP + 2].try_into().unwrap());
84    let generation = u64::from_le_bytes(data[OFF_GENERATION..OFF_GENERATION + 8].try_into().unwrap());
85    let payload_at_len = u32::from_le_bytes(
86        data[OFF_PAYLOAD_AT_LEN..OFF_PAYLOAD_AT_LEN + 4]
87            .try_into()
88            .unwrap(),
89    );
90    let mut o = OFF_PAYLOAD_AT;
91    let mut payload_at = Vec::with_capacity(payload_at_len as usize);
92    for _ in 0..payload_at_len {
93        if o + 2 > data.len() {
94            return Err(ScratchError::FrameDecode("truncated payload_at".into()));
95        }
96        payload_at.push(u16::from_le_bytes(data[o..o + 2].try_into().unwrap()));
97        o += 2;
98    }
99    if payload_at_len != index_cap as u32 {
100        return Err(ScratchError::FrameDecode(format!(
101            "payload_at length mismatch: {payload_at_len} vs index_cap {index_cap}"
102        )));
103    }
104    if o + 4 > data.len() {
105        return Err(ScratchError::FrameDecode("truncated tape length".into()));
106    }
107    let tape_len = u32::from_le_bytes(data[o..o + 4].try_into().unwrap());
108    o += 4;
109    if o + tape_len as usize != data.len() {
110        return Err(ScratchError::FrameDecode("tape length mismatch".into()));
111    }
112    let tape = data[o..o + tape_len as usize].to_vec();
113    Ok(DecodedFrame {
114        authority,
115        cursor,
116        index_count,
117        index_cap,
118        generation,
119        payload_at,
120        tape,
121    })
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use ifx_core::wire::{Expr, LetBinding, ValueType};
128
129    #[test]
130    fn decode_rejects_short_data() {
131        assert!(decode_frame_account(&[ACCOUNT_DISC_FRAME]).is_err());
132    }
133
134    #[test]
135    fn read_u64_roundtrip_on_tape() {
136        let dec = DecodedFrame {
137            authority: Pubkey::new_unique(),
138            cursor: 8,
139            index_count: 1,
140            index_cap: 1,
141            generation: 0,
142            payload_at: vec![0],
143            tape: 1u64.to_le_bytes().to_vec(),
144        };
145        let sv = ScratchValue {
146            binding: LetBinding::Eval {
147                expr: Expr::ConstU64(1),
148            },
149            index: 0,
150            ty: ValueType::U64,
151            remaining: vec![],
152        };
153        assert_eq!(dec.read_u64(&sv).unwrap(), 1);
154    }
155}