Skip to main content

hopper_schema/
clientgen.rs

1//! # Client Generation Module
2//!
3//! Generates typed client SDKs from Hopper schema types.
4//!
5//! The generator operates on the in-memory schema types (`ProgramManifest`,
6//! `LayoutManifest`, `InstructionDescriptor`, etc.) and produces source code
7//! via `core::fmt::Display` wrappers.
8//!
9//! ## Supported Languages
10//!
11//! - **TypeScript**: Full SDK with accounts, instructions, events, types.
12//! - **Kotlin**: Android SDK for Solana Mobile, using `@solana/web3.js` patterns
13//!   mapped to `com.solana.mobilewalletadapter` and `org.sol4k`.
14//!
15//! ## Usage
16//!
17//! ```text
18//! hopper client gen --ts @my-program.manifest.json
19//! hopper client gen --kt @my-program.manifest.json
20//! ```
21
22use core::fmt;
23
24extern crate alloc;
25
26use crate::{InstructionDescriptor, ProgramManifest};
27
28// ---------------------------------------------------------------------------
29// Type Mapping
30// ---------------------------------------------------------------------------
31
32/// Map a Hopper canonical type name to a TypeScript type.
33fn 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" // fallback for unknown types
44            }
45        }
46    }
47}
48
49/// PascalCase from snake_case or as-is.
50fn 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
67/// camelCase from snake_case or as-is.
68fn 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
91// ---------------------------------------------------------------------------
92// TypeScript Accounts
93// ---------------------------------------------------------------------------
94
95/// Generates `accounts.ts` content from a `ProgramManifest`.
96pub 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        // Header size constant
110        writeln!(f, "/** Hopper account header size in bytes. */")?;
111        writeln!(f, "export const HEADER_SIZE = 16;")?;
112        writeln!(f)?;
113        // Offset of the 8-byte LAYOUT_ID fingerprint within the
114        // Hopper header. Clients read bytes [4, 12) to assert the
115        // account matches the expected layout. See audit ST2 + the
116        // "winning architecture" design's client-side ABI guard.
117        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        // Generic layout-fingerprint verifier. Used by every
126        // per-layout `assertXxxLayout(data)` generated below.
127        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            // Interface
181            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            // Layout fingerprint constant (hex). Pairs with the
193            // runtime's `T::LAYOUT_ID` so the client and the program
194            // agree on the ABI byte-for-byte.
195            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            // Per-layout assertion helper. Thin wrapper over
205            // `assertLayoutId` that fills in the expected hex for
206            // convenience at call sites.
207            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            // Discriminator constant
217            write!(f, "export const ")?;
218            write_upper_snake(f, layout.name)?;
219            writeln!(f, "_DISC = {};", layout.disc)?;
220            writeln!(f)?;
221
222            // Decoder
223            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
269// ---------------------------------------------------------------------------
270// TypeScript Instructions
271// ---------------------------------------------------------------------------
272
273/// Generates `instructions.ts` content from a `ProgramManifest`.
274pub 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            // Args interface
292            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            // Accounts interface
306            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            // Builder function
318            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            // Build instruction data
333            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; // first byte is tag
339            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            // Build keys array
347            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
371// ---------------------------------------------------------------------------
372// TypeScript Events
373// ---------------------------------------------------------------------------
374
375/// Generates `events.ts` content from a `ProgramManifest`.
376pub 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            // Interface
396            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            // Discriminator constant
408            write!(f, "export const ")?;
409            write_upper_snake(f, event.name)?;
410            writeln!(f, "_EVENT_DISC = {};", event.tag)?;
411            writeln!(f)?;
412
413            // Decoder
414            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
450// ---------------------------------------------------------------------------
451// TypeScript Types
452// ---------------------------------------------------------------------------
453
454/// Generates `types.ts` content (shared type aliases).
455pub 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
524// ---------------------------------------------------------------------------
525// TypeScript Index
526// ---------------------------------------------------------------------------
527
528/// Generates `index.ts` content (re-exports).
529pub 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
548// ---------------------------------------------------------------------------
549// Full Client Generator
550// ---------------------------------------------------------------------------
551
552/// Generates all TypeScript client files for a program and writes them
553/// using newline separators with file markers.
554///
555/// Output format:
556/// ```text
557/// === accounts.ts ===
558/// <contents>
559/// === instructions.ts ===
560/// <contents>
561/// === events.ts ===
562/// <contents>
563/// === types.ts ===
564/// <contents>
565/// === index.ts ===
566/// <contents>
567/// ```
568pub 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
597// ---------------------------------------------------------------------------
598// Helpers
599// ---------------------------------------------------------------------------
600
601fn 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; // discriminator byte
726    for arg in ix.args.iter() {
727        size += arg.size as usize;
728    }
729    size
730}
731
732// ---------------------------------------------------------------------------
733// Kotlin Type Mapping
734// ---------------------------------------------------------------------------
735
736/// Map a Hopper canonical type name to a Kotlin type.
737fn 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
760/// Write PascalCase (used for Kotlin classes/interfaces).
761fn write_kt_pascal(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
762    write_pascal(f, name)
763}
764
765/// Write camelCase (used for Kotlin properties/functions).
766fn write_kt_camel(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
767    write_camel(f, name)
768}
769
770/// Write SCREAMING_SNAKE_CASE for constants.
771fn 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
910// ---------------------------------------------------------------------------
911// Kotlin Accounts
912// ---------------------------------------------------------------------------
913
914/// Generates Kotlin data classes and decoders for account layouts.
915pub 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        // Hopper header layout constants. The 16-byte header carries the
937        // 8-byte LAYOUT_ID at bytes [4, 12). Clients compare this against
938        // the compiled-in per-layout constant before decoding, which is
939        // the client-side complement to the runtime's `load::<T>()`
940        // fingerprint check.
941        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            // Data class
1006            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            // Layout fingerprint constant (hex). Pairs with the
1023            // runtime's `T::LAYOUT_ID` so the client and the program
1024            // agree on the ABI byte-for-byte.
1025            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            // Per-layout assertion helper. Thin wrapper over
1035            // `assertLayoutId` that fills in the expected hex.
1036            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            // Discriminator constant
1046            write!(f, "const val ")?;
1047            write_kt_const(f, layout.name)?;
1048            writeln!(f, "_DISC: Byte = {}", layout.disc)?;
1049            writeln!(f)?;
1050
1051            // Decoder
1052            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
1099// ---------------------------------------------------------------------------
1100// Kotlin Instructions
1101// ---------------------------------------------------------------------------
1102
1103/// Generates Kotlin instruction builders from a `ProgramManifest`.
1104pub 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            // Args data class
1129            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            // Accounts data class
1148            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            // Builder function
1165            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
1215// ---------------------------------------------------------------------------
1216// Kotlin Events
1217// ---------------------------------------------------------------------------
1218
1219/// Generates Kotlin event data classes and decoders.
1220pub 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
1307// ---------------------------------------------------------------------------
1308// Kotlin Types
1309// ---------------------------------------------------------------------------
1310
1311/// Generates Kotlin header type and discriminator constants.
1312pub 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
1382// ---------------------------------------------------------------------------
1383// Full Kotlin Client Generator
1384// ---------------------------------------------------------------------------
1385
1386/// Generates all Kotlin client files for a program.
1387///
1388/// Output format uses file markers like the TypeScript generator:
1389/// ```text
1390/// === Types.kt ===
1391/// <contents>
1392/// === Accounts.kt ===
1393/// <contents>
1394/// === Instructions.kt ===
1395/// <contents>
1396/// === Events.kt ===
1397/// <contents>
1398/// ```
1399pub 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// ---------------------------------------------------------------------------
1425// Tests
1426// ---------------------------------------------------------------------------
1427
1428#[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        // Generic helper and offset constants are always present.
1652        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        // Per-layout const + assertion with the real 16-hex-char id.
1658        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        // The guard explicitly rejects buffers shorter than the
1668        // header so callers cannot accidentally trust a truncated
1669        // slice.
1670        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        // 1 (disc) + 8 (u64) + 1 (u8) = 10
1698        assert_eq!(instruction_data_size(&ix), 10);
1699    }
1700
1701    // -- Kotlin generator tests --
1702
1703    #[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        // Generic helper and offset constants are always present.
1735        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        // Per-layout const + assertion with the real 16-hex-char id.
1740        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        // Decoder calls the assertion first so a mismatched account
1744        // fails with LayoutMismatchException instead of decoding garbage.
1745        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}