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, " schemaEpoch: number;")?;
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, " schemaEpoch: view.getUint32(12, true),")?;
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 schemaEpoch: UInt")?;
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!(
1362 f,
1363 " schemaEpoch = ByteBuffer.wrap(data, 12, 4).order(ByteOrder.LITTLE_ENDIAN).int.toUInt()"
1364 )?;
1365 writeln!(f, " )")?;
1366 writeln!(f, "}}")?;
1367 writeln!(f)?;
1368 writeln!(
1369 f,
1370 "/** All account discriminators for {} v{}. */",
1371 prog.name, prog.version
1372 )?;
1373 writeln!(f, "object Discriminators {{")?;
1374 for layout in prog.layouts.iter() {
1375 write!(f, " const val ")?;
1376 write_kt_const(f, layout.name)?;
1377 writeln!(f, ": Byte = {}", layout.disc)?;
1378 }
1379 writeln!(f, "}}")?;
1380
1381 Ok(())
1382 }
1383}
1384
1385pub struct KtClientGen<'a>(pub &'a ProgramManifest);
1403
1404impl<'a> fmt::Display for KtClientGen<'a> {
1405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1406 let prog = self.0;
1407
1408 writeln!(f, "=== Types.kt ===")?;
1409 write!(f, "{}", KtTypes(prog))?;
1410 writeln!(f)?;
1411
1412 writeln!(f, "=== Accounts.kt ===")?;
1413 write!(f, "{}", KtAccounts(prog))?;
1414 writeln!(f)?;
1415
1416 writeln!(f, "=== Instructions.kt ===")?;
1417 write!(f, "{}", KtInstructions(prog))?;
1418 writeln!(f)?;
1419
1420 writeln!(f, "=== Events.kt ===")?;
1421 write!(f, "{}", KtEvents(prog))?;
1422
1423 Ok(())
1424 }
1425}
1426
1427#[cfg(test)]
1432mod tests {
1433 extern crate alloc;
1434 use super::*;
1435 use crate::{
1436 AccountEntry, ArgDescriptor, EventDescriptor, FieldDescriptor, FieldIntent, LayoutManifest,
1437 };
1438 use alloc::string::ToString;
1439
1440 fn test_manifest() -> ProgramManifest {
1441 static FIELDS: &[FieldDescriptor] = &[
1442 FieldDescriptor {
1443 name: "authority",
1444 canonical_type: "Pubkey",
1445 size: 32,
1446 offset: 16,
1447 intent: FieldIntent::Custom,
1448 },
1449 FieldDescriptor {
1450 name: "amount",
1451 canonical_type: "u64",
1452 size: 8,
1453 offset: 48,
1454 intent: FieldIntent::Custom,
1455 },
1456 FieldDescriptor {
1457 name: "is_active",
1458 canonical_type: "bool",
1459 size: 1,
1460 offset: 56,
1461 intent: FieldIntent::Custom,
1462 },
1463 ];
1464
1465 static LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
1466 name: "vault",
1467 disc: 1,
1468 version: 1,
1469 layout_id: [0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44],
1470 total_size: 64,
1471 field_count: 3,
1472 fields: FIELDS,
1473 }];
1474
1475 static ARGS: &[ArgDescriptor] = &[ArgDescriptor {
1476 name: "amount",
1477 canonical_type: "u64",
1478 size: 8,
1479 }];
1480
1481 static ACCOUNTS: &[AccountEntry] = &[
1482 AccountEntry {
1483 name: "authority",
1484 writable: false,
1485 signer: true,
1486 layout_ref: "",
1487 },
1488 AccountEntry {
1489 name: "vault",
1490 writable: true,
1491 signer: false,
1492 layout_ref: "vault",
1493 },
1494 ];
1495
1496 static INSTRUCTIONS: &[InstructionDescriptor] = &[InstructionDescriptor {
1497 name: "deposit",
1498 tag: 0,
1499 args: ARGS,
1500 accounts: ACCOUNTS,
1501 capabilities: &["write"],
1502 policy_pack: "standard",
1503 receipt_expected: true,
1504 }];
1505
1506 static EVENT_FIELDS: &[FieldDescriptor] = &[
1507 FieldDescriptor {
1508 name: "depositor",
1509 canonical_type: "Pubkey",
1510 size: 32,
1511 offset: 0,
1512 intent: FieldIntent::Custom,
1513 },
1514 FieldDescriptor {
1515 name: "amount",
1516 canonical_type: "u64",
1517 size: 8,
1518 offset: 32,
1519 intent: FieldIntent::Custom,
1520 },
1521 ];
1522
1523 static EVENTS: &[EventDescriptor] = &[EventDescriptor {
1524 name: "deposit_event",
1525 tag: 0,
1526 fields: EVENT_FIELDS,
1527 }];
1528
1529 ProgramManifest {
1530 name: "test_vault",
1531 version: "0.1.0",
1532 description: "A test vault program",
1533 layouts: LAYOUTS,
1534 layout_metadata: &[],
1535 instructions: INSTRUCTIONS,
1536 events: EVENTS,
1537 policies: &[],
1538 compatibility_pairs: &[],
1539 tooling_hints: &[],
1540 contexts: &[],
1541 }
1542 }
1543
1544 #[test]
1545 fn ts_accounts_generates_interface() {
1546 let m = test_manifest();
1547 let output = TsAccounts(&m).to_string();
1548 assert!(output.contains("export interface Vault {"));
1549 assert!(output.contains("authority: PublicKey;"));
1550 assert!(output.contains("amount: bigint;"));
1551 assert!(output.contains("isActive: boolean;"));
1552 }
1553
1554 #[test]
1555 fn ts_accounts_generates_decoder() {
1556 let m = test_manifest();
1557 let output = TsAccounts(&m).to_string();
1558 assert!(output.contains("export function decodeVault(data: Uint8Array)"));
1559 assert!(output.contains("export function decodeVault(data: Uint8Array): \n Vault {\n assertVaultLayout(data);"));
1560 assert!(
1561 output.contains("throw new Error(`Data too small for vault: ${data.length} < 64`);")
1562 );
1563 assert!(output.contains("new PublicKey(data.slice(16, 48))"));
1564 assert!(output.contains("view.getBigUint64(48, true)"));
1565 assert!(output.contains("data[56] !== 0"));
1566 }
1567
1568 #[test]
1569 fn ts_accounts_generates_discriminator() {
1570 let m = test_manifest();
1571 let output = TsAccounts(&m).to_string();
1572 assert!(output.contains("export const VAULT_DISC = 1;"));
1573 }
1574
1575 #[test]
1576 fn ts_instructions_generates_builder() {
1577 let m = test_manifest();
1578 let output = TsInstructions(&m).to_string();
1579 assert!(output.contains("export interface DepositArgs {"));
1580 assert!(output.contains("export interface DepositAccounts {"));
1581 assert!(output.contains("export function createDepositInstruction("));
1582 assert!(output.contains("data[0] = 0; // instruction discriminator"));
1583 assert!(output.contains("view.setBigUint64(1, args.amount, true);"));
1584 }
1585
1586 #[test]
1587 fn ts_instructions_account_meta() {
1588 let m = test_manifest();
1589 let output = TsInstructions(&m).to_string();
1590 assert!(output.contains("accounts.authority, isSigner: true, isWritable: false"));
1591 assert!(output.contains("accounts.vault, isSigner: false, isWritable: true"));
1592 }
1593
1594 #[test]
1595 fn ts_events_generates_decoder() {
1596 let m = test_manifest();
1597 let output = TsEvents(&m).to_string();
1598 assert!(output.contains("export interface DepositEventEvent {"));
1599 assert!(output.contains("export function decodeDepositEventEvent(data: Uint8Array)"));
1600 assert!(output.contains("DEPOSIT_EVENT_EVENT_DISC = 0;"));
1601 }
1602
1603 #[test]
1604 fn ts_types_generates_header() {
1605 let m = test_manifest();
1606 let output = TsTypes(&m).to_string();
1607 assert!(output.contains("export interface HopperHeader {"));
1608 assert!(output.contains("export function decodeHeader(data: Uint8Array)"));
1609 assert!(output.contains("flags: view.getUint16(2, true),"));
1610 assert!(output.contains(
1611 "layoutId: data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH),"
1612 ));
1613 assert!(output.contains("schemaEpoch: view.getUint32(12, true),"));
1614 assert!(output.contains("Vault: 1,"));
1615 }
1616
1617 #[test]
1618 fn ts_decode_header_and_assert_layout_id_share_offset() {
1619 let m = test_manifest();
1620 let accounts = TsAccounts(&m).to_string();
1621 let types = TsTypes(&m).to_string();
1622 assert!(accounts.contains("export const LAYOUT_ID_OFFSET = 4;"));
1623 assert!(accounts.contains("data[LAYOUT_ID_OFFSET + i]"));
1624 assert!(types.contains("const LAYOUT_ID_OFFSET = 4;"));
1625 assert!(types.contains("data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH)"));
1626 assert!(!types.contains("data.slice(2, 10)"));
1627 }
1628
1629 #[test]
1630 fn ts_index_reexports_all() {
1631 let m = test_manifest();
1632 let output = TsIndex(&m).to_string();
1633 assert!(output.contains("export * from \"./types\";"));
1634 assert!(output.contains("export * from \"./accounts\";"));
1635 assert!(output.contains("export * from \"./instructions\";"));
1636 assert!(output.contains("export * from \"./events\";"));
1637 }
1638
1639 #[test]
1640 fn ts_full_client_gen_has_all_sections() {
1641 let m = test_manifest();
1642 let output = TsClientGen(&m).to_string();
1643 assert!(output.contains("=== types.ts ==="));
1644 assert!(output.contains("=== accounts.ts ==="));
1645 assert!(output.contains("=== instructions.ts ==="));
1646 assert!(output.contains("=== events.ts ==="));
1647 assert!(output.contains("=== index.ts ==="));
1648 }
1649
1650 #[test]
1651 fn ts_accounts_emits_layout_id_constants_and_assertion_helpers() {
1652 let m = test_manifest();
1653 let output = TsAccounts(&m).to_string();
1654 assert!(output.contains("export const LAYOUT_ID_OFFSET = 4;"));
1656 assert!(output.contains("export const LAYOUT_ID_LENGTH = 8;"));
1657 assert!(output.contains(
1658 "export function assertLayoutId(data: Uint8Array, expectedHex: string): void"
1659 ));
1660 assert!(output.contains("export const VAULT_LAYOUT_ID = \"aabbccdd11223344\";"));
1662 assert!(output.contains("export function assertVaultLayout(data: Uint8Array): void"));
1663 assert!(output.contains("assertLayoutId(data, VAULT_LAYOUT_ID);"));
1664 }
1665
1666 #[test]
1667 fn ts_assert_layout_id_handles_short_buffer_check() {
1668 let m = test_manifest();
1669 let output = TsAccounts(&m).to_string();
1670 assert!(output.contains("if (data.length < HEADER_SIZE)"));
1674 assert!(output.contains("throw new Error"));
1675 }
1676
1677 #[test]
1678 fn ts_data_size_calculation() {
1679 static ARGS: &[ArgDescriptor] = &[
1680 ArgDescriptor {
1681 name: "amount",
1682 canonical_type: "u64",
1683 size: 8,
1684 },
1685 ArgDescriptor {
1686 name: "bump",
1687 canonical_type: "u8",
1688 size: 1,
1689 },
1690 ];
1691 let ix = InstructionDescriptor {
1692 name: "test",
1693 tag: 0,
1694 args: ARGS,
1695 accounts: &[],
1696 capabilities: &[],
1697 policy_pack: "",
1698 receipt_expected: false,
1699 };
1700 assert_eq!(instruction_data_size(&ix), 10);
1702 }
1703
1704 #[test]
1707 fn kt_accounts_generates_data_class() {
1708 let m = test_manifest();
1709 let output = KtAccounts(&m).to_string();
1710 assert!(output.contains("data class Vault("));
1711 assert!(output.contains("val authority: PublicKey"));
1712 assert!(output.contains("val amount: ULong"));
1713 assert!(output.contains("val isActive: Boolean"));
1714 }
1715
1716 #[test]
1717 fn kt_accounts_generates_decoder() {
1718 let m = test_manifest();
1719 let output = KtAccounts(&m).to_string();
1720 assert!(output.contains("fun decodeVault(data: ByteArray): Vault {"));
1721 assert!(output.contains("PublicKey(data.copyOfRange(16, 48))"));
1722 assert!(output.contains("ByteBuffer.wrap(data, 48, 8)"));
1723 assert!(output.contains("data[56] != 0.toByte()"));
1724 }
1725
1726 #[test]
1727 fn kt_accounts_generates_discriminator() {
1728 let m = test_manifest();
1729 let output = KtAccounts(&m).to_string();
1730 assert!(output.contains("const val VAULT_DISC: Byte = 1"));
1731 }
1732
1733 #[test]
1734 fn kt_accounts_emits_layout_id_constants_and_assertion_helpers() {
1735 let m = test_manifest();
1736 let output = KtAccounts(&m).to_string();
1737 assert!(output.contains("const val HEADER_SIZE: Int = 16"));
1739 assert!(output.contains("const val LAYOUT_ID_OFFSET: Int = 4"));
1740 assert!(output.contains("const val LAYOUT_ID_LENGTH: Int = 8"));
1741 assert!(output.contains("fun assertLayoutId(data: ByteArray, expectedHex: String) {"));
1742 assert!(output.contains("const val VAULT_LAYOUT_ID: String = \"aabbccdd11223344\""));
1744 assert!(output.contains("fun assertVaultLayout(data: ByteArray) {"));
1745 assert!(output.contains("assertLayoutId(data, VAULT_LAYOUT_ID)"));
1746 assert!(output
1749 .contains("fun decodeVault(data: ByteArray): Vault {\n assertVaultLayout(data)"));
1750 }
1751
1752 #[test]
1753 fn kt_instructions_generates_builder() {
1754 let m = test_manifest();
1755 let output = KtInstructions(&m).to_string();
1756 assert!(output.contains("data class DepositArgs("));
1757 assert!(output.contains("data class DepositAccounts("));
1758 assert!(output.contains("fun createDepositInstruction("));
1759 assert!(output.contains("data[0] = 0.toByte() // instruction discriminator"));
1760 }
1761
1762 #[test]
1763 fn kt_instructions_account_meta() {
1764 let m = test_manifest();
1765 let output = KtInstructions(&m).to_string();
1766 assert!(output.contains("isSigner = true, isWritable = false"));
1767 assert!(output.contains("isSigner = false, isWritable = true"));
1768 }
1769
1770 #[test]
1771 fn kt_events_generates_decoder() {
1772 let m = test_manifest();
1773 let output = KtEvents(&m).to_string();
1774 assert!(output.contains("data class DepositEventEvent("));
1775 assert!(output.contains("fun decodeDepositEventEvent(data: ByteArray)"));
1776 assert!(output.contains("DEPOSIT_EVENT_EVENT_DISC: Byte = 0"));
1777 }
1778
1779 #[test]
1780 fn kt_types_generates_header() {
1781 let m = test_manifest();
1782 let output = KtTypes(&m).to_string();
1783 assert!(output.contains("data class HopperHeader("));
1784 assert!(output.contains("fun decodeHeader(data: ByteArray): HopperHeader {"));
1785 assert!(output.contains(
1786 "flags = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort(),"
1787 ));
1788 assert!(output.contains("layoutId = data.copyOfRange(TYPES_LAYOUT_ID_OFFSET, TYPES_LAYOUT_ID_OFFSET + TYPES_LAYOUT_ID_LENGTH),"));
1789 assert!(output.contains("schemaEpoch = ByteBuffer.wrap(data, 12, 4).order(ByteOrder.LITTLE_ENDIAN).int.toUInt()"));
1790 assert!(!output.contains("data.copyOfRange(2, 10)"));
1791 assert!(output.contains("VAULT: Byte = 1"));
1792 }
1793
1794 #[test]
1795 fn kt_full_client_gen_has_all_sections() {
1796 let m = test_manifest();
1797 let output = KtClientGen(&m).to_string();
1798 assert!(output.contains("=== Types.kt ==="));
1799 assert!(output.contains("=== Accounts.kt ==="));
1800 assert!(output.contains("=== Instructions.kt ==="));
1801 assert!(output.contains("=== Events.kt ==="));
1802 }
1803}