Skip to main content

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}