Skip to main content

ifx_sdk/
scratch.rs

1//! Off-chain Frame tape planner (`cursor` + binding indices).
2
3use std::collections::HashMap;
4
5use ifx_core::layout::{plan_record_offsets, record_byte_length};
6use ifx_core::U8LenVec;
7use ifx_core::wire::{Expr, LetBinding, ValueType};
8use solana_sdk::instruction::{AccountMeta, Instruction};
9use solana_sdk::pubkey::Pubkey;
10
11use crate::binding::{eval_const_bool, eval_const_u64, infer_binding_ty, remap_account_index};
12use crate::constants::{index_cap_for_tape_len, DEFAULT_IFX_PROGRAM_ID};
13use crate::error::ScratchError;
14use crate::frame_authority::public_frame_authority;
15use crate::ix::{
16    build_ix_assert, build_ix_create_frame, build_ix_let, build_ix_reset_frame,
17    let_args_from_bindings, CreateFrameParams, IxOpts,
18};
19use crate::let_builder::LetBuilder;
20use crate::typed::ScratchValue;
21use crate::wire_ix::AssertMultiArgs;
22
23/// Result of [`FrameScratch::plan_new_frame`].
24pub struct PlanNewFrameResult {
25    pub scratch: FrameScratch,
26    pub ix_create: Instruction,
27    pub frame: Pubkey,
28    pub frame_bump: u8,
29}
30
31pub struct PlanNewFrameParams<'a> {
32    pub payer: Pubkey,
33    pub frame_id: &'a [u8; 32],
34    pub authority: Pubkey,
35    pub tape_len: u32,
36    pub program_id: Option<Pubkey>,
37}
38
39/// Off-chain mirror of on-chain Frame session planning.
40#[derive(Clone, Debug)]
41pub struct FrameScratch {
42    pub frame: Pubkey,
43    pub program_id: Pubkey,
44    pub authority: Pubkey,
45    pub tape_len: Option<u32>,
46    pub index_cap: Option<u16>,
47    pub cursor: u32,
48    pub next_index: u8,
49    index_types: HashMap<u8, ValueType>,
50}
51
52impl FrameScratch {
53    pub fn new(
54        frame: Pubkey,
55        tape_len: Option<u32>,
56        program_id: Option<Pubkey>,
57        authority: Pubkey,
58    ) -> Self {
59        let program_id = program_id.unwrap_or(DEFAULT_IFX_PROGRAM_ID);
60        let index_cap = tape_len.map(index_cap_for_tape_len);
61        Self {
62            frame,
63            program_id,
64            authority,
65            tape_len,
66            index_cap,
67            cursor: 0,
68            next_index: 0,
69            index_types: HashMap::new(),
70        }
71    }
72
73    pub fn plan_new_frame(params: PlanNewFrameParams<'_>) -> Result<PlanNewFrameResult, ScratchError> {
74        let program_id = params.program_id.unwrap_or(DEFAULT_IFX_PROGRAM_ID);
75        let created = build_ix_create_frame(CreateFrameParams {
76            payer: params.payer,
77            frame_id: params.frame_id,
78            authority: params.authority,
79            tape_len: params.tape_len,
80            opts: IxOpts {
81                program_id: Some(program_id),
82            },
83        })?;
84        Ok(PlanNewFrameResult {
85            scratch: Self::new(
86                created.frame,
87                Some(params.tape_len),
88                Some(program_id),
89                params.authority,
90            ),
91            ix_create: created.instruction,
92            frame: created.frame,
93            frame_bump: created.frame_bump,
94        })
95    }
96
97    pub fn plan_public_frame(params: PlanNewFrameParams<'_>) -> Result<PlanNewFrameResult, ScratchError> {
98        let program_id = params.program_id.unwrap_or(DEFAULT_IFX_PROGRAM_ID);
99        let authority = public_frame_authority(&params.payer, params.frame_id, &program_id);
100        Self::plan_new_frame(PlanNewFrameParams {
101            payer: params.payer,
102            frame_id: params.frame_id,
103            authority,
104            tape_len: params.tape_len,
105            program_id: Some(program_id),
106        })
107    }
108
109    /// Planner for an **existing** public Frame (`authority == frame` PDA).
110    pub fn for_public_frame(
111        frame: Pubkey,
112        tape_len: Option<u32>,
113        program_id: Option<Pubkey>,
114    ) -> Self {
115        let program_id = program_id.unwrap_or(DEFAULT_IFX_PROGRAM_ID);
116        Self::new(frame, tape_len, Some(program_id), frame)
117    }
118
119    pub fn ix_opts(&self) -> IxOpts {
120        IxOpts {
121            program_id: Some(self.program_id),
122        }
123    }
124
125    pub fn let_builder(&mut self) -> LetBuilder<'_> {
126        LetBuilder::new(self)
127    }
128
129    pub fn ix_reset(&mut self) -> Instruction {
130        self.cursor = 0;
131        self.next_index = 0;
132        self.index_types.clear();
133        build_ix_reset_frame(self.frame, self.authority, self.ix_opts())
134    }
135
136    pub fn ix_let_single(&self, value: &ScratchValue) -> Result<Instruction, ScratchError> {
137        let args = let_args_from_bindings(std::slice::from_ref(&value.binding));
138        build_ix_let(
139            self.frame,
140            self.authority,
141            &args,
142            &value.remaining,
143            self.ix_opts(),
144        )
145    }
146
147    pub fn ix_let_batch(
148        &self,
149        bindings: &[ScratchValue],
150        remaining: &[AccountMeta],
151    ) -> Result<Instruction, ScratchError> {
152        let args = let_args_from_bindings(
153            &bindings.iter().map(|v| v.binding.clone()).collect::<Vec<_>>(),
154        );
155        build_ix_let(
156            self.frame,
157            self.authority,
158            &args,
159            remaining,
160            self.ix_opts(),
161        )
162    }
163
164    pub fn ix_assert(&self, cond: &Expr) -> Result<Instruction, ScratchError> {
165        build_ix_assert(self.frame, cond, self.ix_opts())
166    }
167
168    pub fn let_eval(&mut self, expr: Expr) -> Result<ScratchValue, ScratchError> {
169        self.plan(LetBinding::Eval { expr }, &[])
170    }
171
172    pub fn let_const_u64(&mut self, n: u64) -> Result<ScratchValue, ScratchError> {
173        self.plan(eval_const_u64(n), &[])
174    }
175
176    pub fn let_const_bool(&mut self, v: bool) -> Result<ScratchValue, ScratchError> {
177        self.plan(eval_const_bool(v), &[])
178    }
179
180    pub fn let_lamports(&mut self, account: Pubkey) -> Result<ScratchValue, ScratchError> {
181        self.plan(
182            LetBinding::AccountLamports { account_index: 0 },
183            &[readonly_meta(account)],
184        )
185    }
186
187    pub fn plan_at_remaining_index(
188        &mut self,
189        binding: LetBinding,
190        remaining_account_index: u8,
191    ) -> Result<ScratchValue, ScratchError> {
192        self.plan(
193            remap_account_index(binding, remaining_account_index),
194            &[],
195        )
196    }
197
198    pub fn ix_assert_multi(&self, conds: &[Expr]) -> Result<Instruction, ScratchError> {
199        crate::ix::build_ix_assert_multi(
200            self.frame,
201            &AssertMultiArgs {
202                conds: U8LenVec(conds.to_vec()),
203            },
204            self.ix_opts(),
205        )
206    }
207
208    pub fn ix_close(&self, authority: Pubkey) -> Instruction {
209        crate::ix::build_ix_close_frame(self.frame, authority, self.ix_opts())
210    }
211
212    pub fn ix_cpi(&self, built: &crate::cpi::CpiWireBuildResult) -> Result<Instruction, ScratchError> {
213        crate::ix::build_ix_cpi(self.frame, &built.cpi, &built.remaining, self.ix_opts())
214    }
215
216    pub fn ix_if_else(
217        &self,
218        args: &crate::wire_ix::IfElseArgs,
219        remaining: &[AccountMeta],
220    ) -> Result<Instruction, ScratchError> {
221        crate::ix::build_ix_if_else(self.frame, args, remaining, self.ix_opts())
222    }
223
224    pub(crate) fn plan(
225        &mut self,
226        binding: LetBinding,
227        let_remaining: &[AccountMeta],
228    ) -> Result<ScratchValue, ScratchError> {
229        let ty = infer_binding_ty(&binding, &self.index_types)?;
230        let binding_index = self.next_index;
231        if let Some(cap) = self.index_cap {
232            if binding_index as u16 >= cap {
233                return Err(ScratchError::IndexCapReached {
234                    index: binding_index,
235                    cap,
236                });
237            }
238        }
239        let (_, end_cursor) = plan_record_offsets(self.cursor, ty).map_err(|_| {
240            ScratchError::TapeExceeded {
241                end_cursor: self.cursor,
242                tape_len: self.tape_len.unwrap_or(u32::MAX),
243                record_len: record_byte_length(ty),
244            }
245        })?;
246        if let Some(tape_len) = self.tape_len {
247            if end_cursor > tape_len {
248                return Err(ScratchError::TapeExceeded {
249                    end_cursor,
250                    tape_len,
251                    record_len: record_byte_length(ty),
252                });
253            }
254        }
255        self.cursor = end_cursor;
256        self.next_index = self.next_index.saturating_add(1);
257        self.index_types.insert(binding_index, ty);
258        Ok(ScratchValue {
259            binding,
260            index: binding_index,
261            ty,
262            remaining: let_remaining.to_vec(),
263        })
264    }
265}
266
267fn readonly_meta(pubkey: Pubkey) -> AccountMeta {
268    AccountMeta {
269        pubkey,
270        is_signer: false,
271        is_writable: false,
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::constants::{IFX_LOCALNET_PROGRAM_ID, IX_DISC_CREATE_FRAME, IX_DISC_LET};
279    use ifx_core::wire::LetBinding;
280    use solana_sdk::signature::{Keypair, Signer};
281
282    fn on_curve_authority() -> Pubkey {
283        Keypair::new().pubkey()
284    }
285
286    #[test]
287    fn plan_public_frame_sets_frame_authority() {
288        let payer = Pubkey::new_unique();
289        let mut frame_id = [0u8; 32];
290        frame_id[0] = 11;
291        let plan = FrameScratch::plan_public_frame(PlanNewFrameParams {
292            payer,
293            frame_id: &frame_id,
294            authority: Pubkey::default(),
295            tape_len: 512,
296            program_id: Some(IFX_LOCALNET_PROGRAM_ID),
297        })
298        .unwrap();
299        assert_eq!(plan.scratch.authority, plan.frame);
300        assert_eq!(plan.ix_create.data[0], IX_DISC_CREATE_FRAME);
301        let auth = Pubkey::try_from(&plan.ix_create.data[1 + 32..1 + 64]).unwrap();
302        assert_eq!(auth, plan.frame);
303    }
304
305    #[test]
306    fn for_public_frame_sets_frame_authority() {
307        let frame = Pubkey::new_unique();
308        let s = FrameScratch::for_public_frame(frame, Some(512), Some(IFX_LOCALNET_PROGRAM_ID));
309        assert_eq!(s.authority, frame);
310        assert_eq!(s.frame, frame);
311        assert_eq!(s.cursor, 0);
312        assert_eq!(s.next_index, 0);
313    }
314
315    #[test]
316    fn let_lamports_remaining_index() {
317        let frame = Pubkey::new_unique();
318        let user = Pubkey::new_unique();
319        let mut s = FrameScratch::new(
320            frame,
321            Some(512),
322            Some(IFX_LOCALNET_PROGRAM_ID),
323            on_curve_authority(),
324        );
325        let sv = s.let_lamports(user).unwrap();
326        assert_eq!(sv.remaining.len(), 1);
327        assert_eq!(sv.remaining[0].pubkey, user);
328        assert_eq!(sv.index, 0);
329        let ix = s.ix_let_single(&sv).unwrap();
330        assert_eq!(ix.accounts[2].pubkey, user);
331    }
332
333    #[test]
334    fn let_const_u64_empty_remaining() {
335        let frame = Pubkey::new_unique();
336        let mut s = FrameScratch::new(
337            frame,
338            Some(512),
339            Some(IFX_LOCALNET_PROGRAM_ID),
340            on_curve_authority(),
341        );
342        let sv = s.let_const_u64(42).unwrap();
343        assert!(sv.remaining.is_empty());
344    }
345
346    #[test]
347    fn let_builder_dedupes_accounts() {
348        let frame = Pubkey::new_unique();
349        let user = Pubkey::new_unique();
350        let mut s = FrameScratch::new(
351            frame,
352            Some(512),
353            Some(IFX_LOCALNET_PROGRAM_ID),
354            on_curve_authority(),
355        );
356        let mut b = s.let_builder();
357        b.lamports(user).unwrap();
358        b.data_len(user).unwrap();
359        let fin = b.finish();
360        assert_eq!(fin.remaining.len(), 1);
361        let ix = b.build_ix().unwrap();
362        assert_eq!(ix.accounts.len(), 3);
363    }
364
365    #[test]
366    fn minimal_business_wire() {
367        let frame = Pubkey::new_unique();
368        let mut s = FrameScratch::new(
369            frame,
370            Some(512),
371            Some(IFX_LOCALNET_PROGRAM_ID),
372            on_curve_authority(),
373        );
374        s.ix_reset();
375        let one = s.let_const_u64(1).unwrap();
376        let ix = s.ix_let_single(&one).unwrap();
377        assert_eq!(ix.data[0], IX_DISC_LET);
378        assert_eq!(&ix.data[1..], &[1, 2, 5, 1, 0, 0, 0, 0, 0, 0, 0]);
379        assert!(matches!(one.binding, LetBinding::Eval { .. }));
380    }
381
382    #[test]
383    fn spl_token_amount_golden_matches_core() {
384        let frame = Pubkey::new_unique();
385        let token_acc = Pubkey::new_unique();
386        let mut s = FrameScratch::new(
387            frame,
388            Some(512),
389            Some(IFX_LOCALNET_PROGRAM_ID),
390            on_curve_authority(),
391        );
392        let sv = s
393            .plan(
394                LetBinding::SplTokenAccountAmount { account_index: 0 },
395                &[readonly_meta(token_acc)],
396            )
397            .unwrap();
398        let ix = s.ix_let_single(&sv).unwrap();
399        assert_eq!(&ix.data[1..], &[1, 9, 0]);
400    }
401}