Skip to main content

hopper_core/accounts/
explain.rs

1//! Structured explain output for contexts and accounts.
2
3/// Schema metadata for a single account field in a context.
4#[derive(Clone, Copy)]
5pub struct AccountFieldSchema {
6    /// Field name in the struct.
7    pub name: &'static str,
8    /// Account kind (e.g. "HopperAccount", "Signer", "ProgramRef").
9    pub kind: &'static str,
10    /// Whether the account is writable.
11    pub mutable: bool,
12    /// Whether the account must be a signer.
13    pub signer: bool,
14    /// Layout name bound via `layout = T`, if any.
15    pub layout: Option<&'static str>,
16    /// Policy pack name bound via `policy = P`, if any.
17    pub policy: Option<&'static str>,
18    /// PDA seed expressions (as string representations).
19    pub seeds: &'static [&'static str],
20    /// Whether the account is optional.
21    pub optional: bool,
22}
23
24/// Schema metadata for an entire instruction context.
25#[derive(Clone, Copy)]
26pub struct ContextSchema {
27    /// Context struct name (e.g. "Deposit").
28    pub name: &'static str,
29    /// Per-account field schemas.
30    pub fields: &'static [AccountFieldSchema],
31    /// Policy pack names used by this context.
32    pub policy_names: &'static [&'static str],
33    /// Whether receipts are expected from this instruction.
34    pub receipts_expected: bool,
35    /// Mutation class names (e.g. "Financial", "InPlace").
36    pub mutation_classes: &'static [&'static str],
37}
38
39/// Human-readable explanation of a context.
40pub struct ContextExplain {
41    /// Context name.
42    pub context_name: &'static str,
43    /// Per-account field schemas (full metadata for each account).
44    pub fields: &'static [AccountFieldSchema],
45    /// Policy name list.
46    pub policies: &'static [&'static str],
47    /// Whether receipts are expected.
48    pub receipts_expected: bool,
49    /// Mutation class name list.
50    pub mutation_classes: &'static [&'static str],
51}
52
53impl ContextExplain {
54    /// Build an explain from a schema, or return a blank if no schema exists.
55    pub fn from_schema(schema: Option<&'static ContextSchema>) -> Self {
56        match schema {
57            Some(s) => Self {
58                context_name: s.name,
59                fields: s.fields,
60                policies: s.policy_names,
61                receipts_expected: s.receipts_expected,
62                mutation_classes: s.mutation_classes,
63            },
64            None => Self {
65                context_name: "(unknown)",
66                fields: &[],
67                policies: &[],
68                receipts_expected: false,
69                mutation_classes: &[],
70            },
71        }
72    }
73
74    /// Number of accounts in this context.
75    pub fn account_count(&self) -> usize {
76        self.fields.len()
77    }
78
79    /// Number of signer accounts.
80    pub fn signer_count(&self) -> usize {
81        self.fields.iter().filter(|f| f.signer).count()
82    }
83
84    /// Number of writable accounts.
85    pub fn writable_count(&self) -> usize {
86        self.fields.iter().filter(|f| f.mutable).count()
87    }
88}
89
90/// Human-readable explanation of a single account.
91pub struct AccountExplain {
92    /// Field name.
93    pub name: &'static str,
94    /// Account kind.
95    pub kind: &'static str,
96    /// Layout name, if bound.
97    pub layout: Option<&'static str>,
98    /// Policy name, if bound.
99    pub policy: Option<&'static str>,
100    /// Whether the account is writable.
101    pub mutable: bool,
102    /// Whether the account is a signer.
103    pub signer: bool,
104    /// Whether the account is optional.
105    pub optional: bool,
106}
107
108#[cfg(test)]
109mod tests {
110    extern crate alloc;
111    use super::*;
112
113    static TEST_FIELDS: &[AccountFieldSchema] = &[
114        AccountFieldSchema {
115            name: "authority",
116            kind: "Signer",
117            mutable: true,
118            signer: true,
119            layout: None,
120            policy: None,
121            seeds: &[],
122            optional: false,
123        },
124        AccountFieldSchema {
125            name: "vault",
126            kind: "HopperAccount",
127            mutable: true,
128            signer: false,
129            layout: Some("VaultState"),
130            policy: Some("TREASURY_WRITE"),
131            seeds: &["b\"vault\"", "authority"],
132            optional: false,
133        },
134        AccountFieldSchema {
135            name: "system_program",
136            kind: "ProgramRef",
137            mutable: false,
138            signer: false,
139            layout: None,
140            policy: None,
141            seeds: &[],
142            optional: false,
143        },
144    ];
145
146    static TEST_SCHEMA: ContextSchema = ContextSchema {
147        name: "Deposit",
148        fields: TEST_FIELDS,
149        policy_names: &["TREASURY_WRITE"],
150        receipts_expected: true,
151        mutation_classes: &["Financial"],
152    };
153
154    #[test]
155    fn context_explain_from_schema() {
156        let explain = ContextExplain::from_schema(Some(&TEST_SCHEMA));
157        assert_eq!(explain.context_name, "Deposit");
158        assert_eq!(explain.fields.len(), 3);
159        assert_eq!(explain.fields[0].name, "authority");
160        assert_eq!(explain.fields[1].name, "vault");
161        assert_eq!(explain.policies.len(), 1);
162        assert_eq!(explain.policies[0], "TREASURY_WRITE");
163        assert!(explain.receipts_expected);
164        assert_eq!(explain.mutation_classes.len(), 1);
165        assert_eq!(explain.mutation_classes[0], "Financial");
166        assert_eq!(explain.account_count(), 3);
167        assert_eq!(explain.signer_count(), 1);
168        assert_eq!(explain.writable_count(), 2);
169    }
170
171    #[test]
172    fn context_explain_from_none() {
173        let explain = ContextExplain::from_schema(None);
174        assert_eq!(explain.context_name, "(unknown)");
175        assert!(!explain.receipts_expected);
176        assert!(explain.policies.is_empty());
177        assert_eq!(explain.account_count(), 0);
178        assert_eq!(explain.signer_count(), 0);
179        assert_eq!(explain.writable_count(), 0);
180    }
181
182    #[test]
183    fn account_field_schema_fields() {
184        assert_eq!(TEST_FIELDS[0].name, "authority");
185        assert!(TEST_FIELDS[0].signer);
186        assert!(TEST_FIELDS[0].mutable);
187        assert!(TEST_FIELDS[0].layout.is_none());
188
189        assert_eq!(TEST_FIELDS[1].name, "vault");
190        assert!(!TEST_FIELDS[1].signer);
191        assert!(TEST_FIELDS[1].mutable);
192        assert_eq!(TEST_FIELDS[1].layout, Some("VaultState"));
193        assert_eq!(TEST_FIELDS[1].seeds.len(), 2);
194    }
195
196    #[test]
197    fn context_schema_field_count() {
198        assert_eq!(TEST_SCHEMA.fields.len(), 3);
199        assert_eq!(TEST_SCHEMA.name, "Deposit");
200        assert_eq!(TEST_SCHEMA.policy_names.len(), 1);
201        assert_eq!(TEST_SCHEMA.mutation_classes.len(), 1);
202    }
203}