1extern crate alloc;
38
39use alloc::format;
40use alloc::string::{String, ToString};
41use core::fmt;
42
43use crate::{InstructionDescriptor, LayoutManifest, ProgramManifest};
44
45pub struct RsClientGen<'a>(pub &'a ProgramManifest);
52
53impl<'a> fmt::Display for RsClientGen<'a> {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let prog = self.0;
56
57 writeln!(
58 f,
59 "// Auto-generated by `hopper compile --emit rust-client`"
60 )?;
61 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
62 writeln!(f, "// DO NOT EDIT")?;
63 writeln!(f)?;
64 writeln!(
65 f,
66 "//! Off-chain Rust client for the `{}` Hopper program.",
67 prog.name
68 )?;
69 writeln!(f, "//!")?;
70 writeln!(
71 f,
72 "//! Every account decoder calls `assert_{{name}}_layout` first, which"
73 )?;
74 writeln!(
75 f,
76 "//! compares the on-chain `LAYOUT_ID` fingerprint to the compiled-in"
77 )?;
78 writeln!(
79 f,
80 "//! constant. A mismatch raises `LayoutMismatch` instead of reading"
81 )?;
82 writeln!(
83 f,
84 "//! stale bytes as if they were the new layout. this is the"
85 )?;
86 writeln!(
87 f,
88 "//! client-side complement to the runtime's header check."
89 )?;
90 writeln!(f)?;
91 writeln!(
92 f,
93 "use solana_program::instruction::{{AccountMeta, Instruction}};"
94 )?;
95 writeln!(f, "use solana_program::pubkey::Pubkey;")?;
96 writeln!(f)?;
97 writeln!(f, "/// Hopper account-header size (bytes).")?;
98 writeln!(f, "pub const HOPPER_HEADER_SIZE: usize = 16;")?;
99 writeln!(
100 f,
101 "/// Byte offset of the 8-byte `LAYOUT_ID` fingerprint in a Hopper header."
102 )?;
103 writeln!(f, "pub const LAYOUT_ID_OFFSET: usize = 4;")?;
104 writeln!(f, "/// Byte length of the fingerprint (always 8).")?;
105 writeln!(f, "pub const LAYOUT_ID_LENGTH: usize = 8;")?;
106 writeln!(f)?;
107 writeln!(f, "/// Shared error type for every decoder in this module.")?;
108 writeln!(f, "#[derive(Clone, Copy, Debug, PartialEq, Eq)]")?;
109 writeln!(f, "pub enum ClientError {{")?;
110 writeln!(
111 f,
112 " /// Buffer smaller than the 16-byte Hopper header + declared body."
113 )?;
114 writeln!(f, " BufferTooSmall {{ need: usize, got: usize }},")?;
115 writeln!(
116 f,
117 " /// Account header's `LAYOUT_ID` does not match the layout the client"
118 )?;
119 writeln!(
120 f,
121 " /// was generated against. The on-chain ABI drifted from this client."
122 )?;
123 writeln!(
124 f,
125 " LayoutMismatch {{ expected: [u8; 8], actual: [u8; 8] }},"
126 )?;
127 writeln!(f, "}}")?;
128 writeln!(f)?;
129 writeln!(f, "impl core::fmt::Display for ClientError {{")?;
130 writeln!(
131 f,
132 " fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {{"
133 )?;
134 writeln!(f, " match self {{")?;
135 writeln!(f, " Self::BufferTooSmall {{ need, got }} => {{")?;
136 writeln!(
137 f,
138 " write!(f, \"hopper client: account too small, need {{}} bytes got {{}}\", need, got)"
139 )?;
140 writeln!(f, " }}")?;
141 writeln!(
142 f,
143 " Self::LayoutMismatch {{ expected, actual }} => {{"
144 )?;
145 writeln!(
146 f,
147 " write!(f, \"hopper client: layout mismatch: expected {{:02x?}}, got {{:02x?}}\", expected, actual)"
148 )?;
149 writeln!(f, " }}")?;
150 writeln!(f, " }}")?;
151 writeln!(f, " }}")?;
152 writeln!(f, "}}")?;
153 writeln!(f)?;
154 writeln!(
155 f,
156 "/// Internal helper. read the 8-byte `LAYOUT_ID` from a Hopper header."
157 )?;
158 writeln!(f, "#[inline]")?;
159 writeln!(
160 f,
161 "fn read_layout_id(data: &[u8]) -> Result<[u8; 8], ClientError> {{"
162 )?;
163 writeln!(f, " if data.len() < HOPPER_HEADER_SIZE {{")?;
164 writeln!(
165 f,
166 " return Err(ClientError::BufferTooSmall {{ need: HOPPER_HEADER_SIZE, got: data.len() }});"
167 )?;
168 writeln!(f, " }}")?;
169 writeln!(f, " let mut id = [0u8; 8];")?;
170 writeln!(
171 f,
172 " id.copy_from_slice(&data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH]);"
173 )?;
174 writeln!(f, " Ok(id)")?;
175 writeln!(f, "}}")?;
176 writeln!(f)?;
177
178 for layout in prog.layouts.iter() {
179 write_layout_const_and_decoder(f, layout)?;
180 }
181
182 for ix in prog.instructions.iter() {
183 write_instruction_builder(f, ix, &prog.name)?;
184 }
185
186 Ok(())
187 }
188}
189
190fn write_layout_const_and_decoder(
191 f: &mut fmt::Formatter<'_>,
192 layout: &LayoutManifest,
193) -> fmt::Result {
194 let pascal = pascal_case(layout.name);
195 let snake = snake_case(layout.name);
196 let upper = upper_snake_case(layout.name);
197
198 writeln!(
199 f,
200 "// {} ({} bytes total, {} body bytes)",
201 pascal,
202 layout.total_size,
203 body_size(layout)
204 )?;
205 write!(f, "pub const {}_LAYOUT_ID: [u8; 8] = [", upper)?;
207 for (i, b) in layout.layout_id.iter().enumerate() {
208 if i > 0 {
209 write!(f, ", ")?;
210 }
211 write!(f, "0x{:02x}", b)?;
212 }
213 writeln!(f, "];")?;
214 writeln!(f, "pub const {}_DISC: u8 = {};", upper, layout.disc)?;
215 writeln!(f, "pub const {}_VERSION: u8 = {};", upper, layout.version)?;
216 writeln!(
217 f,
218 "pub const {}_TOTAL_SIZE: usize = {};",
219 upper, layout.total_size
220 )?;
221 writeln!(f)?;
222 for field in layout.fields.iter() {
223 writeln!(
224 f,
225 "pub const {}_{}_OFFSET: usize = {};",
226 upper,
227 upper_snake_case(field.name),
228 field.offset
229 )?;
230 writeln!(
231 f,
232 "pub const {}_{}_SIZE: usize = {};",
233 upper,
234 upper_snake_case(field.name),
235 field.size
236 )?;
237 }
238 writeln!(f)?;
239
240 writeln!(f, "/// Decoded `{}` account.", pascal)?;
242 writeln!(f, "#[derive(Clone, Debug)]")?;
243 writeln!(f, "pub struct {} {{", pascal)?;
244 for field in layout.fields.iter() {
245 writeln!(
246 f,
247 " pub {}: {},",
248 snake_case(field.name),
249 rust_field_type(field.canonical_type)
250 )?;
251 }
252 writeln!(f, "}}")?;
253 writeln!(f)?;
254
255 writeln!(
257 f,
258 "/// Refuse to decode if the header's `LAYOUT_ID` disagrees with the"
259 )?;
260 writeln!(
261 f,
262 "/// compiled-in `{}_LAYOUT_ID`. This is the client-side audit guard.",
263 upper
264 )?;
265 writeln!(
266 f,
267 "pub fn assert_{}_layout(data: &[u8]) -> Result<(), ClientError> {{",
268 snake
269 )?;
270 writeln!(f, " let actual = read_layout_id(data)?;")?;
271 writeln!(f, " if actual != {}_LAYOUT_ID {{", upper)?;
272 writeln!(
273 f,
274 " return Err(ClientError::LayoutMismatch {{ expected: {}_LAYOUT_ID, actual }});",
275 upper
276 )?;
277 writeln!(f, " }}")?;
278 writeln!(f, " Ok(())")?;
279 writeln!(f, "}}")?;
280 writeln!(f)?;
281
282 writeln!(
284 f,
285 "/// Decode a `{}` account buffer into the typed struct.",
286 pascal
287 )?;
288 writeln!(f, "///")?;
289 writeln!(
290 f,
291 "/// Performs `assert_{}_layout` first; on success, reads each field out",
292 snake
293 )?;
294 writeln!(f, "/// of the byte buffer at its declared offset.")?;
295 writeln!(
296 f,
297 "pub fn decode_{}(data: &[u8]) -> Result<{}, ClientError> {{",
298 snake, pascal
299 )?;
300 writeln!(f, " assert_{}_layout(data)?;", snake)?;
301 writeln!(f, " if data.len() < {}_TOTAL_SIZE {{", upper)?;
302 writeln!(
303 f,
304 " return Err(ClientError::BufferTooSmall {{ need: {}_TOTAL_SIZE, got: data.len() }});",
305 upper
306 )?;
307 writeln!(f, " }}")?;
308 for field in layout.fields.iter() {
309 writeln!(f, " let {} = {{", snake_case(field.name))?;
310 write_field_decode(
311 f,
312 field.canonical_type,
313 field.offset as usize,
314 field.size as usize,
315 )?;
316 writeln!(f, " }};")?;
317 }
318 writeln!(f, " Ok({} {{", pascal)?;
319 for field in layout.fields.iter() {
320 writeln!(f, " {},", snake_case(field.name))?;
321 }
322 writeln!(f, " }})")?;
323 writeln!(f, "}}")?;
324 writeln!(f)?;
325 Ok(())
326}
327
328fn write_field_decode(
329 f: &mut fmt::Formatter<'_>,
330 canonical: &str,
331 offset: usize,
332 size: usize,
333) -> fmt::Result {
334 let end = offset + size;
335 match canonical {
336 "u8" => writeln!(f, " data[{}]", offset),
337 "i8" => writeln!(f, " data[{}] as i8", offset),
338 "u16" => writeln!(
339 f,
340 " u16::from_le_bytes([data[{}], data[{}]])",
341 offset,
342 offset + 1
343 ),
344 "i16" => writeln!(
345 f,
346 " i16::from_le_bytes([data[{}], data[{}]])",
347 offset,
348 offset + 1
349 ),
350 "u32" => writeln!(
351 f,
352 " u32::from_le_bytes([data[{}], data[{}], data[{}], data[{}]])",
353 offset,
354 offset + 1,
355 offset + 2,
356 offset + 3
357 ),
358 "i32" => writeln!(
359 f,
360 " i32::from_le_bytes([data[{}], data[{}], data[{}], data[{}]])",
361 offset,
362 offset + 1,
363 offset + 2,
364 offset + 3
365 ),
366 "u64" | "WireU64" => {
367 writeln!(f, " let mut buf = [0u8; 8];")?;
368 writeln!(
369 f,
370 " buf.copy_from_slice(&data[{}..{}]);",
371 offset, end
372 )?;
373 writeln!(f, " u64::from_le_bytes(buf)")
374 }
375 "i64" | "WireI64" => {
376 writeln!(f, " let mut buf = [0u8; 8];")?;
377 writeln!(
378 f,
379 " buf.copy_from_slice(&data[{}..{}]);",
380 offset, end
381 )?;
382 writeln!(f, " i64::from_le_bytes(buf)")
383 }
384 "u128" => {
385 writeln!(f, " let mut buf = [0u8; 16];")?;
386 writeln!(
387 f,
388 " buf.copy_from_slice(&data[{}..{}]);",
389 offset, end
390 )?;
391 writeln!(f, " u128::from_le_bytes(buf)")
392 }
393 "bool" | "WireBool" => writeln!(f, " data[{}] != 0", offset),
394 "Pubkey" => {
395 writeln!(f, " let mut buf = [0u8; 32];")?;
396 writeln!(
397 f,
398 " buf.copy_from_slice(&data[{}..{}]);",
399 offset, end
400 )?;
401 writeln!(f, " Pubkey::new_from_array(buf)")
402 }
403 _ => {
404 writeln!(f, " let mut buf = [0u8; {}];", size)?;
407 writeln!(
408 f,
409 " buf.copy_from_slice(&data[{}..{}]);",
410 offset, end
411 )?;
412 writeln!(f, " buf")
413 }
414 }
415}
416
417fn write_instruction_builder(
418 f: &mut fmt::Formatter<'_>,
419 ix: &InstructionDescriptor,
420 _program: &str,
421) -> fmt::Result {
422 let pascal = pascal_case(ix.name);
423 let snake = snake_case(ix.name);
424 let upper = upper_snake_case(ix.name);
425
426 writeln!(f, "// {} instruction (discriminator = {})", pascal, ix.tag)?;
427 writeln!(f, "pub const {}_DISC: u8 = {};", upper, ix.tag)?;
428 writeln!(f)?;
429
430 if !ix.args.is_empty() {
431 writeln!(f, "/// Arguments for the `{}` instruction.", snake)?;
432 writeln!(f, "#[derive(Clone, Debug)]")?;
433 writeln!(f, "pub struct {}Args {{", pascal)?;
434 for arg in ix.args.iter() {
435 writeln!(
436 f,
437 " pub {}: {},",
438 snake_case(arg.name),
439 rust_field_type(arg.canonical_type)
440 )?;
441 }
442 writeln!(f, "}}")?;
443 writeln!(f)?;
444 }
445
446 writeln!(
447 f,
448 "/// Account keys for the `{}` instruction, in the order Hopper expects.",
449 snake
450 )?;
451 writeln!(f, "#[derive(Clone, Debug)]")?;
452 writeln!(f, "pub struct {}Accounts {{", pascal)?;
453 for acc in ix.accounts.iter() {
454 writeln!(f, " pub {}: Pubkey,", snake_case(acc.name))?;
455 }
456 writeln!(f, "}}")?;
457 writeln!(f)?;
458
459 writeln!(f, "/// Build a `{}` transaction instruction.", snake)?;
461 writeln!(f, "///")?;
462 writeln!(
463 f,
464 "/// The returned `Instruction` carries the exact `AccountMeta` order"
465 )?;
466 writeln!(
467 f,
468 "/// and discriminator byte Hopper's runtime dispatcher expects."
469 )?;
470 writeln!(f, "pub fn {}_ix(", snake)?;
471 writeln!(f, " program_id: &Pubkey,")?;
472 writeln!(f, " accounts: &{}Accounts,", pascal)?;
473 if !ix.args.is_empty() {
474 writeln!(f, " args: &{}Args,", pascal)?;
475 }
476 writeln!(f, ") -> Instruction {{")?;
477 let arg_bytes: usize = ix.args.iter().map(|a| a.size as usize).sum();
479 writeln!(
480 f,
481 " let mut data = Vec::with_capacity(1 + {});",
482 arg_bytes
483 )?;
484 writeln!(f, " data.push({}_DISC);", upper)?;
485 for arg in ix.args.iter() {
486 write_arg_encode(f, arg.canonical_type, &snake_case(arg.name))?;
487 }
488 writeln!(f, " let account_metas = vec![")?;
489 for acc in ix.accounts.iter() {
490 let ctor = match (acc.writable, acc.signer) {
491 (true, true) => "new",
492 (true, false) => "new",
493 (false, true) => "new_readonly",
494 (false, false) => "new_readonly",
495 };
496 let signer_bool = if acc.signer { "true" } else { "false" };
497 writeln!(
498 f,
499 " AccountMeta::{}(accounts.{}, {}),",
500 if acc.writable { "new" } else { "new_readonly" },
501 snake_case(acc.name),
502 signer_bool
503 )?;
504 let _ = ctor;
505 }
506 writeln!(f, " ];")?;
507 writeln!(f, " Instruction {{")?;
508 writeln!(f, " program_id: *program_id,")?;
509 writeln!(f, " accounts: account_metas,")?;
510 writeln!(f, " data,")?;
511 writeln!(f, " }}")?;
512 writeln!(f, "}}")?;
513 writeln!(f)?;
514 Ok(())
515}
516
517fn write_arg_encode(f: &mut fmt::Formatter<'_>, canonical: &str, name: &str) -> fmt::Result {
518 match canonical {
519 "u8" => writeln!(f, " data.push(args.{});", name),
520 "i8" => writeln!(f, " data.push(args.{} as u8);", name),
521 "u16" | "i16" | "u32" | "i32" | "u64" | "i64" | "u128" | "i128" | "WireU64" | "WireI64" => {
522 writeln!(
523 f,
524 " data.extend_from_slice(&args.{}.to_le_bytes());",
525 name
526 )
527 }
528 "bool" | "WireBool" => {
529 writeln!(f, " data.push(if args.{} {{ 1 }} else {{ 0 }});", name)
530 }
531 "Pubkey" => writeln!(f, " data.extend_from_slice(args.{}.as_ref());", name),
532 _ => {
533 writeln!(f, " data.extend_from_slice(args.{}.as_ref());", name)
535 }
536 }
537}
538
539fn rust_field_type(canonical: &str) -> String {
540 match canonical {
541 "u8" => "u8".into(),
542 "i8" => "i8".into(),
543 "u16" => "u16".into(),
544 "i16" => "i16".into(),
545 "u32" => "u32".into(),
546 "i32" => "i32".into(),
547 "u64" | "WireU64" => "u64".into(),
548 "i64" | "WireI64" => "i64".into(),
549 "u128" => "u128".into(),
550 "i128" => "i128".into(),
551 "bool" | "WireBool" => "bool".into(),
552 "Pubkey" => "Pubkey".into(),
553 s if s.starts_with("[u8;") => s.to_string(),
554 _ => {
555 format!("[u8; /* {} */ 0]", canonical)
561 }
562 }
563}
564
565fn pascal_case(s: &str) -> String {
566 let mut out = String::new();
567 let mut upper_next = true;
568 for c in s.chars() {
569 if c == '_' || c == '-' || c == ' ' {
570 upper_next = true;
571 continue;
572 }
573 if upper_next {
574 out.extend(c.to_uppercase());
575 upper_next = false;
576 } else {
577 out.push(c);
578 }
579 }
580 out
581}
582
583fn snake_case(s: &str) -> String {
584 let mut out = String::new();
585 let mut first = true;
586 for c in s.chars() {
587 if c == '_' || c == ' ' || c == '-' {
588 out.push('_');
589 continue;
590 }
591 if c.is_uppercase() {
592 if !first && !out.ends_with('_') {
593 out.push('_');
594 }
595 out.extend(c.to_lowercase());
596 } else {
597 out.push(c);
598 }
599 first = false;
600 }
601 out
602}
603
604fn upper_snake_case(s: &str) -> String {
605 snake_case(s).to_uppercase()
606}
607
608fn body_size(layout: &LayoutManifest) -> usize {
609 layout.total_size.saturating_sub(16)
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::{
616 AccountEntry, ArgDescriptor, EventDescriptor, FieldDescriptor, FieldIntent, LayoutManifest,
617 PolicyDescriptor,
618 };
619
620 fn test_manifest() -> ProgramManifest {
621 static VAULT_FIELDS: &[FieldDescriptor] = &[
622 FieldDescriptor {
623 name: "authority",
624 canonical_type: "Pubkey",
625 size: 32,
626 offset: 16,
627 intent: FieldIntent::Authority,
628 },
629 FieldDescriptor {
630 name: "balance",
631 canonical_type: "u64",
632 size: 8,
633 offset: 48,
634 intent: FieldIntent::Balance,
635 },
636 ];
637 static VAULT_LAYOUT: LayoutManifest = LayoutManifest {
638 name: "Vault",
639 version: 1,
640 disc: 42,
641 layout_id: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
642 total_size: 56,
643 field_count: 2,
644 fields: VAULT_FIELDS,
645 };
646 static LAYOUTS: &[LayoutManifest] = &[VAULT_LAYOUT];
647 static DEPOSIT_ARGS: &[ArgDescriptor] = &[ArgDescriptor {
648 name: "amount",
649 canonical_type: "u64",
650 size: 8,
651 }];
652 static DEPOSIT_ACCTS: &[AccountEntry] = &[
653 AccountEntry {
654 name: "vault",
655 writable: true,
656 signer: false,
657 layout_ref: "Vault",
658 },
659 AccountEntry {
660 name: "authority",
661 writable: false,
662 signer: true,
663 layout_ref: "",
664 },
665 ];
666 static DEPOSIT: InstructionDescriptor = InstructionDescriptor {
667 name: "deposit",
668 tag: 0,
669 args: DEPOSIT_ARGS,
670 accounts: DEPOSIT_ACCTS,
671 capabilities: &[],
672 policy_pack: "",
673 receipt_expected: false,
674 };
675 static INSTRUCTIONS: &[InstructionDescriptor] = &[DEPOSIT];
676 static EVENTS: &[EventDescriptor] = &[];
677 static POLICIES: &[PolicyDescriptor] = &[];
678
679 ProgramManifest {
680 name: "vault_program",
681 version: "0.1.0",
682 description: "test",
683 layouts: LAYOUTS,
684 layout_metadata: &[],
685 instructions: INSTRUCTIONS,
686 events: EVENTS,
687 policies: POLICIES,
688 compatibility_pairs: &[],
689 tooling_hints: &[],
690 contexts: &[],
691 }
692 }
693
694 #[test]
695 fn rs_client_emits_layout_id_constant_with_bytes() {
696 let m = test_manifest();
697 let out = RsClientGen(&m).to_string();
698 assert!(out.contains("pub const VAULT_LAYOUT_ID: [u8; 8] = ["));
699 assert!(out.contains("0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08"));
700 }
701
702 #[test]
703 fn rs_client_emits_typed_account_struct() {
704 let m = test_manifest();
705 let out = RsClientGen(&m).to_string();
706 assert!(out.contains("pub struct Vault {"));
707 assert!(out.contains("pub authority: Pubkey,"));
708 assert!(out.contains("pub balance: u64,"));
709 }
710
711 #[test]
712 fn rs_client_emits_per_field_offset_and_size_consts() {
713 let m = test_manifest();
714 let out = RsClientGen(&m).to_string();
715 assert!(out.contains("pub const VAULT_AUTHORITY_OFFSET: usize = 16;"));
716 assert!(out.contains("pub const VAULT_AUTHORITY_SIZE: usize = 32;"));
717 assert!(out.contains("pub const VAULT_BALANCE_OFFSET: usize = 48;"));
718 assert!(out.contains("pub const VAULT_BALANCE_SIZE: usize = 8;"));
719 }
720
721 #[test]
722 fn rs_client_emits_layout_assertion_helper() {
723 let m = test_manifest();
724 let out = RsClientGen(&m).to_string();
725 assert!(out.contains("pub fn assert_vault_layout(data: &[u8]) -> Result<(), ClientError>"));
726 assert!(out.contains("if actual != VAULT_LAYOUT_ID"));
727 assert!(out.contains("ClientError::LayoutMismatch"));
728 }
729
730 #[test]
731 fn rs_client_decode_calls_layout_assertion_first() {
732 let m = test_manifest();
733 let out = RsClientGen(&m).to_string();
734 let assertion = out.find("pub fn decode_vault").unwrap();
737 let body = &out[assertion..];
738 let assert_pos = body.find("assert_vault_layout(data)?").unwrap();
739 let field_read_pos = body.find("authority").unwrap();
740 assert!(
741 assert_pos < field_read_pos,
742 "layout assert must precede field reads"
743 );
744 }
745
746 #[test]
747 fn rs_client_emits_instruction_builder_with_disc_prefix() {
748 let m = test_manifest();
749 let out = RsClientGen(&m).to_string();
750 assert!(out.contains("pub fn deposit_ix("));
751 assert!(out.contains("data.push(DEPOSIT_DISC);"));
752 assert!(out.contains("program_id: *program_id,"));
753 assert!(out.contains("AccountMeta::new"));
754 }
755
756 #[test]
757 fn rs_client_rejects_truncated_header() {
758 let m = test_manifest();
759 let out = RsClientGen(&m).to_string();
760 assert!(out.contains("if data.len() < HOPPER_HEADER_SIZE"));
763 assert!(out.contains("ClientError::BufferTooSmall"));
764 }
765
766 #[test]
767 fn rs_client_emits_args_struct_and_accounts_struct() {
768 let m = test_manifest();
769 let out = RsClientGen(&m).to_string();
770 assert!(out.contains("pub struct DepositArgs {"));
771 assert!(out.contains("pub amount: u64,"));
772 assert!(out.contains("pub struct DepositAccounts {"));
773 assert!(out.contains("pub vault: Pubkey,"));
774 assert!(out.contains("pub authority: Pubkey,"));
775 }
776
777 #[test]
778 fn rs_client_uses_solana_sdk_types() {
779 let m = test_manifest();
780 let out = RsClientGen(&m).to_string();
781 assert!(out.contains("use solana_program::instruction::{AccountMeta, Instruction};"));
782 assert!(out.contains("use solana_program::pubkey::Pubkey;"));
783 }
784}