1use 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
23pub 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#[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(¶ms.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 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}