1use core::fmt;
9
10#[derive(Clone, Copy)]
18pub struct ContextAccountDescriptor {
19 pub name: &'static str,
21 pub kind: &'static str,
23 pub writable: bool,
25 pub signer: bool,
27 pub layout_ref: &'static str,
29 pub policy_ref: &'static str,
31 pub seeds: &'static [&'static str],
33 pub optional: bool,
35 pub lifecycle: AccountLifecycle,
39 pub payer: &'static str,
42 pub init_space: u32,
45 pub has_one: &'static [&'static str],
48 pub expected_address: &'static str,
51 pub expected_owner: &'static str,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum AccountLifecycle {
64 Existing,
66 Init,
68 Realloc,
70 Close,
72}
73
74impl AccountLifecycle {
75 pub const fn as_str(&self) -> &'static str {
76 match self {
77 AccountLifecycle::Existing => "existing",
78 AccountLifecycle::Init => "init",
79 AccountLifecycle::Realloc => "realloc",
80 AccountLifecycle::Close => "close",
81 }
82 }
83}
84
85impl fmt::Display for ContextAccountDescriptor {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "{}: {}", self.name, self.kind)?;
88 if self.writable {
89 write!(f, " [mut]")?;
90 }
91 if self.signer {
92 write!(f, " [signer]")?;
93 }
94 if !self.layout_ref.is_empty() {
95 write!(f, " layout={}", self.layout_ref)?;
96 }
97 if !self.policy_ref.is_empty() {
98 write!(f, " policy={}", self.policy_ref)?;
99 }
100 if self.optional {
101 write!(f, " [optional]")?;
102 }
103 if !self.seeds.is_empty() {
104 write!(f, " seeds=[")?;
105 for (i, s) in self.seeds.iter().enumerate() {
106 if i > 0 {
107 write!(f, ", ")?;
108 }
109 write!(f, "{}", s)?;
110 }
111 write!(f, "]")?;
112 }
113 Ok(())
114 }
115}
116
117#[derive(Clone, Copy)]
123pub struct ContextDescriptor {
124 pub name: &'static str,
126 pub accounts: &'static [ContextAccountDescriptor],
128 pub policies: &'static [&'static str],
130 pub receipts_expected: bool,
132 pub mutation_classes: &'static [&'static str],
134}
135
136impl ContextDescriptor {
137 pub const fn account_count(&self) -> usize {
139 self.accounts.len()
140 }
141
142 pub fn signer_count(&self) -> usize {
144 let mut count = 0;
145 let mut i = 0;
146 while i < self.accounts.len() {
147 if self.accounts[i].signer {
148 count += 1;
149 }
150 i += 1;
151 }
152 count
153 }
154
155 pub fn writable_count(&self) -> usize {
157 let mut count = 0;
158 let mut i = 0;
159 while i < self.accounts.len() {
160 if self.accounts[i].writable {
161 count += 1;
162 }
163 i += 1;
164 }
165 count
166 }
167
168 pub fn find_account(&self, name: &str) -> Option<&ContextAccountDescriptor> {
170 let mut i = 0;
171 while i < self.accounts.len() {
172 if str_eq(self.accounts[i].name, name) {
173 return Some(&self.accounts[i]);
174 }
175 i += 1;
176 }
177 None
178 }
179}
180
181impl fmt::Display for ContextDescriptor {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 writeln!(f, "Context: {}", self.name)?;
184 for acct in self.accounts {
185 writeln!(f, " {}", acct)?;
186 }
187 if !self.policies.is_empty() {
188 write!(f, " Policies:")?;
189 for p in self.policies {
190 write!(f, " {}", p)?;
191 }
192 writeln!(f)?;
193 }
194 if self.receipts_expected {
195 writeln!(f, " Receipts: expected")?;
196 }
197 if !self.mutation_classes.is_empty() {
198 write!(f, " Mutations:")?;
199 for m in self.mutation_classes {
200 write!(f, " {}", m)?;
201 }
202 writeln!(f)?;
203 }
204 Ok(())
205 }
206}
207
208#[inline]
210fn str_eq(a: &str, b: &str) -> bool {
211 let a = a.as_bytes();
212 let b = b.as_bytes();
213 if a.len() != b.len() {
214 return false;
215 }
216 let mut i = 0;
217 while i < a.len() {
218 if a[i] != b[i] {
219 return false;
220 }
221 i += 1;
222 }
223 true
224}
225
226#[cfg(test)]
227mod tests {
228 extern crate alloc;
229 use super::*;
230 use alloc::format;
231
232 static TEST_ACCOUNTS: &[ContextAccountDescriptor] = &[
233 ContextAccountDescriptor {
234 name: "authority",
235 kind: "Signer",
236 writable: true,
237 signer: true,
238 layout_ref: "",
239 policy_ref: "",
240 seeds: &[],
241 optional: false,
242 lifecycle: AccountLifecycle::Existing,
243 payer: "",
244 init_space: 0,
245 has_one: &[],
246 expected_address: "",
247 expected_owner: "",
248 },
249 ContextAccountDescriptor {
250 name: "vault",
251 kind: "HopperAccount",
252 writable: true,
253 signer: false,
254 layout_ref: "VaultState",
255 policy_ref: "TREASURY_WRITE",
256 seeds: &["b\"vault\"", "authority"],
257 optional: false,
258 lifecycle: AccountLifecycle::Existing,
259 payer: "",
260 init_space: 0,
261 has_one: &["authority"],
262 expected_address: "",
263 expected_owner: "",
264 },
265 ContextAccountDescriptor {
266 name: "system_program",
267 kind: "ProgramRef",
268 writable: false,
269 signer: false,
270 layout_ref: "",
271 policy_ref: "",
272 seeds: &[],
273 optional: false,
274 lifecycle: AccountLifecycle::Existing,
275 payer: "",
276 init_space: 0,
277 has_one: &[],
278 expected_address: "",
279 expected_owner: "",
280 },
281 ];
282
283 static TEST_CTX: ContextDescriptor = ContextDescriptor {
284 name: "Deposit",
285 accounts: TEST_ACCOUNTS,
286 policies: &["TREASURY_WRITE"],
287 receipts_expected: true,
288 mutation_classes: &["Financial"],
289 };
290
291 #[test]
292 fn context_descriptor_counts() {
293 assert_eq!(TEST_CTX.account_count(), 3);
294 assert_eq!(TEST_CTX.signer_count(), 1);
295 assert_eq!(TEST_CTX.writable_count(), 2);
296 }
297
298 #[test]
299 fn context_descriptor_find() {
300 let found = TEST_CTX.find_account("vault");
301 assert!(found.is_some());
302 let vault = found.unwrap();
303 assert_eq!(vault.kind, "HopperAccount");
304 assert_eq!(vault.layout_ref, "VaultState");
305 assert_eq!(vault.seeds.len(), 2);
306 assert!(vault.writable);
307 assert!(!vault.signer);
308
309 assert!(TEST_CTX.find_account("nonexistent").is_none());
310 }
311
312 #[test]
313 fn context_descriptor_display() {
314 let s = format!("{}", TEST_CTX);
315 assert!(s.contains("Context: Deposit"));
316 assert!(s.contains("authority: Signer"));
317 assert!(s.contains("[mut]"));
318 assert!(s.contains("[signer]"));
319 assert!(s.contains("layout=VaultState"));
320 assert!(s.contains("policy=TREASURY_WRITE"));
321 assert!(s.contains("seeds=["));
322 assert!(s.contains("Policies: TREASURY_WRITE"));
323 assert!(s.contains("Mutations: Financial"));
324 }
325
326 #[test]
327 fn account_descriptor_display() {
328 let s = format!("{}", TEST_ACCOUNTS[2]);
329 assert!(s.contains("system_program: ProgramRef"));
330 assert!(!s.contains("[mut]"));
331 assert!(!s.contains("[signer]"));
332 }
333
334 #[test]
335 fn optional_account_display() {
336 let opt = ContextAccountDescriptor {
337 name: "extra",
338 kind: "Unchecked",
339 writable: false,
340 signer: false,
341 layout_ref: "",
342 policy_ref: "",
343 seeds: &[],
344 optional: true,
345 lifecycle: AccountLifecycle::Existing,
346 payer: "",
347 init_space: 0,
348 has_one: &[],
349 expected_address: "",
350 expected_owner: "",
351 };
352 let s = format!("{}", opt);
353 assert!(s.contains("[optional]"));
354 }
355
356 #[test]
357 fn lifecycle_as_str_roundtrips_all_variants() {
358 assert_eq!(AccountLifecycle::Existing.as_str(), "existing");
359 assert_eq!(AccountLifecycle::Init.as_str(), "init");
360 assert_eq!(AccountLifecycle::Realloc.as_str(), "realloc");
361 assert_eq!(AccountLifecycle::Close.as_str(), "close");
362 }
363
364 #[test]
365 fn init_account_descriptor_carries_lifecycle_metadata() {
366 let init_acc = ContextAccountDescriptor {
367 name: "position",
368 kind: "InitAccount",
369 writable: true,
370 signer: false,
371 layout_ref: "Position",
372 policy_ref: "",
373 seeds: &["b\"position\"", "authority.key()"],
374 optional: false,
375 lifecycle: AccountLifecycle::Init,
376 payer: "authority",
377 init_space: 128,
378 has_one: &[],
379 expected_address: "",
380 expected_owner: "",
381 };
382 assert_eq!(init_acc.lifecycle, AccountLifecycle::Init);
383 assert_eq!(init_acc.payer, "authority");
384 assert_eq!(init_acc.init_space, 128);
385 assert_eq!(init_acc.seeds.len(), 2);
386 }
387
388 #[test]
389 fn close_account_descriptor_roundtrips() {
390 let close_acc = ContextAccountDescriptor {
391 name: "vault",
392 kind: "HopperAccount",
393 writable: true,
394 signer: false,
395 layout_ref: "Vault",
396 policy_ref: "",
397 seeds: &[],
398 optional: false,
399 lifecycle: AccountLifecycle::Close,
400 payer: "",
401 init_space: 0,
402 has_one: &[],
403 expected_address: "",
404 expected_owner: "",
405 };
406 assert_eq!(close_acc.lifecycle.as_str(), "close");
407 }
408}