1use core::fmt;
23
24extern crate alloc;
25
26use crate::{InstructionDescriptor, ProgramManifest};
27
28fn ts_type(canonical: &str) -> &str {
34 match canonical {
35 "u8" | "u16" | "u32" | "i8" | "i16" | "i32" => "number",
36 "u64" | "u128" | "i64" | "i128" => "bigint",
37 "bool" => "boolean",
38 "Pubkey" => "PublicKey",
39 _ => {
40 if canonical.starts_with("[u8;") {
41 "Uint8Array"
42 } else {
43 "Uint8Array" }
45 }
46 }
47}
48
49fn write_pascal(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
51 let mut capitalize_next = true;
52 for c in name.chars() {
53 if c == '_' || c == '-' {
54 capitalize_next = true;
55 } else if capitalize_next {
56 for uc in c.to_uppercase() {
57 write!(f, "{}", uc)?;
58 }
59 capitalize_next = false;
60 } else {
61 write!(f, "{}", c)?;
62 }
63 }
64 Ok(())
65}
66
67fn write_camel(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
69 let mut capitalize_next = false;
70 let mut first = true;
71 for c in name.chars() {
72 if c == '_' || c == '-' {
73 capitalize_next = true;
74 } else if capitalize_next {
75 for uc in c.to_uppercase() {
76 write!(f, "{}", uc)?;
77 }
78 capitalize_next = false;
79 } else if first {
80 for lc in c.to_lowercase() {
81 write!(f, "{}", lc)?;
82 }
83 first = false;
84 } else {
85 write!(f, "{}", c)?;
86 }
87 }
88 Ok(())
89}
90
91pub struct TsAccounts<'a>(pub &'a ProgramManifest);
97
98impl<'a> fmt::Display for TsAccounts<'a> {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 let prog = self.0;
101
102 writeln!(f, "// Auto-generated by hopper client gen --ts")?;
103 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
104 writeln!(f, "// DO NOT EDIT")?;
105 writeln!(f)?;
106 writeln!(f, "import {{ PublicKey }} from \"@solana/web3.js\";")?;
107 writeln!(f)?;
108
109 writeln!(f, "/** Hopper account header size in bytes. */")?;
111 writeln!(f, "export const HEADER_SIZE = 16;")?;
112 writeln!(f)?;
113 writeln!(
118 f,
119 "/** Byte offset of the 8-byte layout fingerprint in a Hopper account header. */"
120 )?;
121 writeln!(f, "export const LAYOUT_ID_OFFSET = 4;")?;
122 writeln!(f, "/** Byte length of the layout fingerprint. */")?;
123 writeln!(f, "export const LAYOUT_ID_LENGTH = 8;")?;
124 writeln!(f)?;
125 writeln!(f, "/**")?;
128 writeln!(
129 f,
130 " * Raise if `data` is not a Hopper account encoding the expected layout."
131 )?;
132 writeln!(f, " *")?;
133 writeln!(
134 f,
135 " * Reads the 8-byte LAYOUT_ID fingerprint from the 16-byte Hopper header"
136 )?;
137 writeln!(
138 f,
139 " * (bytes 4..12) and compares it against `expectedHex` (16 lowercase hex chars)."
140 )?;
141 writeln!(
142 f,
143 " * This is the client-side complement to the runtime check `load::<T>()` runs"
144 )?;
145 writeln!(
146 f,
147 " * before handing out a typed Ref. Mismatch means the on-chain program was"
148 )?;
149 writeln!(
150 f,
151 " * upgraded with a different ABI than the client was generated against."
152 )?;
153 writeln!(f, " */")?;
154 writeln!(
155 f,
156 "export function assertLayoutId(data: Uint8Array, expectedHex: string): void {{"
157 )?;
158 writeln!(f, " if (data.length < HEADER_SIZE) {{")?;
159 writeln!(
160 f,
161 " throw new Error(`Hopper account too short: ${{data.length}} < ${{HEADER_SIZE}}`);"
162 )?;
163 writeln!(f, " }}")?;
164 writeln!(f, " let actualHex = \"\";")?;
165 writeln!(f, " for (let i = 0; i < LAYOUT_ID_LENGTH; i++) {{")?;
166 writeln!(
167 f,
168 " actualHex += data[LAYOUT_ID_OFFSET + i].toString(16).padStart(2, \"0\");"
169 )?;
170 writeln!(f, " }}")?;
171 writeln!(f, " if (actualHex !== expectedHex.toLowerCase()) {{")?;
172 writeln!(f, " throw new Error(")?;
173 writeln!(f, " `Hopper layout mismatch: account header reports ${{actualHex}}, expected ${{expectedHex}}`,")?;
174 writeln!(f, " );")?;
175 writeln!(f, " }}")?;
176 writeln!(f, "}}")?;
177 writeln!(f)?;
178
179 for layout in prog.layouts.iter() {
180 write!(f, "export interface ")?;
182 write_pascal(f, layout.name)?;
183 writeln!(f, " {{")?;
184 for field in layout.fields.iter() {
185 write!(f, " ")?;
186 write_camel(f, field.name)?;
187 writeln!(f, ": {};", ts_type(field.canonical_type))?;
188 }
189 writeln!(f, "}}")?;
190 writeln!(f)?;
191
192 write!(f, "export const ")?;
196 write_upper_snake(f, layout.name)?;
197 write!(f, "_LAYOUT_ID = \"")?;
198 for b in layout.layout_id.iter() {
199 write!(f, "{:02x}", b)?;
200 }
201 writeln!(f, "\";")?;
202 writeln!(f)?;
203
204 write!(f, "export function assert")?;
208 write_pascal(f, layout.name)?;
209 writeln!(f, "Layout(data: Uint8Array): void {{")?;
210 write!(f, " assertLayoutId(data, ")?;
211 write_upper_snake(f, layout.name)?;
212 writeln!(f, "_LAYOUT_ID);")?;
213 writeln!(f, "}}")?;
214 writeln!(f)?;
215
216 write!(f, "export const ")?;
218 write_upper_snake(f, layout.name)?;
219 writeln!(f, "_DISC = {};", layout.disc)?;
220 writeln!(f)?;
221
222 write!(f, "export function decode")?;
224 write_pascal(f, layout.name)?;
225 writeln!(f, "(data: Uint8Array): ")?;
226 write!(f, " ")?;
227 write_pascal(f, layout.name)?;
228 writeln!(f, " {{")?;
229 write!(f, " assert")?;
230 write_pascal(f, layout.name)?;
231 writeln!(f, "Layout(data);")?;
232 writeln!(f, " if (data.length < {}) {{", layout.total_size)?;
233 writeln!(
234 f,
235 " throw new Error(`Data too small for {}: ${{data.length}} < {}`);",
236 layout.name, layout.total_size
237 )?;
238 writeln!(f, " }}")?;
239 writeln!(
240 f,
241 " const view = new DataView(data.buffer, data.byteOffset, data.byteLength);"
242 )?;
243
244 for field in layout.fields.iter() {
245 let offset = field.offset as usize;
246 let end = offset + field.size as usize;
247 write!(f, " const ")?;
248 write_camel(f, field.name)?;
249 write!(f, " = ")?;
250 write_decode_expr(f, field.canonical_type, offset, end)?;
251 writeln!(f, ";")?;
252 }
253
254 writeln!(f, " return {{")?;
255 for field in layout.fields.iter() {
256 write!(f, " ")?;
257 write_camel(f, field.name)?;
258 writeln!(f, ",")?;
259 }
260 writeln!(f, " }};")?;
261 writeln!(f, "}}")?;
262 writeln!(f)?;
263 }
264
265 Ok(())
266 }
267}
268
269pub struct TsInstructions<'a>(pub &'a ProgramManifest);
275
276impl<'a> fmt::Display for TsInstructions<'a> {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 let prog = self.0;
279
280 writeln!(f, "// Auto-generated by hopper client gen --ts")?;
281 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
282 writeln!(f, "// DO NOT EDIT")?;
283 writeln!(f)?;
284 writeln!(
285 f,
286 "import {{ PublicKey, TransactionInstruction }} from \"@solana/web3.js\";"
287 )?;
288 writeln!(f)?;
289
290 for ix in prog.instructions.iter() {
291 if !ix.args.is_empty() {
293 write!(f, "export interface ")?;
294 write_pascal(f, ix.name)?;
295 writeln!(f, "Args {{")?;
296 for arg in ix.args.iter() {
297 write!(f, " ")?;
298 write_camel(f, arg.name)?;
299 writeln!(f, ": {};", ts_type(arg.canonical_type))?;
300 }
301 writeln!(f, "}}")?;
302 writeln!(f)?;
303 }
304
305 write!(f, "export interface ")?;
307 write_pascal(f, ix.name)?;
308 writeln!(f, "Accounts {{")?;
309 for acc in ix.accounts.iter() {
310 write!(f, " ")?;
311 write_camel(f, acc.name)?;
312 writeln!(f, ": PublicKey;")?;
313 }
314 writeln!(f, "}}")?;
315 writeln!(f)?;
316
317 write!(f, "export function create")?;
319 write_pascal(f, ix.name)?;
320 writeln!(f, "Instruction(")?;
321 if !ix.args.is_empty() {
322 write!(f, " args: ")?;
323 write_pascal(f, ix.name)?;
324 writeln!(f, "Args,")?;
325 }
326 write!(f, " accounts: ")?;
327 write_pascal(f, ix.name)?;
328 writeln!(f, "Accounts,")?;
329 writeln!(f, " programId: PublicKey,")?;
330 writeln!(f, "): TransactionInstruction {{")?;
331
332 let data_size = instruction_data_size(ix);
334 writeln!(f, " const data = new Uint8Array({});", data_size)?;
335 writeln!(f, " const view = new DataView(data.buffer);")?;
336 writeln!(f, " data[0] = {}; // instruction discriminator", ix.tag)?;
337
338 let mut offset = 1usize; for arg in ix.args.iter() {
340 write_encode_expr(f, arg.canonical_type, arg.name, offset)?;
341 offset += arg.size as usize;
342 }
343
344 writeln!(f)?;
345
346 writeln!(f, " const keys = [")?;
348 for acc in ix.accounts.iter() {
349 write!(f, " {{ pubkey: accounts.")?;
350 write_camel(f, acc.name)?;
351 writeln!(
352 f,
353 ", isSigner: {}, isWritable: {} }},",
354 acc.signer, acc.writable
355 )?;
356 }
357 writeln!(f, " ];")?;
358 writeln!(f)?;
359 writeln!(
360 f,
361 " return new TransactionInstruction({{ keys, programId, data }});"
362 )?;
363 writeln!(f, "}}")?;
364 writeln!(f)?;
365 }
366
367 Ok(())
368 }
369}
370
371pub struct TsEvents<'a>(pub &'a ProgramManifest);
377
378impl<'a> fmt::Display for TsEvents<'a> {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 let prog = self.0;
381
382 writeln!(f, "// Auto-generated by hopper client gen --ts")?;
383 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
384 writeln!(f, "// DO NOT EDIT")?;
385 writeln!(f)?;
386 writeln!(f, "import {{ PublicKey }} from \"@solana/web3.js\";")?;
387 writeln!(f)?;
388
389 if prog.events.is_empty() {
390 writeln!(f, "// No events defined for this program.")?;
391 return Ok(());
392 }
393
394 for event in prog.events.iter() {
395 write!(f, "export interface ")?;
397 write_pascal(f, event.name)?;
398 writeln!(f, "Event {{")?;
399 for field in event.fields.iter() {
400 write!(f, " ")?;
401 write_camel(f, field.name)?;
402 writeln!(f, ": {};", ts_type(field.canonical_type))?;
403 }
404 writeln!(f, "}}")?;
405 writeln!(f)?;
406
407 write!(f, "export const ")?;
409 write_upper_snake(f, event.name)?;
410 writeln!(f, "_EVENT_DISC = {};", event.tag)?;
411 writeln!(f)?;
412
413 write!(f, "export function decode")?;
415 write_pascal(f, event.name)?;
416 writeln!(f, "Event(data: Uint8Array): ")?;
417 write!(f, " ")?;
418 write_pascal(f, event.name)?;
419 writeln!(f, "Event {{")?;
420 writeln!(
421 f,
422 " const view = new DataView(data.buffer, data.byteOffset, data.byteLength);"
423 )?;
424
425 for field in event.fields.iter() {
426 let offset = field.offset as usize;
427 let end = offset + field.size as usize;
428 write!(f, " const ")?;
429 write_camel(f, field.name)?;
430 write!(f, " = ")?;
431 write_decode_expr(f, field.canonical_type, offset, end)?;
432 writeln!(f, ";")?;
433 }
434
435 writeln!(f, " return {{")?;
436 for field in event.fields.iter() {
437 write!(f, " ")?;
438 write_camel(f, field.name)?;
439 writeln!(f, ",")?;
440 }
441 writeln!(f, " }};")?;
442 writeln!(f, "}}")?;
443 writeln!(f)?;
444 }
445
446 Ok(())
447 }
448}
449
450pub struct TsTypes<'a>(pub &'a ProgramManifest);
456
457impl<'a> fmt::Display for TsTypes<'a> {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 let prog = self.0;
460
461 writeln!(f, "// Auto-generated by hopper client gen --ts")?;
462 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
463 writeln!(f, "// DO NOT EDIT")?;
464 writeln!(f)?;
465 writeln!(f, "import {{ PublicKey }} from \"@solana/web3.js\";")?;
466 writeln!(f)?;
467 writeln!(f, "/** Hopper account header (16 bytes). */")?;
468 writeln!(f, "export interface HopperHeader {{")?;
469 writeln!(f, " disc: number;")?;
470 writeln!(f, " version: number;")?;
471 writeln!(f, " flags: number;")?;
472 writeln!(f, " layoutId: Uint8Array;")?;
473 writeln!(f, " reserved: Uint8Array;")?;
474 writeln!(f, "}}")?;
475 writeln!(f)?;
476 writeln!(f, "const HEADER_SIZE = 16;")?;
477 writeln!(f, "const LAYOUT_ID_OFFSET = 4;")?;
478 writeln!(f, "const LAYOUT_ID_LENGTH = 8;")?;
479 writeln!(f)?;
480 writeln!(f, "/** Decode the Hopper 16-byte account header. */")?;
481 writeln!(
482 f,
483 "export function decodeHeader(data: Uint8Array): HopperHeader {{"
484 )?;
485 writeln!(f, " if (data.length < HEADER_SIZE) {{")?;
486 writeln!(
487 f,
488 " throw new Error(`Hopper account too short: ${{data.length}} < ${{HEADER_SIZE}}`);"
489 )?;
490 writeln!(f, " }}")?;
491 writeln!(
492 f,
493 " const view = new DataView(data.buffer, data.byteOffset, data.byteLength);"
494 )?;
495 writeln!(f, " return {{")?;
496 writeln!(f, " disc: data[0],")?;
497 writeln!(f, " version: data[1],")?;
498 writeln!(f, " flags: view.getUint16(2, true),")?;
499 writeln!(
500 f,
501 " layoutId: data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH),"
502 )?;
503 writeln!(f, " reserved: data.slice(12, 16),")?;
504 writeln!(f, " }};")?;
505 writeln!(f, "}}")?;
506 writeln!(f)?;
507 writeln!(
508 f,
509 "/** All account discriminators for {} v{}. */",
510 prog.name, prog.version
511 )?;
512 writeln!(f, "export const Discriminators = {{")?;
513 for layout in prog.layouts.iter() {
514 write!(f, " ")?;
515 write_pascal(f, layout.name)?;
516 writeln!(f, ": {},", layout.disc)?;
517 }
518 writeln!(f, "}} as const;")?;
519
520 Ok(())
521 }
522}
523
524pub struct TsIndex<'a>(pub &'a ProgramManifest);
530
531impl<'a> fmt::Display for TsIndex<'a> {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 let prog = self.0;
534
535 writeln!(f, "// Auto-generated by hopper client gen --ts")?;
536 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
537 writeln!(f, "// DO NOT EDIT")?;
538 writeln!(f)?;
539 writeln!(f, "export * from \"./types\";")?;
540 writeln!(f, "export * from \"./accounts\";")?;
541 writeln!(f, "export * from \"./instructions\";")?;
542 writeln!(f, "export * from \"./events\";")?;
543
544 Ok(())
545 }
546}
547
548pub struct TsClientGen<'a>(pub &'a ProgramManifest);
569
570impl<'a> fmt::Display for TsClientGen<'a> {
571 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572 let prog = self.0;
573
574 writeln!(f, "=== types.ts ===")?;
575 write!(f, "{}", TsTypes(prog))?;
576 writeln!(f)?;
577
578 writeln!(f, "=== accounts.ts ===")?;
579 write!(f, "{}", TsAccounts(prog))?;
580 writeln!(f)?;
581
582 writeln!(f, "=== instructions.ts ===")?;
583 write!(f, "{}", TsInstructions(prog))?;
584 writeln!(f)?;
585
586 writeln!(f, "=== events.ts ===")?;
587 write!(f, "{}", TsEvents(prog))?;
588 writeln!(f)?;
589
590 writeln!(f, "=== index.ts ===")?;
591 write!(f, "{}", TsIndex(prog))?;
592
593 Ok(())
594 }
595}
596
597fn write_upper_snake(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
602 for c in name.chars() {
603 if c == '-' || c == ' ' {
604 write!(f, "_")?;
605 } else {
606 for uc in c.to_uppercase() {
607 write!(f, "{}", uc)?;
608 }
609 }
610 }
611 Ok(())
612}
613
614fn write_decode_expr(
615 f: &mut fmt::Formatter<'_>,
616 canonical: &str,
617 offset: usize,
618 end: usize,
619) -> fmt::Result {
620 match canonical {
621 "u8" => write!(f, "data[{}]", offset),
622 "i8" => write!(f, "view.getInt8({})", offset),
623 "u16" => write!(f, "view.getUint16({}, true)", offset),
624 "i16" => write!(f, "view.getInt16({}, true)", offset),
625 "u32" => write!(f, "view.getUint32({}, true)", offset),
626 "i32" => write!(f, "view.getInt32({}, true)", offset),
627 "u64" => write!(f, "view.getBigUint64({}, true)", offset),
628 "i64" => write!(f, "view.getBigInt64({}, true)", offset),
629 "u128" => {
630 write!(
631 f,
632 "view.getBigUint64({}, true) | (view.getBigUint64({}, true) << 64n)",
633 offset,
634 offset + 8
635 )
636 }
637 "i128" => {
638 write!(
639 f,
640 "view.getBigInt64({}, true) | (view.getBigUint64({}, true) << 64n)",
641 offset + 8,
642 offset
643 )
644 }
645 "bool" => write!(f, "data[{}] !== 0", offset),
646 "Pubkey" => write!(f, "new PublicKey(data.slice({}, {}))", offset, end),
647 _ => write!(f, "data.slice({}, {})", offset, end),
648 }
649}
650
651fn write_encode_expr(
652 f: &mut fmt::Formatter<'_>,
653 canonical: &str,
654 name: &str,
655 offset: usize,
656) -> fmt::Result {
657 match canonical {
658 "u8" => {
659 write!(f, " data[{}] = args.", offset)?;
660 write_camel(f, name)?;
661 writeln!(f, ";")
662 }
663 "i8" => {
664 write!(f, " view.setInt8({}, args.", offset)?;
665 write_camel(f, name)?;
666 writeln!(f, ");")
667 }
668 "u16" => {
669 write!(f, " view.setUint16({}, args.", offset)?;
670 write_camel(f, name)?;
671 writeln!(f, ", true);")
672 }
673 "i16" => {
674 write!(f, " view.setInt16({}, args.", offset)?;
675 write_camel(f, name)?;
676 writeln!(f, ", true);")
677 }
678 "u32" => {
679 write!(f, " view.setUint32({}, args.", offset)?;
680 write_camel(f, name)?;
681 writeln!(f, ", true);")
682 }
683 "i32" => {
684 write!(f, " view.setInt32({}, args.", offset)?;
685 write_camel(f, name)?;
686 writeln!(f, ", true);")
687 }
688 "u64" => {
689 write!(f, " view.setBigUint64({}, args.", offset)?;
690 write_camel(f, name)?;
691 writeln!(f, ", true);")
692 }
693 "i64" => {
694 write!(f, " view.setBigInt64({}, args.", offset)?;
695 write_camel(f, name)?;
696 writeln!(f, ", true);")
697 }
698 "u128" => {
699 write!(f, " view.setBigUint64({}, args.", offset)?;
700 write_camel(f, name)?;
701 writeln!(f, " & 0xFFFFFFFFFFFFFFFFn, true);")?;
702 write!(f, " view.setBigUint64({}, args.", offset + 8)?;
703 write_camel(f, name)?;
704 writeln!(f, " >> 64n, true);")
705 }
706 "bool" => {
707 write!(f, " data[{}] = args.", offset)?;
708 write_camel(f, name)?;
709 writeln!(f, " ? 1 : 0;")
710 }
711 "Pubkey" => {
712 write!(f, " data.set(args.")?;
713 write_camel(f, name)?;
714 writeln!(f, ".toBytes(), {});", offset)
715 }
716 _ => {
717 write!(f, " data.set(args.")?;
718 write_camel(f, name)?;
719 writeln!(f, ", {});", offset)
720 }
721 }
722}
723
724fn instruction_data_size(ix: &InstructionDescriptor) -> usize {
725 let mut size = 1usize; for arg in ix.args.iter() {
727 size += arg.size as usize;
728 }
729 size
730}
731
732fn kt_type(canonical: &str) -> &str {
738 match canonical {
739 "u8" => "UByte",
740 "i8" => "Byte",
741 "u16" => "UShort",
742 "i16" => "Short",
743 "u32" => "UInt",
744 "i32" => "Int",
745 "u64" => "ULong",
746 "i64" => "Long",
747 "u128" | "i128" => "ByteArray",
748 "bool" => "Boolean",
749 "Pubkey" => "PublicKey",
750 _ => {
751 if canonical.starts_with("[u8;") {
752 "ByteArray"
753 } else {
754 "ByteArray"
755 }
756 }
757 }
758}
759
760fn write_kt_pascal(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
762 write_pascal(f, name)
763}
764
765fn write_kt_camel(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
767 write_camel(f, name)
768}
769
770fn write_kt_const(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
772 write_upper_snake(f, name)
773}
774
775fn write_kt_decode_expr(
776 f: &mut fmt::Formatter<'_>,
777 canonical: &str,
778 offset: usize,
779 end: usize,
780) -> fmt::Result {
781 match canonical {
782 "u8" => write!(f, "data[{}].toUByte()", offset),
783 "i8" => write!(f, "data[{}]", offset),
784 "u16" => write!(
785 f,
786 "ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort()",
787 offset
788 ),
789 "i16" => write!(
790 f,
791 "ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).short",
792 offset
793 ),
794 "u32" => write!(
795 f,
796 "ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).int.toUInt()",
797 offset
798 ),
799 "i32" => write!(
800 f,
801 "ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).int",
802 offset
803 ),
804 "u64" => write!(
805 f,
806 "ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).long.toULong()",
807 offset
808 ),
809 "i64" => write!(
810 f,
811 "ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).long",
812 offset
813 ),
814 "u128" | "i128" => write!(f, "data.copyOfRange({}, {})", offset, end),
815 "bool" => write!(f, "data[{}] != 0.toByte()", offset),
816 "Pubkey" => write!(f, "PublicKey(data.copyOfRange({}, {}))", offset, end),
817 _ => write!(f, "data.copyOfRange({}, {})", offset, end),
818 }
819}
820
821fn write_kt_encode_expr(
822 f: &mut fmt::Formatter<'_>,
823 canonical: &str,
824 name: &str,
825 offset: usize,
826) -> fmt::Result {
827 match canonical {
828 "u8" => {
829 write!(f, " data[{}] = args.", offset)?;
830 write_kt_camel(f, name)?;
831 writeln!(f, ".toByte()")
832 }
833 "i8" => {
834 write!(f, " data[{}] = args.", offset)?;
835 write_kt_camel(f, name)?;
836 writeln!(f, "")
837 }
838 "u16" => {
839 write!(
840 f,
841 " ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(args.",
842 offset
843 )?;
844 write_kt_camel(f, name)?;
845 writeln!(f, ".toShort())")
846 }
847 "i16" => {
848 write!(
849 f,
850 " ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(args.",
851 offset
852 )?;
853 write_kt_camel(f, name)?;
854 writeln!(f, ")")
855 }
856 "u32" => {
857 write!(
858 f,
859 " ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(args.",
860 offset
861 )?;
862 write_kt_camel(f, name)?;
863 writeln!(f, ".toInt())")
864 }
865 "i32" => {
866 write!(
867 f,
868 " ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(args.",
869 offset
870 )?;
871 write_kt_camel(f, name)?;
872 writeln!(f, ")")
873 }
874 "u64" => {
875 write!(
876 f,
877 " ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).putLong(args.",
878 offset
879 )?;
880 write_kt_camel(f, name)?;
881 writeln!(f, ".toLong())")
882 }
883 "i64" => {
884 write!(
885 f,
886 " ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).putLong(args.",
887 offset
888 )?;
889 write_kt_camel(f, name)?;
890 writeln!(f, ")")
891 }
892 "bool" => {
893 write!(f, " data[{}] = if (args.", offset)?;
894 write_kt_camel(f, name)?;
895 writeln!(f, ") 1.toByte() else 0.toByte()")
896 }
897 "Pubkey" => {
898 write!(f, " args.")?;
899 write_kt_camel(f, name)?;
900 writeln!(f, ".toByteArray().copyInto(data, {})", offset)
901 }
902 _ => {
903 write!(f, " args.")?;
904 write_kt_camel(f, name)?;
905 writeln!(f, ".copyInto(data, {})", offset)
906 }
907 }
908}
909
910pub struct KtAccounts<'a>(pub &'a ProgramManifest);
916
917impl<'a> fmt::Display for KtAccounts<'a> {
918 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919 let prog = self.0;
920
921 writeln!(f, "// Auto-generated by hopper client gen --kt")?;
922 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
923 writeln!(f, "// DO NOT EDIT")?;
924 writeln!(f)?;
925 writeln!(
926 f,
927 "package hopper.generated.{}",
928 prog.name.replace('-', "_")
929 )?;
930 writeln!(f)?;
931 writeln!(f, "import org.sol4k.PublicKey")?;
932 writeln!(f, "import java.nio.ByteBuffer")?;
933 writeln!(f, "import java.nio.ByteOrder")?;
934 writeln!(f)?;
935
936 writeln!(f, "/** Hopper account header size in bytes. */")?;
942 writeln!(f, "const val HEADER_SIZE: Int = 16")?;
943 writeln!(
944 f,
945 "/** Byte offset of the 8-byte layout fingerprint in a Hopper header. */"
946 )?;
947 writeln!(f, "const val LAYOUT_ID_OFFSET: Int = 4")?;
948 writeln!(f, "/** Byte length of the layout fingerprint. */")?;
949 writeln!(f, "const val LAYOUT_ID_LENGTH: Int = 8")?;
950 writeln!(f)?;
951
952 writeln!(
953 f,
954 "class LayoutMismatchException(expected: String, actual: String) :"
955 )?;
956 writeln!(f, " RuntimeException(\"Hopper layout mismatch: account header reports $actual, expected $expected\")")?;
957 writeln!(f)?;
958
959 writeln!(f, "/**")?;
960 writeln!(
961 f,
962 " * Raise if `data` is not a Hopper account encoding the expected layout."
963 )?;
964 writeln!(f, " *")?;
965 writeln!(
966 f,
967 " * Reads the 8-byte LAYOUT_ID fingerprint from the 16-byte Hopper header"
968 )?;
969 writeln!(
970 f,
971 " * (bytes 4..12) and compares it against `expectedHex` (16 lowercase hex chars)."
972 )?;
973 writeln!(
974 f,
975 " * Mismatch means the on-chain program was upgraded with a different ABI"
976 )?;
977 writeln!(f, " * than the client was generated against.")?;
978 writeln!(f, " */")?;
979 writeln!(
980 f,
981 "fun assertLayoutId(data: ByteArray, expectedHex: String) {{"
982 )?;
983 writeln!(f, " if (data.size < HEADER_SIZE) {{")?;
984 writeln!(f, " throw RuntimeException(\"Hopper account too short: ${{data.size}} < $HEADER_SIZE\")")?;
985 writeln!(f, " }}")?;
986 writeln!(f, " val sb = StringBuilder(LAYOUT_ID_LENGTH * 2)")?;
987 writeln!(f, " for (i in 0 until LAYOUT_ID_LENGTH) {{")?;
988 writeln!(
989 f,
990 " val byte = data[LAYOUT_ID_OFFSET + i].toInt() and 0xFF"
991 )?;
992 writeln!(f, " sb.append(String.format(\"%02x\", byte))")?;
993 writeln!(f, " }}")?;
994 writeln!(f, " val actualHex = sb.toString()")?;
995 writeln!(f, " if (actualHex != expectedHex.lowercase()) {{")?;
996 writeln!(
997 f,
998 " throw LayoutMismatchException(expectedHex, actualHex)"
999 )?;
1000 writeln!(f, " }}")?;
1001 writeln!(f, "}}")?;
1002 writeln!(f)?;
1003
1004 for layout in prog.layouts.iter() {
1005 write!(f, "data class ")?;
1007 write_kt_pascal(f, layout.name)?;
1008 writeln!(f, "(")?;
1009 for (i, field) in layout.fields.iter().enumerate() {
1010 write!(f, " val ")?;
1011 write_kt_camel(f, field.name)?;
1012 write!(f, ": {}", kt_type(field.canonical_type))?;
1013 if i + 1 < layout.fields.len() {
1014 writeln!(f, ",")?;
1015 } else {
1016 writeln!(f)?;
1017 }
1018 }
1019 writeln!(f, ")")?;
1020 writeln!(f)?;
1021
1022 write!(f, "const val ")?;
1026 write_kt_const(f, layout.name)?;
1027 write!(f, "_LAYOUT_ID: String = \"")?;
1028 for b in layout.layout_id.iter() {
1029 write!(f, "{:02x}", b)?;
1030 }
1031 writeln!(f, "\"")?;
1032 writeln!(f)?;
1033
1034 write!(f, "fun assert")?;
1037 write_kt_pascal(f, layout.name)?;
1038 writeln!(f, "Layout(data: ByteArray) {{")?;
1039 write!(f, " assertLayoutId(data, ")?;
1040 write_kt_const(f, layout.name)?;
1041 writeln!(f, "_LAYOUT_ID)")?;
1042 writeln!(f, "}}")?;
1043 writeln!(f)?;
1044
1045 write!(f, "const val ")?;
1047 write_kt_const(f, layout.name)?;
1048 writeln!(f, "_DISC: Byte = {}", layout.disc)?;
1049 writeln!(f)?;
1050
1051 write!(f, "fun decode")?;
1053 write_kt_pascal(f, layout.name)?;
1054 write!(f, "(data: ByteArray): ")?;
1055 write_kt_pascal(f, layout.name)?;
1056 writeln!(f, " {{")?;
1057 write!(f, " assert")?;
1058 write_kt_pascal(f, layout.name)?;
1059 writeln!(f, "Layout(data)")?;
1060 writeln!(
1061 f,
1062 " require(data.size >= {}) {{ \"Data too small for {}\" }}",
1063 layout.total_size, layout.name
1064 )?;
1065
1066 for field in layout.fields.iter() {
1067 let offset = field.offset as usize;
1068 let end = offset + field.size as usize;
1069 write!(f, " val ")?;
1070 write_kt_camel(f, field.name)?;
1071 write!(f, " = ")?;
1072 write_kt_decode_expr(f, field.canonical_type, offset, end)?;
1073 writeln!(f)?;
1074 }
1075
1076 write!(f, " return ")?;
1077 write_kt_pascal(f, layout.name)?;
1078 writeln!(f, "(")?;
1079 for (i, field) in layout.fields.iter().enumerate() {
1080 write!(f, " ")?;
1081 write_kt_camel(f, field.name)?;
1082 write!(f, " = ")?;
1083 write_kt_camel(f, field.name)?;
1084 if i + 1 < layout.fields.len() {
1085 writeln!(f, ",")?;
1086 } else {
1087 writeln!(f)?;
1088 }
1089 }
1090 writeln!(f, " )")?;
1091 writeln!(f, "}}")?;
1092 writeln!(f)?;
1093 }
1094
1095 Ok(())
1096 }
1097}
1098
1099pub struct KtInstructions<'a>(pub &'a ProgramManifest);
1105
1106impl<'a> fmt::Display for KtInstructions<'a> {
1107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1108 let prog = self.0;
1109
1110 writeln!(f, "// Auto-generated by hopper client gen --kt")?;
1111 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
1112 writeln!(f, "// DO NOT EDIT")?;
1113 writeln!(f)?;
1114 writeln!(
1115 f,
1116 "package hopper.generated.{}",
1117 prog.name.replace('-', "_")
1118 )?;
1119 writeln!(f)?;
1120 writeln!(f, "import org.sol4k.PublicKey")?;
1121 writeln!(f, "import org.sol4k.instruction.AccountMeta")?;
1122 writeln!(f, "import org.sol4k.instruction.Instruction")?;
1123 writeln!(f, "import java.nio.ByteBuffer")?;
1124 writeln!(f, "import java.nio.ByteOrder")?;
1125 writeln!(f)?;
1126
1127 for ix in prog.instructions.iter() {
1128 if !ix.args.is_empty() {
1130 write!(f, "data class ")?;
1131 write_kt_pascal(f, ix.name)?;
1132 writeln!(f, "Args(")?;
1133 for (i, arg) in ix.args.iter().enumerate() {
1134 write!(f, " val ")?;
1135 write_kt_camel(f, arg.name)?;
1136 write!(f, ": {}", kt_type(arg.canonical_type))?;
1137 if i + 1 < ix.args.len() {
1138 writeln!(f, ",")?;
1139 } else {
1140 writeln!(f)?;
1141 }
1142 }
1143 writeln!(f, ")")?;
1144 writeln!(f)?;
1145 }
1146
1147 write!(f, "data class ")?;
1149 write_kt_pascal(f, ix.name)?;
1150 writeln!(f, "Accounts(")?;
1151 for (i, acc) in ix.accounts.iter().enumerate() {
1152 write!(f, " val ")?;
1153 write_kt_camel(f, acc.name)?;
1154 write!(f, ": PublicKey")?;
1155 if i + 1 < ix.accounts.len() {
1156 writeln!(f, ",")?;
1157 } else {
1158 writeln!(f)?;
1159 }
1160 }
1161 writeln!(f, ")")?;
1162 writeln!(f)?;
1163
1164 write!(f, "fun create")?;
1166 write_kt_pascal(f, ix.name)?;
1167 writeln!(f, "Instruction(")?;
1168 if !ix.args.is_empty() {
1169 write!(f, " args: ")?;
1170 write_kt_pascal(f, ix.name)?;
1171 writeln!(f, "Args,")?;
1172 }
1173 write!(f, " accounts: ")?;
1174 write_kt_pascal(f, ix.name)?;
1175 writeln!(f, "Accounts,")?;
1176 writeln!(f, " programId: PublicKey,")?;
1177 writeln!(f, "): Instruction {{")?;
1178
1179 let data_size = instruction_data_size(ix);
1180 writeln!(f, " val data = ByteArray({})", data_size)?;
1181 writeln!(
1182 f,
1183 " data[0] = {}.toByte() // instruction discriminator",
1184 ix.tag
1185 )?;
1186
1187 let mut offset = 1usize;
1188 for arg in ix.args.iter() {
1189 write_kt_encode_expr(f, arg.canonical_type, arg.name, offset)?;
1190 offset += arg.size as usize;
1191 }
1192
1193 writeln!(f)?;
1194 writeln!(f, " val keys = listOf(")?;
1195 for acc in ix.accounts.iter() {
1196 write!(f, " AccountMeta(accounts.")?;
1197 write_kt_camel(f, acc.name)?;
1198 writeln!(
1199 f,
1200 ", isSigner = {}, isWritable = {}),",
1201 acc.signer, acc.writable
1202 )?;
1203 }
1204 writeln!(f, " )")?;
1205 writeln!(f)?;
1206 writeln!(f, " return Instruction(programId, keys, data)")?;
1207 writeln!(f, "}}")?;
1208 writeln!(f)?;
1209 }
1210
1211 Ok(())
1212 }
1213}
1214
1215pub struct KtEvents<'a>(pub &'a ProgramManifest);
1221
1222impl<'a> fmt::Display for KtEvents<'a> {
1223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1224 let prog = self.0;
1225
1226 writeln!(f, "// Auto-generated by hopper client gen --kt")?;
1227 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
1228 writeln!(f, "// DO NOT EDIT")?;
1229 writeln!(f)?;
1230 writeln!(
1231 f,
1232 "package hopper.generated.{}",
1233 prog.name.replace('-', "_")
1234 )?;
1235 writeln!(f)?;
1236 writeln!(f, "import org.sol4k.PublicKey")?;
1237 writeln!(f, "import java.nio.ByteBuffer")?;
1238 writeln!(f, "import java.nio.ByteOrder")?;
1239 writeln!(f)?;
1240
1241 if prog.events.is_empty() {
1242 writeln!(f, "// No events defined for this program.")?;
1243 return Ok(());
1244 }
1245
1246 for event in prog.events.iter() {
1247 write!(f, "data class ")?;
1248 write_kt_pascal(f, event.name)?;
1249 writeln!(f, "Event(")?;
1250 for (i, field) in event.fields.iter().enumerate() {
1251 write!(f, " val ")?;
1252 write_kt_camel(f, field.name)?;
1253 write!(f, ": {}", kt_type(field.canonical_type))?;
1254 if i + 1 < event.fields.len() {
1255 writeln!(f, ",")?;
1256 } else {
1257 writeln!(f)?;
1258 }
1259 }
1260 writeln!(f, ")")?;
1261 writeln!(f)?;
1262
1263 write!(f, "const val ")?;
1264 write_kt_const(f, event.name)?;
1265 writeln!(f, "_EVENT_DISC: Byte = {}", event.tag)?;
1266 writeln!(f)?;
1267
1268 write!(f, "fun decode")?;
1269 write_kt_pascal(f, event.name)?;
1270 write!(f, "Event(data: ByteArray): ")?;
1271 write_kt_pascal(f, event.name)?;
1272 writeln!(f, "Event {{")?;
1273
1274 for field in event.fields.iter() {
1275 let offset = field.offset as usize;
1276 let end = offset + field.size as usize;
1277 write!(f, " val ")?;
1278 write_kt_camel(f, field.name)?;
1279 write!(f, " = ")?;
1280 write_kt_decode_expr(f, field.canonical_type, offset, end)?;
1281 writeln!(f)?;
1282 }
1283
1284 write!(f, " return ")?;
1285 write_kt_pascal(f, event.name)?;
1286 writeln!(f, "Event(")?;
1287 for (i, field) in event.fields.iter().enumerate() {
1288 write!(f, " ")?;
1289 write_kt_camel(f, field.name)?;
1290 write!(f, " = ")?;
1291 write_kt_camel(f, field.name)?;
1292 if i + 1 < event.fields.len() {
1293 writeln!(f, ",")?;
1294 } else {
1295 writeln!(f)?;
1296 }
1297 }
1298 writeln!(f, " )")?;
1299 writeln!(f, "}}")?;
1300 writeln!(f)?;
1301 }
1302
1303 Ok(())
1304 }
1305}
1306
1307pub struct KtTypes<'a>(pub &'a ProgramManifest);
1313
1314impl<'a> fmt::Display for KtTypes<'a> {
1315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1316 let prog = self.0;
1317
1318 writeln!(f, "// Auto-generated by hopper client gen --kt")?;
1319 writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
1320 writeln!(f, "// DO NOT EDIT")?;
1321 writeln!(f)?;
1322 writeln!(
1323 f,
1324 "package hopper.generated.{}",
1325 prog.name.replace('-', "_")
1326 )?;
1327 writeln!(f)?;
1328 writeln!(f, "import java.nio.ByteBuffer")?;
1329 writeln!(f, "import java.nio.ByteOrder")?;
1330 writeln!(f)?;
1331 writeln!(f, "/** Hopper account header (16 bytes). */")?;
1332 writeln!(f, "data class HopperHeader(")?;
1333 writeln!(f, " val disc: UByte,")?;
1334 writeln!(f, " val version: UByte,")?;
1335 writeln!(f, " val flags: UShort,")?;
1336 writeln!(f, " val layoutId: ByteArray,")?;
1337 writeln!(f, " val reserved: ByteArray")?;
1338 writeln!(f, ")")?;
1339 writeln!(f)?;
1340 writeln!(f, "private const val TYPES_HEADER_SIZE: Int = 16")?;
1341 writeln!(f, "private const val TYPES_LAYOUT_ID_OFFSET: Int = 4")?;
1342 writeln!(f, "private const val TYPES_LAYOUT_ID_LENGTH: Int = 8")?;
1343 writeln!(f)?;
1344 writeln!(f, "/** Decode the Hopper 16-byte account header. */")?;
1345 writeln!(f, "fun decodeHeader(data: ByteArray): HopperHeader {{")?;
1346 writeln!(
1347 f,
1348 " require(data.size >= TYPES_HEADER_SIZE) {{ \"Data too small for header\" }}"
1349 )?;
1350 writeln!(f, " return HopperHeader(")?;
1351 writeln!(f, " disc = data[0].toUByte(),")?;
1352 writeln!(f, " version = data[1].toUByte(),")?;
1353 writeln!(
1354 f,
1355 " flags = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort(),"
1356 )?;
1357 writeln!(
1358 f,
1359 " layoutId = data.copyOfRange(TYPES_LAYOUT_ID_OFFSET, TYPES_LAYOUT_ID_OFFSET + TYPES_LAYOUT_ID_LENGTH),"
1360 )?;
1361 writeln!(f, " reserved = data.copyOfRange(12, 16)")?;
1362 writeln!(f, " )")?;
1363 writeln!(f, "}}")?;
1364 writeln!(f)?;
1365 writeln!(
1366 f,
1367 "/** All account discriminators for {} v{}. */",
1368 prog.name, prog.version
1369 )?;
1370 writeln!(f, "object Discriminators {{")?;
1371 for layout in prog.layouts.iter() {
1372 write!(f, " const val ")?;
1373 write_kt_const(f, layout.name)?;
1374 writeln!(f, ": Byte = {}", layout.disc)?;
1375 }
1376 writeln!(f, "}}")?;
1377
1378 Ok(())
1379 }
1380}
1381
1382pub struct KtClientGen<'a>(pub &'a ProgramManifest);
1400
1401impl<'a> fmt::Display for KtClientGen<'a> {
1402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1403 let prog = self.0;
1404
1405 writeln!(f, "=== Types.kt ===")?;
1406 write!(f, "{}", KtTypes(prog))?;
1407 writeln!(f)?;
1408
1409 writeln!(f, "=== Accounts.kt ===")?;
1410 write!(f, "{}", KtAccounts(prog))?;
1411 writeln!(f)?;
1412
1413 writeln!(f, "=== Instructions.kt ===")?;
1414 write!(f, "{}", KtInstructions(prog))?;
1415 writeln!(f)?;
1416
1417 writeln!(f, "=== Events.kt ===")?;
1418 write!(f, "{}", KtEvents(prog))?;
1419
1420 Ok(())
1421 }
1422}
1423
1424#[cfg(test)]
1429mod tests {
1430 extern crate alloc;
1431 use super::*;
1432 use crate::{
1433 AccountEntry, ArgDescriptor, EventDescriptor, FieldDescriptor, FieldIntent, LayoutManifest,
1434 };
1435 use alloc::string::ToString;
1436
1437 fn test_manifest() -> ProgramManifest {
1438 static FIELDS: &[FieldDescriptor] = &[
1439 FieldDescriptor {
1440 name: "authority",
1441 canonical_type: "Pubkey",
1442 size: 32,
1443 offset: 16,
1444 intent: FieldIntent::Custom,
1445 },
1446 FieldDescriptor {
1447 name: "amount",
1448 canonical_type: "u64",
1449 size: 8,
1450 offset: 48,
1451 intent: FieldIntent::Custom,
1452 },
1453 FieldDescriptor {
1454 name: "is_active",
1455 canonical_type: "bool",
1456 size: 1,
1457 offset: 56,
1458 intent: FieldIntent::Custom,
1459 },
1460 ];
1461
1462 static LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
1463 name: "vault",
1464 disc: 1,
1465 version: 1,
1466 layout_id: [0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44],
1467 total_size: 64,
1468 field_count: 3,
1469 fields: FIELDS,
1470 }];
1471
1472 static ARGS: &[ArgDescriptor] = &[ArgDescriptor {
1473 name: "amount",
1474 canonical_type: "u64",
1475 size: 8,
1476 }];
1477
1478 static ACCOUNTS: &[AccountEntry] = &[
1479 AccountEntry {
1480 name: "authority",
1481 writable: false,
1482 signer: true,
1483 layout_ref: "",
1484 },
1485 AccountEntry {
1486 name: "vault",
1487 writable: true,
1488 signer: false,
1489 layout_ref: "vault",
1490 },
1491 ];
1492
1493 static INSTRUCTIONS: &[InstructionDescriptor] = &[InstructionDescriptor {
1494 name: "deposit",
1495 tag: 0,
1496 args: ARGS,
1497 accounts: ACCOUNTS,
1498 capabilities: &["write"],
1499 policy_pack: "standard",
1500 receipt_expected: true,
1501 }];
1502
1503 static EVENT_FIELDS: &[FieldDescriptor] = &[
1504 FieldDescriptor {
1505 name: "depositor",
1506 canonical_type: "Pubkey",
1507 size: 32,
1508 offset: 0,
1509 intent: FieldIntent::Custom,
1510 },
1511 FieldDescriptor {
1512 name: "amount",
1513 canonical_type: "u64",
1514 size: 8,
1515 offset: 32,
1516 intent: FieldIntent::Custom,
1517 },
1518 ];
1519
1520 static EVENTS: &[EventDescriptor] = &[EventDescriptor {
1521 name: "deposit_event",
1522 tag: 0,
1523 fields: EVENT_FIELDS,
1524 }];
1525
1526 ProgramManifest {
1527 name: "test_vault",
1528 version: "0.1.0",
1529 description: "A test vault program",
1530 layouts: LAYOUTS,
1531 layout_metadata: &[],
1532 instructions: INSTRUCTIONS,
1533 events: EVENTS,
1534 policies: &[],
1535 compatibility_pairs: &[],
1536 tooling_hints: &[],
1537 contexts: &[],
1538 }
1539 }
1540
1541 #[test]
1542 fn ts_accounts_generates_interface() {
1543 let m = test_manifest();
1544 let output = TsAccounts(&m).to_string();
1545 assert!(output.contains("export interface Vault {"));
1546 assert!(output.contains("authority: PublicKey;"));
1547 assert!(output.contains("amount: bigint;"));
1548 assert!(output.contains("isActive: boolean;"));
1549 }
1550
1551 #[test]
1552 fn ts_accounts_generates_decoder() {
1553 let m = test_manifest();
1554 let output = TsAccounts(&m).to_string();
1555 assert!(output.contains("export function decodeVault(data: Uint8Array)"));
1556 assert!(output.contains("export function decodeVault(data: Uint8Array): \n Vault {\n assertVaultLayout(data);"));
1557 assert!(
1558 output.contains("throw new Error(`Data too small for vault: ${data.length} < 64`);")
1559 );
1560 assert!(output.contains("new PublicKey(data.slice(16, 48))"));
1561 assert!(output.contains("view.getBigUint64(48, true)"));
1562 assert!(output.contains("data[56] !== 0"));
1563 }
1564
1565 #[test]
1566 fn ts_accounts_generates_discriminator() {
1567 let m = test_manifest();
1568 let output = TsAccounts(&m).to_string();
1569 assert!(output.contains("export const VAULT_DISC = 1;"));
1570 }
1571
1572 #[test]
1573 fn ts_instructions_generates_builder() {
1574 let m = test_manifest();
1575 let output = TsInstructions(&m).to_string();
1576 assert!(output.contains("export interface DepositArgs {"));
1577 assert!(output.contains("export interface DepositAccounts {"));
1578 assert!(output.contains("export function createDepositInstruction("));
1579 assert!(output.contains("data[0] = 0; // instruction discriminator"));
1580 assert!(output.contains("view.setBigUint64(1, args.amount, true);"));
1581 }
1582
1583 #[test]
1584 fn ts_instructions_account_meta() {
1585 let m = test_manifest();
1586 let output = TsInstructions(&m).to_string();
1587 assert!(output.contains("accounts.authority, isSigner: true, isWritable: false"));
1588 assert!(output.contains("accounts.vault, isSigner: false, isWritable: true"));
1589 }
1590
1591 #[test]
1592 fn ts_events_generates_decoder() {
1593 let m = test_manifest();
1594 let output = TsEvents(&m).to_string();
1595 assert!(output.contains("export interface DepositEventEvent {"));
1596 assert!(output.contains("export function decodeDepositEventEvent(data: Uint8Array)"));
1597 assert!(output.contains("DEPOSIT_EVENT_EVENT_DISC = 0;"));
1598 }
1599
1600 #[test]
1601 fn ts_types_generates_header() {
1602 let m = test_manifest();
1603 let output = TsTypes(&m).to_string();
1604 assert!(output.contains("export interface HopperHeader {"));
1605 assert!(output.contains("export function decodeHeader(data: Uint8Array)"));
1606 assert!(output.contains("flags: view.getUint16(2, true),"));
1607 assert!(output.contains(
1608 "layoutId: data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH),"
1609 ));
1610 assert!(output.contains("reserved: data.slice(12, 16),"));
1611 assert!(output.contains("Vault: 1,"));
1612 }
1613
1614 #[test]
1615 fn ts_decode_header_and_assert_layout_id_share_offset() {
1616 let m = test_manifest();
1617 let accounts = TsAccounts(&m).to_string();
1618 let types = TsTypes(&m).to_string();
1619 assert!(accounts.contains("export const LAYOUT_ID_OFFSET = 4;"));
1620 assert!(accounts.contains("data[LAYOUT_ID_OFFSET + i]"));
1621 assert!(types.contains("const LAYOUT_ID_OFFSET = 4;"));
1622 assert!(types.contains("data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH)"));
1623 assert!(!types.contains("data.slice(2, 10)"));
1624 }
1625
1626 #[test]
1627 fn ts_index_reexports_all() {
1628 let m = test_manifest();
1629 let output = TsIndex(&m).to_string();
1630 assert!(output.contains("export * from \"./types\";"));
1631 assert!(output.contains("export * from \"./accounts\";"));
1632 assert!(output.contains("export * from \"./instructions\";"));
1633 assert!(output.contains("export * from \"./events\";"));
1634 }
1635
1636 #[test]
1637 fn ts_full_client_gen_has_all_sections() {
1638 let m = test_manifest();
1639 let output = TsClientGen(&m).to_string();
1640 assert!(output.contains("=== types.ts ==="));
1641 assert!(output.contains("=== accounts.ts ==="));
1642 assert!(output.contains("=== instructions.ts ==="));
1643 assert!(output.contains("=== events.ts ==="));
1644 assert!(output.contains("=== index.ts ==="));
1645 }
1646
1647 #[test]
1648 fn ts_accounts_emits_layout_id_constants_and_assertion_helpers() {
1649 let m = test_manifest();
1650 let output = TsAccounts(&m).to_string();
1651 assert!(output.contains("export const LAYOUT_ID_OFFSET = 4;"));
1653 assert!(output.contains("export const LAYOUT_ID_LENGTH = 8;"));
1654 assert!(output.contains(
1655 "export function assertLayoutId(data: Uint8Array, expectedHex: string): void"
1656 ));
1657 assert!(output.contains("export const VAULT_LAYOUT_ID = \"aabbccdd11223344\";"));
1659 assert!(output.contains("export function assertVaultLayout(data: Uint8Array): void"));
1660 assert!(output.contains("assertLayoutId(data, VAULT_LAYOUT_ID);"));
1661 }
1662
1663 #[test]
1664 fn ts_assert_layout_id_handles_short_buffer_check() {
1665 let m = test_manifest();
1666 let output = TsAccounts(&m).to_string();
1667 assert!(output.contains("if (data.length < HEADER_SIZE)"));
1671 assert!(output.contains("throw new Error"));
1672 }
1673
1674 #[test]
1675 fn ts_data_size_calculation() {
1676 static ARGS: &[ArgDescriptor] = &[
1677 ArgDescriptor {
1678 name: "amount",
1679 canonical_type: "u64",
1680 size: 8,
1681 },
1682 ArgDescriptor {
1683 name: "bump",
1684 canonical_type: "u8",
1685 size: 1,
1686 },
1687 ];
1688 let ix = InstructionDescriptor {
1689 name: "test",
1690 tag: 0,
1691 args: ARGS,
1692 accounts: &[],
1693 capabilities: &[],
1694 policy_pack: "",
1695 receipt_expected: false,
1696 };
1697 assert_eq!(instruction_data_size(&ix), 10);
1699 }
1700
1701 #[test]
1704 fn kt_accounts_generates_data_class() {
1705 let m = test_manifest();
1706 let output = KtAccounts(&m).to_string();
1707 assert!(output.contains("data class Vault("));
1708 assert!(output.contains("val authority: PublicKey"));
1709 assert!(output.contains("val amount: ULong"));
1710 assert!(output.contains("val isActive: Boolean"));
1711 }
1712
1713 #[test]
1714 fn kt_accounts_generates_decoder() {
1715 let m = test_manifest();
1716 let output = KtAccounts(&m).to_string();
1717 assert!(output.contains("fun decodeVault(data: ByteArray): Vault {"));
1718 assert!(output.contains("PublicKey(data.copyOfRange(16, 48))"));
1719 assert!(output.contains("ByteBuffer.wrap(data, 48, 8)"));
1720 assert!(output.contains("data[56] != 0.toByte()"));
1721 }
1722
1723 #[test]
1724 fn kt_accounts_generates_discriminator() {
1725 let m = test_manifest();
1726 let output = KtAccounts(&m).to_string();
1727 assert!(output.contains("const val VAULT_DISC: Byte = 1"));
1728 }
1729
1730 #[test]
1731 fn kt_accounts_emits_layout_id_constants_and_assertion_helpers() {
1732 let m = test_manifest();
1733 let output = KtAccounts(&m).to_string();
1734 assert!(output.contains("const val HEADER_SIZE: Int = 16"));
1736 assert!(output.contains("const val LAYOUT_ID_OFFSET: Int = 4"));
1737 assert!(output.contains("const val LAYOUT_ID_LENGTH: Int = 8"));
1738 assert!(output.contains("fun assertLayoutId(data: ByteArray, expectedHex: String) {"));
1739 assert!(output.contains("const val VAULT_LAYOUT_ID: String = \"aabbccdd11223344\""));
1741 assert!(output.contains("fun assertVaultLayout(data: ByteArray) {"));
1742 assert!(output.contains("assertLayoutId(data, VAULT_LAYOUT_ID)"));
1743 assert!(output
1746 .contains("fun decodeVault(data: ByteArray): Vault {\n assertVaultLayout(data)"));
1747 }
1748
1749 #[test]
1750 fn kt_instructions_generates_builder() {
1751 let m = test_manifest();
1752 let output = KtInstructions(&m).to_string();
1753 assert!(output.contains("data class DepositArgs("));
1754 assert!(output.contains("data class DepositAccounts("));
1755 assert!(output.contains("fun createDepositInstruction("));
1756 assert!(output.contains("data[0] = 0.toByte() // instruction discriminator"));
1757 }
1758
1759 #[test]
1760 fn kt_instructions_account_meta() {
1761 let m = test_manifest();
1762 let output = KtInstructions(&m).to_string();
1763 assert!(output.contains("isSigner = true, isWritable = false"));
1764 assert!(output.contains("isSigner = false, isWritable = true"));
1765 }
1766
1767 #[test]
1768 fn kt_events_generates_decoder() {
1769 let m = test_manifest();
1770 let output = KtEvents(&m).to_string();
1771 assert!(output.contains("data class DepositEventEvent("));
1772 assert!(output.contains("fun decodeDepositEventEvent(data: ByteArray)"));
1773 assert!(output.contains("DEPOSIT_EVENT_EVENT_DISC: Byte = 0"));
1774 }
1775
1776 #[test]
1777 fn kt_types_generates_header() {
1778 let m = test_manifest();
1779 let output = KtTypes(&m).to_string();
1780 assert!(output.contains("data class HopperHeader("));
1781 assert!(output.contains("fun decodeHeader(data: ByteArray): HopperHeader {"));
1782 assert!(output.contains(
1783 "flags = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort(),"
1784 ));
1785 assert!(output.contains("layoutId = data.copyOfRange(TYPES_LAYOUT_ID_OFFSET, TYPES_LAYOUT_ID_OFFSET + TYPES_LAYOUT_ID_LENGTH),"));
1786 assert!(output.contains("reserved = data.copyOfRange(12, 16)"));
1787 assert!(!output.contains("data.copyOfRange(2, 10)"));
1788 assert!(output.contains("VAULT: Byte = 1"));
1789 }
1790
1791 #[test]
1792 fn kt_full_client_gen_has_all_sections() {
1793 let m = test_manifest();
1794 let output = KtClientGen(&m).to_string();
1795 assert!(output.contains("=== Types.kt ==="));
1796 assert!(output.contains("=== Accounts.kt ==="));
1797 assert!(output.contains("=== Instructions.kt ==="));
1798 assert!(output.contains("=== Events.kt ==="));
1799 }
1800}