hopper_runtime/crank.rs
1//! Crank marker type emitted by the `#[hopper::crank]` attribute.
2//!
3//! A `CrankMarker` is a compile-time const that sits next to the
4//! decorated handler. It records the handler's name and an optional
5//! set of seed hints used by `hopper manager crank run` to resolve
6//! PDA accounts autonomously.
7//!
8//! Indexers and off-chain tools walk the program's emitted `const`
9//! items, collect every `CrankMarker`, and surface the resulting set
10//! as "this program's crankable instruction list". Having the data
11//! stamped in the binary (rather than only in the off-chain
12//! manifest) keeps the ground truth on chain.
13
14/// Compile-time descriptor for a crank handler.
15///
16/// `seed_hints` is a slice of `(account_field_name, seed_bytes_list)`
17/// tuples. Each entry says: "the account field named X on this
18/// crank's context can be derived from these seeds under the
19/// program's id". The crank runner calls `find_program_address`
20/// with the seeds and fills the account slot automatically.
21#[derive(Copy, Clone, Debug)]
22pub struct CrankMarker {
23 /// The handler function name, same as `stringify!(fn_name)`.
24 pub handler_name: &'static str,
25 /// Per-account seed hints. Each tuple is
26 /// `(field_name, &[seed_0, seed_1, ...])`.
27 pub seed_hints: &'static [(&'static str, &'static [&'static [u8]])],
28}
29
30impl CrankMarker {
31 /// Number of declared seed hints.
32 #[inline(always)]
33 pub const fn seed_count(&self) -> usize {
34 self.seed_hints.len()
35 }
36
37 /// Lookup a single field's seed list by name. Returns `None`
38 /// when the field has no declared hint (the runner then falls
39 /// back to `--account` supplied at the CLI).
40 #[inline]
41 pub fn seeds_for(&self, field: &str) -> Option<&'static [&'static [u8]]> {
42 let mut i = 0;
43 while i < self.seed_hints.len() {
44 if str_eq(self.seed_hints[i].0, field) {
45 return Some(self.seed_hints[i].1);
46 }
47 i += 1;
48 }
49 None
50 }
51}
52
53/// Const-context `&str` equality. The stdlib's `str::eq` is not
54/// const-callable on stable, so we inline the byte-wise compare.
55#[inline]
56const fn str_eq(a: &str, b: &str) -> bool {
57 let ab = a.as_bytes();
58 let bb = b.as_bytes();
59 if ab.len() != bb.len() {
60 return false;
61 }
62 let mut i = 0;
63 while i < ab.len() {
64 if ab[i] != bb[i] {
65 return false;
66 }
67 i += 1;
68 }
69 true
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 const EMPTY: CrankMarker = CrankMarker {
77 handler_name: "settle",
78 seed_hints: &[],
79 };
80
81 const WITH_HINTS: CrankMarker = CrankMarker {
82 handler_name: "rotate",
83 seed_hints: &[
84 ("vault", &[b"vault".as_slice()] as &[&[u8]]),
85 (
86 "fee_account",
87 &[b"fee".as_slice(), b"account".as_slice()] as &[&[u8]],
88 ),
89 ],
90 };
91
92 #[test]
93 fn empty_marker_has_zero_hints() {
94 assert_eq!(EMPTY.seed_count(), 0);
95 assert!(EMPTY.seeds_for("anything").is_none());
96 }
97
98 #[test]
99 fn lookup_resolves_named_field() {
100 let s = WITH_HINTS.seeds_for("vault").unwrap();
101 assert_eq!(s.len(), 1);
102 assert_eq!(s[0], b"vault");
103
104 let s = WITH_HINTS.seeds_for("fee_account").unwrap();
105 assert_eq!(s.len(), 2);
106 assert_eq!(s[0], b"fee");
107 assert_eq!(s[1], b"account");
108 }
109
110 #[test]
111 fn lookup_miss_returns_none() {
112 assert!(WITH_HINTS.seeds_for("not_declared").is_none());
113 }
114}