1#![cfg(feature = "builder")]
23
24extern crate alloc;
25
26use alloc::vec::Vec;
27use hopper_schema::{InstructionDescriptor, ProgramManifest};
28
29#[derive(Debug, Clone, Copy)]
33pub struct AccountMeta {
34 pub pubkey: [u8; 32],
36 pub is_signer: bool,
38 pub is_writable: bool,
40}
41
42#[derive(Debug, Clone)]
44pub struct BuiltInstruction {
45 pub program_id: [u8; 32],
47 pub data: Vec<u8>,
49 pub accounts: Vec<AccountMeta>,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum BuildError {
56 UnknownInstruction,
58 ArgCountMismatch {
60 expected: usize,
62 got: usize,
64 },
65 ArgSizeMismatch {
67 index: usize,
69 expected: u16,
71 got: usize,
73 },
74 AccountCountMismatch {
77 expected: usize,
79 got: usize,
81 },
82}
83
84#[derive(Debug)]
86pub struct InstructionBuilder<'a> {
87 program_id: [u8; 32],
88 ix: &'a InstructionDescriptor,
89 args: Vec<&'a [u8]>,
90 accounts: Vec<[u8; 32]>,
91}
92
93impl<'a> InstructionBuilder<'a> {
94 pub fn new(
97 manifest: &'a ProgramManifest,
98 program_id: [u8; 32],
99 ix_name: &str,
100 ) -> Result<Self, BuildError> {
101 let ix = find_instruction(manifest, ix_name).ok_or(BuildError::UnknownInstruction)?;
102 Ok(Self {
103 program_id,
104 ix,
105 args: Vec::with_capacity(ix.args.len()),
106 accounts: Vec::with_capacity(ix.accounts.len()),
107 })
108 }
109
110 pub fn arg(mut self, bytes: &'a [u8]) -> Self {
112 self.args.push(bytes);
113 self
114 }
115
116 pub fn account(mut self, pubkey: [u8; 32]) -> Self {
118 self.accounts.push(pubkey);
119 self
120 }
121
122 pub fn build(self) -> Result<BuiltInstruction, BuildError> {
124 if self.args.len() != self.ix.args.len() {
125 return Err(BuildError::ArgCountMismatch {
126 expected: self.ix.args.len(),
127 got: self.args.len(),
128 });
129 }
130 if self.accounts.len() != self.ix.accounts.len() {
131 return Err(BuildError::AccountCountMismatch {
132 expected: self.ix.accounts.len(),
133 got: self.accounts.len(),
134 });
135 }
136
137 let mut data_len: usize = 1;
139 let mut i = 0;
140 while i < self.args.len() {
141 let want = self.ix.args[i].size as usize;
142 if self.args[i].len() != want {
143 return Err(BuildError::ArgSizeMismatch {
144 index: i,
145 expected: self.ix.args[i].size,
146 got: self.args[i].len(),
147 });
148 }
149 data_len += want;
150 i += 1;
151 }
152
153 let mut data = Vec::with_capacity(data_len);
154 data.push(self.ix.tag);
155 for a in &self.args {
156 data.extend_from_slice(a);
157 }
158
159 let mut metas = Vec::with_capacity(self.accounts.len());
160 let mut i = 0;
161 while i < self.accounts.len() {
162 let entry = &self.ix.accounts[i];
163 metas.push(AccountMeta {
164 pubkey: self.accounts[i],
165 is_signer: entry.signer,
166 is_writable: entry.writable,
167 });
168 i += 1;
169 }
170
171 Ok(BuiltInstruction {
172 program_id: self.program_id,
173 data,
174 accounts: metas,
175 })
176 }
177}
178
179fn find_instruction<'a>(m: &'a ProgramManifest, name: &str) -> Option<&'a InstructionDescriptor> {
180 let mut i = 0;
181 while i < m.instructions.len() {
182 if m.instructions[i].name == name {
183 return Some(&m.instructions[i]);
184 }
185 i += 1;
186 }
187 None
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use hopper_schema::{AccountEntry, ArgDescriptor, InstructionDescriptor, ProgramManifest};
194
195 fn sample_manifest() -> ProgramManifest {
196 static ARGS: [ArgDescriptor; 2] = [
197 ArgDescriptor {
198 name: "amount",
199 canonical_type: "u64",
200 size: 8,
201 },
202 ArgDescriptor {
203 name: "bump",
204 canonical_type: "u8",
205 size: 1,
206 },
207 ];
208 static ACCTS: [AccountEntry; 2] = [
209 AccountEntry {
210 name: "vault",
211 writable: true,
212 signer: false,
213 layout_ref: "Vault",
214 },
215 AccountEntry {
216 name: "authority",
217 writable: false,
218 signer: true,
219 layout_ref: "",
220 },
221 ];
222 static IX: [InstructionDescriptor; 1] = [InstructionDescriptor {
223 name: "deposit",
224 tag: 3,
225 args: &ARGS,
226 accounts: &ACCTS,
227 capabilities: &[],
228 policy_pack: "",
229 receipt_expected: true,
230 }];
231 ProgramManifest {
232 name: "test",
233 version: "0",
234 description: "",
235 layouts: &[],
236 layout_metadata: &[],
237 instructions: &IX,
238 events: &[],
239 policies: &[],
240 compatibility_pairs: &[],
241 tooling_hints: &[],
242 contexts: &[],
243 }
244 }
245
246 #[test]
247 fn builds_a_valid_ix() {
248 let m = sample_manifest();
249 let amount = 42u64.to_le_bytes();
250 let bump = [254u8];
251 let vault = [1u8; 32];
252 let auth = [2u8; 32];
253 let ix = InstructionBuilder::new(&m, [9u8; 32], "deposit")
254 .unwrap()
255 .arg(&amount)
256 .arg(&bump)
257 .account(vault)
258 .account(auth)
259 .build()
260 .unwrap();
261 assert_eq!(ix.program_id, [9u8; 32]);
262 assert_eq!(ix.data.len(), 1 + 8 + 1);
263 assert_eq!(ix.data[0], 3);
264 assert_eq!(ix.accounts.len(), 2);
265 assert!(ix.accounts[0].is_writable);
266 assert!(ix.accounts[1].is_signer);
267 }
268
269 #[test]
270 fn rejects_mismatched_arg_size() {
271 let m = sample_manifest();
272 let amount_wrong = [1u8; 4];
273 let bump = [0u8; 1];
274 let err = InstructionBuilder::new(&m, [0u8; 32], "deposit")
275 .unwrap()
276 .arg(&amount_wrong)
277 .arg(&bump)
278 .account([0u8; 32])
279 .account([0u8; 32])
280 .build()
281 .unwrap_err();
282 assert!(matches!(
283 err,
284 BuildError::ArgSizeMismatch {
285 index: 0,
286 expected: 8,
287 got: 4
288 }
289 ));
290 }
291
292 #[test]
293 fn rejects_unknown_instruction() {
294 let m = sample_manifest();
295 let err = InstructionBuilder::new(&m, [0u8; 32], "withdraw").unwrap_err();
296 assert_eq!(err, BuildError::UnknownInstruction);
297 }
298}