use core::fmt;
extern crate alloc;
use crate::{InstructionDescriptor, ProgramManifest};
fn ts_type(canonical: &str) -> &str {
match canonical {
"u8" | "u16" | "u32" | "i8" | "i16" | "i32" => "number",
"u64" | "u128" | "i64" | "i128" => "bigint",
"bool" => "boolean",
"Pubkey" => "PublicKey",
_ => {
if canonical.starts_with("[u8;") {
"Uint8Array"
} else {
"Uint8Array" }
}
}
}
fn write_pascal(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
let mut capitalize_next = true;
for c in name.chars() {
if c == '_' || c == '-' {
capitalize_next = true;
} else if capitalize_next {
for uc in c.to_uppercase() {
write!(f, "{}", uc)?;
}
capitalize_next = false;
} else {
write!(f, "{}", c)?;
}
}
Ok(())
}
fn write_camel(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
let mut capitalize_next = false;
let mut first = true;
for c in name.chars() {
if c == '_' || c == '-' {
capitalize_next = true;
} else if capitalize_next {
for uc in c.to_uppercase() {
write!(f, "{}", uc)?;
}
capitalize_next = false;
} else if first {
for lc in c.to_lowercase() {
write!(f, "{}", lc)?;
}
first = false;
} else {
write!(f, "{}", c)?;
}
}
Ok(())
}
pub struct TsAccounts<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for TsAccounts<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --ts")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(f, "import {{ PublicKey }} from \"@solana/web3.js\";")?;
writeln!(f)?;
writeln!(f, "/** Hopper account header size in bytes. */")?;
writeln!(f, "export const HEADER_SIZE = 16;")?;
writeln!(f)?;
writeln!(
f,
"/** Byte offset of the 8-byte layout fingerprint in a Hopper account header. */"
)?;
writeln!(f, "export const LAYOUT_ID_OFFSET = 4;")?;
writeln!(f, "/** Byte length of the layout fingerprint. */")?;
writeln!(f, "export const LAYOUT_ID_LENGTH = 8;")?;
writeln!(f)?;
writeln!(f, "/**")?;
writeln!(
f,
" * Raise if `data` is not a Hopper account encoding the expected layout."
)?;
writeln!(f, " *")?;
writeln!(
f,
" * Reads the 8-byte LAYOUT_ID fingerprint from the 16-byte Hopper header"
)?;
writeln!(
f,
" * (bytes 4..12) and compares it against `expectedHex` (16 lowercase hex chars)."
)?;
writeln!(
f,
" * This is the client-side complement to the runtime check `load::<T>()` runs"
)?;
writeln!(
f,
" * before handing out a typed Ref. Mismatch means the on-chain program was"
)?;
writeln!(
f,
" * upgraded with a different ABI than the client was generated against."
)?;
writeln!(f, " */")?;
writeln!(
f,
"export function assertLayoutId(data: Uint8Array, expectedHex: string): void {{"
)?;
writeln!(f, " if (data.length < HEADER_SIZE) {{")?;
writeln!(
f,
" throw new Error(`Hopper account too short: ${{data.length}} < ${{HEADER_SIZE}}`);"
)?;
writeln!(f, " }}")?;
writeln!(f, " let actualHex = \"\";")?;
writeln!(f, " for (let i = 0; i < LAYOUT_ID_LENGTH; i++) {{")?;
writeln!(
f,
" actualHex += data[LAYOUT_ID_OFFSET + i].toString(16).padStart(2, \"0\");"
)?;
writeln!(f, " }}")?;
writeln!(f, " if (actualHex !== expectedHex.toLowerCase()) {{")?;
writeln!(f, " throw new Error(")?;
writeln!(f, " `Hopper layout mismatch: account header reports ${{actualHex}}, expected ${{expectedHex}}`,")?;
writeln!(f, " );")?;
writeln!(f, " }}")?;
writeln!(f, "}}")?;
writeln!(f)?;
for layout in prog.layouts.iter() {
write!(f, "export interface ")?;
write_pascal(f, layout.name)?;
writeln!(f, " {{")?;
for field in layout.fields.iter() {
write!(f, " ")?;
write_camel(f, field.name)?;
writeln!(f, ": {};", ts_type(field.canonical_type))?;
}
writeln!(f, "}}")?;
writeln!(f)?;
write!(f, "export const ")?;
write_upper_snake(f, layout.name)?;
write!(f, "_LAYOUT_ID = \"")?;
for b in layout.layout_id.iter() {
write!(f, "{:02x}", b)?;
}
writeln!(f, "\";")?;
writeln!(f)?;
write!(f, "export function assert")?;
write_pascal(f, layout.name)?;
writeln!(f, "Layout(data: Uint8Array): void {{")?;
write!(f, " assertLayoutId(data, ")?;
write_upper_snake(f, layout.name)?;
writeln!(f, "_LAYOUT_ID);")?;
writeln!(f, "}}")?;
writeln!(f)?;
write!(f, "export const ")?;
write_upper_snake(f, layout.name)?;
writeln!(f, "_DISC = {};", layout.disc)?;
writeln!(f)?;
write!(f, "export function decode")?;
write_pascal(f, layout.name)?;
writeln!(f, "(data: Uint8Array): ")?;
write!(f, " ")?;
write_pascal(f, layout.name)?;
writeln!(f, " {{")?;
write!(f, " assert")?;
write_pascal(f, layout.name)?;
writeln!(f, "Layout(data);")?;
writeln!(f, " if (data.length < {}) {{", layout.total_size)?;
writeln!(
f,
" throw new Error(`Data too small for {}: ${{data.length}} < {}`);",
layout.name, layout.total_size
)?;
writeln!(f, " }}")?;
writeln!(
f,
" const view = new DataView(data.buffer, data.byteOffset, data.byteLength);"
)?;
for field in layout.fields.iter() {
let offset = field.offset as usize;
let end = offset + field.size as usize;
write!(f, " const ")?;
write_camel(f, field.name)?;
write!(f, " = ")?;
write_decode_expr(f, field.canonical_type, offset, end)?;
writeln!(f, ";")?;
}
writeln!(f, " return {{")?;
for field in layout.fields.iter() {
write!(f, " ")?;
write_camel(f, field.name)?;
writeln!(f, ",")?;
}
writeln!(f, " }};")?;
writeln!(f, "}}")?;
writeln!(f)?;
}
Ok(())
}
}
pub struct TsInstructions<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for TsInstructions<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --ts")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(
f,
"import {{ PublicKey, TransactionInstruction }} from \"@solana/web3.js\";"
)?;
writeln!(f)?;
for ix in prog.instructions.iter() {
if !ix.args.is_empty() {
write!(f, "export interface ")?;
write_pascal(f, ix.name)?;
writeln!(f, "Args {{")?;
for arg in ix.args.iter() {
write!(f, " ")?;
write_camel(f, arg.name)?;
writeln!(f, ": {};", ts_type(arg.canonical_type))?;
}
writeln!(f, "}}")?;
writeln!(f)?;
}
write!(f, "export interface ")?;
write_pascal(f, ix.name)?;
writeln!(f, "Accounts {{")?;
for acc in ix.accounts.iter() {
write!(f, " ")?;
write_camel(f, acc.name)?;
writeln!(f, ": PublicKey;")?;
}
writeln!(f, "}}")?;
writeln!(f)?;
write!(f, "export function create")?;
write_pascal(f, ix.name)?;
writeln!(f, "Instruction(")?;
if !ix.args.is_empty() {
write!(f, " args: ")?;
write_pascal(f, ix.name)?;
writeln!(f, "Args,")?;
}
write!(f, " accounts: ")?;
write_pascal(f, ix.name)?;
writeln!(f, "Accounts,")?;
writeln!(f, " programId: PublicKey,")?;
writeln!(f, "): TransactionInstruction {{")?;
let data_size = instruction_data_size(ix);
writeln!(f, " const data = new Uint8Array({});", data_size)?;
writeln!(f, " const view = new DataView(data.buffer);")?;
writeln!(f, " data[0] = {}; // instruction discriminator", ix.tag)?;
let mut offset = 1usize; for arg in ix.args.iter() {
write_encode_expr(f, arg.canonical_type, arg.name, offset)?;
offset += arg.size as usize;
}
writeln!(f)?;
writeln!(f, " const keys = [")?;
for acc in ix.accounts.iter() {
write!(f, " {{ pubkey: accounts.")?;
write_camel(f, acc.name)?;
writeln!(
f,
", isSigner: {}, isWritable: {} }},",
acc.signer, acc.writable
)?;
}
writeln!(f, " ];")?;
writeln!(f)?;
writeln!(
f,
" return new TransactionInstruction({{ keys, programId, data }});"
)?;
writeln!(f, "}}")?;
writeln!(f)?;
}
Ok(())
}
}
pub struct TsEvents<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for TsEvents<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --ts")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(f, "import {{ PublicKey }} from \"@solana/web3.js\";")?;
writeln!(f)?;
if prog.events.is_empty() {
writeln!(f, "// No events defined for this program.")?;
return Ok(());
}
for event in prog.events.iter() {
write!(f, "export interface ")?;
write_pascal(f, event.name)?;
writeln!(f, "Event {{")?;
for field in event.fields.iter() {
write!(f, " ")?;
write_camel(f, field.name)?;
writeln!(f, ": {};", ts_type(field.canonical_type))?;
}
writeln!(f, "}}")?;
writeln!(f)?;
write!(f, "export const ")?;
write_upper_snake(f, event.name)?;
writeln!(f, "_EVENT_DISC = {};", event.tag)?;
writeln!(f)?;
write!(f, "export function decode")?;
write_pascal(f, event.name)?;
writeln!(f, "Event(data: Uint8Array): ")?;
write!(f, " ")?;
write_pascal(f, event.name)?;
writeln!(f, "Event {{")?;
writeln!(
f,
" const view = new DataView(data.buffer, data.byteOffset, data.byteLength);"
)?;
for field in event.fields.iter() {
let offset = field.offset as usize;
let end = offset + field.size as usize;
write!(f, " const ")?;
write_camel(f, field.name)?;
write!(f, " = ")?;
write_decode_expr(f, field.canonical_type, offset, end)?;
writeln!(f, ";")?;
}
writeln!(f, " return {{")?;
for field in event.fields.iter() {
write!(f, " ")?;
write_camel(f, field.name)?;
writeln!(f, ",")?;
}
writeln!(f, " }};")?;
writeln!(f, "}}")?;
writeln!(f)?;
}
Ok(())
}
}
pub struct TsTypes<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for TsTypes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --ts")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(f, "import {{ PublicKey }} from \"@solana/web3.js\";")?;
writeln!(f)?;
writeln!(f, "/** Hopper account header (16 bytes). */")?;
writeln!(f, "export interface HopperHeader {{")?;
writeln!(f, " disc: number;")?;
writeln!(f, " version: number;")?;
writeln!(f, " flags: number;")?;
writeln!(f, " layoutId: Uint8Array;")?;
writeln!(f, " reserved: Uint8Array;")?;
writeln!(f, "}}")?;
writeln!(f)?;
writeln!(f, "const HEADER_SIZE = 16;")?;
writeln!(f, "const LAYOUT_ID_OFFSET = 4;")?;
writeln!(f, "const LAYOUT_ID_LENGTH = 8;")?;
writeln!(f)?;
writeln!(f, "/** Decode the Hopper 16-byte account header. */")?;
writeln!(
f,
"export function decodeHeader(data: Uint8Array): HopperHeader {{"
)?;
writeln!(f, " if (data.length < HEADER_SIZE) {{")?;
writeln!(
f,
" throw new Error(`Hopper account too short: ${{data.length}} < ${{HEADER_SIZE}}`);"
)?;
writeln!(f, " }}")?;
writeln!(
f,
" const view = new DataView(data.buffer, data.byteOffset, data.byteLength);"
)?;
writeln!(f, " return {{")?;
writeln!(f, " disc: data[0],")?;
writeln!(f, " version: data[1],")?;
writeln!(f, " flags: view.getUint16(2, true),")?;
writeln!(
f,
" layoutId: data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH),"
)?;
writeln!(f, " reserved: data.slice(12, 16),")?;
writeln!(f, " }};")?;
writeln!(f, "}}")?;
writeln!(f)?;
writeln!(
f,
"/** All account discriminators for {} v{}. */",
prog.name, prog.version
)?;
writeln!(f, "export const Discriminators = {{")?;
for layout in prog.layouts.iter() {
write!(f, " ")?;
write_pascal(f, layout.name)?;
writeln!(f, ": {},", layout.disc)?;
}
writeln!(f, "}} as const;")?;
Ok(())
}
}
pub struct TsIndex<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for TsIndex<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --ts")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(f, "export * from \"./types\";")?;
writeln!(f, "export * from \"./accounts\";")?;
writeln!(f, "export * from \"./instructions\";")?;
writeln!(f, "export * from \"./events\";")?;
Ok(())
}
}
pub struct TsClientGen<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for TsClientGen<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "=== types.ts ===")?;
write!(f, "{}", TsTypes(prog))?;
writeln!(f)?;
writeln!(f, "=== accounts.ts ===")?;
write!(f, "{}", TsAccounts(prog))?;
writeln!(f)?;
writeln!(f, "=== instructions.ts ===")?;
write!(f, "{}", TsInstructions(prog))?;
writeln!(f)?;
writeln!(f, "=== events.ts ===")?;
write!(f, "{}", TsEvents(prog))?;
writeln!(f)?;
writeln!(f, "=== index.ts ===")?;
write!(f, "{}", TsIndex(prog))?;
Ok(())
}
}
fn write_upper_snake(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
for c in name.chars() {
if c == '-' || c == ' ' {
write!(f, "_")?;
} else {
for uc in c.to_uppercase() {
write!(f, "{}", uc)?;
}
}
}
Ok(())
}
fn write_decode_expr(
f: &mut fmt::Formatter<'_>,
canonical: &str,
offset: usize,
end: usize,
) -> fmt::Result {
match canonical {
"u8" => write!(f, "data[{}]", offset),
"i8" => write!(f, "view.getInt8({})", offset),
"u16" => write!(f, "view.getUint16({}, true)", offset),
"i16" => write!(f, "view.getInt16({}, true)", offset),
"u32" => write!(f, "view.getUint32({}, true)", offset),
"i32" => write!(f, "view.getInt32({}, true)", offset),
"u64" => write!(f, "view.getBigUint64({}, true)", offset),
"i64" => write!(f, "view.getBigInt64({}, true)", offset),
"u128" => {
write!(
f,
"view.getBigUint64({}, true) | (view.getBigUint64({}, true) << 64n)",
offset,
offset + 8
)
}
"i128" => {
write!(
f,
"view.getBigInt64({}, true) | (view.getBigUint64({}, true) << 64n)",
offset + 8,
offset
)
}
"bool" => write!(f, "data[{}] !== 0", offset),
"Pubkey" => write!(f, "new PublicKey(data.slice({}, {}))", offset, end),
_ => write!(f, "data.slice({}, {})", offset, end),
}
}
fn write_encode_expr(
f: &mut fmt::Formatter<'_>,
canonical: &str,
name: &str,
offset: usize,
) -> fmt::Result {
match canonical {
"u8" => {
write!(f, " data[{}] = args.", offset)?;
write_camel(f, name)?;
writeln!(f, ";")
}
"i8" => {
write!(f, " view.setInt8({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ");")
}
"u16" => {
write!(f, " view.setUint16({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ", true);")
}
"i16" => {
write!(f, " view.setInt16({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ", true);")
}
"u32" => {
write!(f, " view.setUint32({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ", true);")
}
"i32" => {
write!(f, " view.setInt32({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ", true);")
}
"u64" => {
write!(f, " view.setBigUint64({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ", true);")
}
"i64" => {
write!(f, " view.setBigInt64({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, ", true);")
}
"u128" => {
write!(f, " view.setBigUint64({}, args.", offset)?;
write_camel(f, name)?;
writeln!(f, " & 0xFFFFFFFFFFFFFFFFn, true);")?;
write!(f, " view.setBigUint64({}, args.", offset + 8)?;
write_camel(f, name)?;
writeln!(f, " >> 64n, true);")
}
"bool" => {
write!(f, " data[{}] = args.", offset)?;
write_camel(f, name)?;
writeln!(f, " ? 1 : 0;")
}
"Pubkey" => {
write!(f, " data.set(args.")?;
write_camel(f, name)?;
writeln!(f, ".toBytes(), {});", offset)
}
_ => {
write!(f, " data.set(args.")?;
write_camel(f, name)?;
writeln!(f, ", {});", offset)
}
}
}
fn instruction_data_size(ix: &InstructionDescriptor) -> usize {
let mut size = 1usize; for arg in ix.args.iter() {
size += arg.size as usize;
}
size
}
fn kt_type(canonical: &str) -> &str {
match canonical {
"u8" => "UByte",
"i8" => "Byte",
"u16" => "UShort",
"i16" => "Short",
"u32" => "UInt",
"i32" => "Int",
"u64" => "ULong",
"i64" => "Long",
"u128" | "i128" => "ByteArray",
"bool" => "Boolean",
"Pubkey" => "PublicKey",
_ => {
if canonical.starts_with("[u8;") {
"ByteArray"
} else {
"ByteArray"
}
}
}
}
fn write_kt_pascal(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
write_pascal(f, name)
}
fn write_kt_camel(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
write_camel(f, name)
}
fn write_kt_const(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
write_upper_snake(f, name)
}
fn write_kt_decode_expr(
f: &mut fmt::Formatter<'_>,
canonical: &str,
offset: usize,
end: usize,
) -> fmt::Result {
match canonical {
"u8" => write!(f, "data[{}].toUByte()", offset),
"i8" => write!(f, "data[{}]", offset),
"u16" => write!(
f,
"ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort()",
offset
),
"i16" => write!(
f,
"ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).short",
offset
),
"u32" => write!(
f,
"ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).int.toUInt()",
offset
),
"i32" => write!(
f,
"ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).int",
offset
),
"u64" => write!(
f,
"ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).long.toULong()",
offset
),
"i64" => write!(
f,
"ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).long",
offset
),
"u128" | "i128" => write!(f, "data.copyOfRange({}, {})", offset, end),
"bool" => write!(f, "data[{}] != 0.toByte()", offset),
"Pubkey" => write!(f, "PublicKey(data.copyOfRange({}, {}))", offset, end),
_ => write!(f, "data.copyOfRange({}, {})", offset, end),
}
}
fn write_kt_encode_expr(
f: &mut fmt::Formatter<'_>,
canonical: &str,
name: &str,
offset: usize,
) -> fmt::Result {
match canonical {
"u8" => {
write!(f, " data[{}] = args.", offset)?;
write_kt_camel(f, name)?;
writeln!(f, ".toByte()")
}
"i8" => {
write!(f, " data[{}] = args.", offset)?;
write_kt_camel(f, name)?;
writeln!(f, "")
}
"u16" => {
write!(
f,
" ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(args.",
offset
)?;
write_kt_camel(f, name)?;
writeln!(f, ".toShort())")
}
"i16" => {
write!(
f,
" ByteBuffer.wrap(data, {}, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(args.",
offset
)?;
write_kt_camel(f, name)?;
writeln!(f, ")")
}
"u32" => {
write!(
f,
" ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(args.",
offset
)?;
write_kt_camel(f, name)?;
writeln!(f, ".toInt())")
}
"i32" => {
write!(
f,
" ByteBuffer.wrap(data, {}, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(args.",
offset
)?;
write_kt_camel(f, name)?;
writeln!(f, ")")
}
"u64" => {
write!(
f,
" ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).putLong(args.",
offset
)?;
write_kt_camel(f, name)?;
writeln!(f, ".toLong())")
}
"i64" => {
write!(
f,
" ByteBuffer.wrap(data, {}, 8).order(ByteOrder.LITTLE_ENDIAN).putLong(args.",
offset
)?;
write_kt_camel(f, name)?;
writeln!(f, ")")
}
"bool" => {
write!(f, " data[{}] = if (args.", offset)?;
write_kt_camel(f, name)?;
writeln!(f, ") 1.toByte() else 0.toByte()")
}
"Pubkey" => {
write!(f, " args.")?;
write_kt_camel(f, name)?;
writeln!(f, ".toByteArray().copyInto(data, {})", offset)
}
_ => {
write!(f, " args.")?;
write_kt_camel(f, name)?;
writeln!(f, ".copyInto(data, {})", offset)
}
}
}
pub struct KtAccounts<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for KtAccounts<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --kt")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(
f,
"package hopper.generated.{}",
prog.name.replace('-', "_")
)?;
writeln!(f)?;
writeln!(f, "import org.sol4k.PublicKey")?;
writeln!(f, "import java.nio.ByteBuffer")?;
writeln!(f, "import java.nio.ByteOrder")?;
writeln!(f)?;
writeln!(f, "/** Hopper account header size in bytes. */")?;
writeln!(f, "const val HEADER_SIZE: Int = 16")?;
writeln!(
f,
"/** Byte offset of the 8-byte layout fingerprint in a Hopper header. */"
)?;
writeln!(f, "const val LAYOUT_ID_OFFSET: Int = 4")?;
writeln!(f, "/** Byte length of the layout fingerprint. */")?;
writeln!(f, "const val LAYOUT_ID_LENGTH: Int = 8")?;
writeln!(f)?;
writeln!(
f,
"class LayoutMismatchException(expected: String, actual: String) :"
)?;
writeln!(f, " RuntimeException(\"Hopper layout mismatch: account header reports $actual, expected $expected\")")?;
writeln!(f)?;
writeln!(f, "/**")?;
writeln!(
f,
" * Raise if `data` is not a Hopper account encoding the expected layout."
)?;
writeln!(f, " *")?;
writeln!(
f,
" * Reads the 8-byte LAYOUT_ID fingerprint from the 16-byte Hopper header"
)?;
writeln!(
f,
" * (bytes 4..12) and compares it against `expectedHex` (16 lowercase hex chars)."
)?;
writeln!(
f,
" * Mismatch means the on-chain program was upgraded with a different ABI"
)?;
writeln!(f, " * than the client was generated against.")?;
writeln!(f, " */")?;
writeln!(
f,
"fun assertLayoutId(data: ByteArray, expectedHex: String) {{"
)?;
writeln!(f, " if (data.size < HEADER_SIZE) {{")?;
writeln!(f, " throw RuntimeException(\"Hopper account too short: ${{data.size}} < $HEADER_SIZE\")")?;
writeln!(f, " }}")?;
writeln!(f, " val sb = StringBuilder(LAYOUT_ID_LENGTH * 2)")?;
writeln!(f, " for (i in 0 until LAYOUT_ID_LENGTH) {{")?;
writeln!(
f,
" val byte = data[LAYOUT_ID_OFFSET + i].toInt() and 0xFF"
)?;
writeln!(f, " sb.append(String.format(\"%02x\", byte))")?;
writeln!(f, " }}")?;
writeln!(f, " val actualHex = sb.toString()")?;
writeln!(f, " if (actualHex != expectedHex.lowercase()) {{")?;
writeln!(
f,
" throw LayoutMismatchException(expectedHex, actualHex)"
)?;
writeln!(f, " }}")?;
writeln!(f, "}}")?;
writeln!(f)?;
for layout in prog.layouts.iter() {
write!(f, "data class ")?;
write_kt_pascal(f, layout.name)?;
writeln!(f, "(")?;
for (i, field) in layout.fields.iter().enumerate() {
write!(f, " val ")?;
write_kt_camel(f, field.name)?;
write!(f, ": {}", kt_type(field.canonical_type))?;
if i + 1 < layout.fields.len() {
writeln!(f, ",")?;
} else {
writeln!(f)?;
}
}
writeln!(f, ")")?;
writeln!(f)?;
write!(f, "const val ")?;
write_kt_const(f, layout.name)?;
write!(f, "_LAYOUT_ID: String = \"")?;
for b in layout.layout_id.iter() {
write!(f, "{:02x}", b)?;
}
writeln!(f, "\"")?;
writeln!(f)?;
write!(f, "fun assert")?;
write_kt_pascal(f, layout.name)?;
writeln!(f, "Layout(data: ByteArray) {{")?;
write!(f, " assertLayoutId(data, ")?;
write_kt_const(f, layout.name)?;
writeln!(f, "_LAYOUT_ID)")?;
writeln!(f, "}}")?;
writeln!(f)?;
write!(f, "const val ")?;
write_kt_const(f, layout.name)?;
writeln!(f, "_DISC: Byte = {}", layout.disc)?;
writeln!(f)?;
write!(f, "fun decode")?;
write_kt_pascal(f, layout.name)?;
write!(f, "(data: ByteArray): ")?;
write_kt_pascal(f, layout.name)?;
writeln!(f, " {{")?;
write!(f, " assert")?;
write_kt_pascal(f, layout.name)?;
writeln!(f, "Layout(data)")?;
writeln!(
f,
" require(data.size >= {}) {{ \"Data too small for {}\" }}",
layout.total_size, layout.name
)?;
for field in layout.fields.iter() {
let offset = field.offset as usize;
let end = offset + field.size as usize;
write!(f, " val ")?;
write_kt_camel(f, field.name)?;
write!(f, " = ")?;
write_kt_decode_expr(f, field.canonical_type, offset, end)?;
writeln!(f)?;
}
write!(f, " return ")?;
write_kt_pascal(f, layout.name)?;
writeln!(f, "(")?;
for (i, field) in layout.fields.iter().enumerate() {
write!(f, " ")?;
write_kt_camel(f, field.name)?;
write!(f, " = ")?;
write_kt_camel(f, field.name)?;
if i + 1 < layout.fields.len() {
writeln!(f, ",")?;
} else {
writeln!(f)?;
}
}
writeln!(f, " )")?;
writeln!(f, "}}")?;
writeln!(f)?;
}
Ok(())
}
}
pub struct KtInstructions<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for KtInstructions<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --kt")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(
f,
"package hopper.generated.{}",
prog.name.replace('-', "_")
)?;
writeln!(f)?;
writeln!(f, "import org.sol4k.PublicKey")?;
writeln!(f, "import org.sol4k.instruction.AccountMeta")?;
writeln!(f, "import org.sol4k.instruction.Instruction")?;
writeln!(f, "import java.nio.ByteBuffer")?;
writeln!(f, "import java.nio.ByteOrder")?;
writeln!(f)?;
for ix in prog.instructions.iter() {
if !ix.args.is_empty() {
write!(f, "data class ")?;
write_kt_pascal(f, ix.name)?;
writeln!(f, "Args(")?;
for (i, arg) in ix.args.iter().enumerate() {
write!(f, " val ")?;
write_kt_camel(f, arg.name)?;
write!(f, ": {}", kt_type(arg.canonical_type))?;
if i + 1 < ix.args.len() {
writeln!(f, ",")?;
} else {
writeln!(f)?;
}
}
writeln!(f, ")")?;
writeln!(f)?;
}
write!(f, "data class ")?;
write_kt_pascal(f, ix.name)?;
writeln!(f, "Accounts(")?;
for (i, acc) in ix.accounts.iter().enumerate() {
write!(f, " val ")?;
write_kt_camel(f, acc.name)?;
write!(f, ": PublicKey")?;
if i + 1 < ix.accounts.len() {
writeln!(f, ",")?;
} else {
writeln!(f)?;
}
}
writeln!(f, ")")?;
writeln!(f)?;
write!(f, "fun create")?;
write_kt_pascal(f, ix.name)?;
writeln!(f, "Instruction(")?;
if !ix.args.is_empty() {
write!(f, " args: ")?;
write_kt_pascal(f, ix.name)?;
writeln!(f, "Args,")?;
}
write!(f, " accounts: ")?;
write_kt_pascal(f, ix.name)?;
writeln!(f, "Accounts,")?;
writeln!(f, " programId: PublicKey,")?;
writeln!(f, "): Instruction {{")?;
let data_size = instruction_data_size(ix);
writeln!(f, " val data = ByteArray({})", data_size)?;
writeln!(
f,
" data[0] = {}.toByte() // instruction discriminator",
ix.tag
)?;
let mut offset = 1usize;
for arg in ix.args.iter() {
write_kt_encode_expr(f, arg.canonical_type, arg.name, offset)?;
offset += arg.size as usize;
}
writeln!(f)?;
writeln!(f, " val keys = listOf(")?;
for acc in ix.accounts.iter() {
write!(f, " AccountMeta(accounts.")?;
write_kt_camel(f, acc.name)?;
writeln!(
f,
", isSigner = {}, isWritable = {}),",
acc.signer, acc.writable
)?;
}
writeln!(f, " )")?;
writeln!(f)?;
writeln!(f, " return Instruction(programId, keys, data)")?;
writeln!(f, "}}")?;
writeln!(f)?;
}
Ok(())
}
}
pub struct KtEvents<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for KtEvents<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --kt")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(
f,
"package hopper.generated.{}",
prog.name.replace('-', "_")
)?;
writeln!(f)?;
writeln!(f, "import org.sol4k.PublicKey")?;
writeln!(f, "import java.nio.ByteBuffer")?;
writeln!(f, "import java.nio.ByteOrder")?;
writeln!(f)?;
if prog.events.is_empty() {
writeln!(f, "// No events defined for this program.")?;
return Ok(());
}
for event in prog.events.iter() {
write!(f, "data class ")?;
write_kt_pascal(f, event.name)?;
writeln!(f, "Event(")?;
for (i, field) in event.fields.iter().enumerate() {
write!(f, " val ")?;
write_kt_camel(f, field.name)?;
write!(f, ": {}", kt_type(field.canonical_type))?;
if i + 1 < event.fields.len() {
writeln!(f, ",")?;
} else {
writeln!(f)?;
}
}
writeln!(f, ")")?;
writeln!(f)?;
write!(f, "const val ")?;
write_kt_const(f, event.name)?;
writeln!(f, "_EVENT_DISC: Byte = {}", event.tag)?;
writeln!(f)?;
write!(f, "fun decode")?;
write_kt_pascal(f, event.name)?;
write!(f, "Event(data: ByteArray): ")?;
write_kt_pascal(f, event.name)?;
writeln!(f, "Event {{")?;
for field in event.fields.iter() {
let offset = field.offset as usize;
let end = offset + field.size as usize;
write!(f, " val ")?;
write_kt_camel(f, field.name)?;
write!(f, " = ")?;
write_kt_decode_expr(f, field.canonical_type, offset, end)?;
writeln!(f)?;
}
write!(f, " return ")?;
write_kt_pascal(f, event.name)?;
writeln!(f, "Event(")?;
for (i, field) in event.fields.iter().enumerate() {
write!(f, " ")?;
write_kt_camel(f, field.name)?;
write!(f, " = ")?;
write_kt_camel(f, field.name)?;
if i + 1 < event.fields.len() {
writeln!(f, ",")?;
} else {
writeln!(f)?;
}
}
writeln!(f, " )")?;
writeln!(f, "}}")?;
writeln!(f)?;
}
Ok(())
}
}
pub struct KtTypes<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for KtTypes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "// Auto-generated by hopper client gen --kt")?;
writeln!(f, "// Program: {} v{}", prog.name, prog.version)?;
writeln!(f, "// DO NOT EDIT")?;
writeln!(f)?;
writeln!(
f,
"package hopper.generated.{}",
prog.name.replace('-', "_")
)?;
writeln!(f)?;
writeln!(f, "import java.nio.ByteBuffer")?;
writeln!(f, "import java.nio.ByteOrder")?;
writeln!(f)?;
writeln!(f, "/** Hopper account header (16 bytes). */")?;
writeln!(f, "data class HopperHeader(")?;
writeln!(f, " val disc: UByte,")?;
writeln!(f, " val version: UByte,")?;
writeln!(f, " val flags: UShort,")?;
writeln!(f, " val layoutId: ByteArray,")?;
writeln!(f, " val reserved: ByteArray")?;
writeln!(f, ")")?;
writeln!(f)?;
writeln!(f, "private const val TYPES_HEADER_SIZE: Int = 16")?;
writeln!(f, "private const val TYPES_LAYOUT_ID_OFFSET: Int = 4")?;
writeln!(f, "private const val TYPES_LAYOUT_ID_LENGTH: Int = 8")?;
writeln!(f)?;
writeln!(f, "/** Decode the Hopper 16-byte account header. */")?;
writeln!(f, "fun decodeHeader(data: ByteArray): HopperHeader {{")?;
writeln!(
f,
" require(data.size >= TYPES_HEADER_SIZE) {{ \"Data too small for header\" }}"
)?;
writeln!(f, " return HopperHeader(")?;
writeln!(f, " disc = data[0].toUByte(),")?;
writeln!(f, " version = data[1].toUByte(),")?;
writeln!(
f,
" flags = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort(),"
)?;
writeln!(
f,
" layoutId = data.copyOfRange(TYPES_LAYOUT_ID_OFFSET, TYPES_LAYOUT_ID_OFFSET + TYPES_LAYOUT_ID_LENGTH),"
)?;
writeln!(f, " reserved = data.copyOfRange(12, 16)")?;
writeln!(f, " )")?;
writeln!(f, "}}")?;
writeln!(f)?;
writeln!(
f,
"/** All account discriminators for {} v{}. */",
prog.name, prog.version
)?;
writeln!(f, "object Discriminators {{")?;
for layout in prog.layouts.iter() {
write!(f, " const val ")?;
write_kt_const(f, layout.name)?;
writeln!(f, ": Byte = {}", layout.disc)?;
}
writeln!(f, "}}")?;
Ok(())
}
}
pub struct KtClientGen<'a>(pub &'a ProgramManifest);
impl<'a> fmt::Display for KtClientGen<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prog = self.0;
writeln!(f, "=== Types.kt ===")?;
write!(f, "{}", KtTypes(prog))?;
writeln!(f)?;
writeln!(f, "=== Accounts.kt ===")?;
write!(f, "{}", KtAccounts(prog))?;
writeln!(f)?;
writeln!(f, "=== Instructions.kt ===")?;
write!(f, "{}", KtInstructions(prog))?;
writeln!(f)?;
writeln!(f, "=== Events.kt ===")?;
write!(f, "{}", KtEvents(prog))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
use crate::{
AccountEntry, ArgDescriptor, EventDescriptor, FieldDescriptor, FieldIntent, LayoutManifest,
};
use alloc::string::ToString;
fn test_manifest() -> ProgramManifest {
static FIELDS: &[FieldDescriptor] = &[
FieldDescriptor {
name: "authority",
canonical_type: "Pubkey",
size: 32,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "amount",
canonical_type: "u64",
size: 8,
offset: 48,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "is_active",
canonical_type: "bool",
size: 1,
offset: 56,
intent: FieldIntent::Custom,
},
];
static LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
name: "vault",
disc: 1,
version: 1,
layout_id: [0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44],
total_size: 64,
field_count: 3,
fields: FIELDS,
}];
static ARGS: &[ArgDescriptor] = &[ArgDescriptor {
name: "amount",
canonical_type: "u64",
size: 8,
}];
static ACCOUNTS: &[AccountEntry] = &[
AccountEntry {
name: "authority",
writable: false,
signer: true,
layout_ref: "",
},
AccountEntry {
name: "vault",
writable: true,
signer: false,
layout_ref: "vault",
},
];
static INSTRUCTIONS: &[InstructionDescriptor] = &[InstructionDescriptor {
name: "deposit",
tag: 0,
args: ARGS,
accounts: ACCOUNTS,
capabilities: &["write"],
policy_pack: "standard",
receipt_expected: true,
}];
static EVENT_FIELDS: &[FieldDescriptor] = &[
FieldDescriptor {
name: "depositor",
canonical_type: "Pubkey",
size: 32,
offset: 0,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "amount",
canonical_type: "u64",
size: 8,
offset: 32,
intent: FieldIntent::Custom,
},
];
static EVENTS: &[EventDescriptor] = &[EventDescriptor {
name: "deposit_event",
tag: 0,
fields: EVENT_FIELDS,
}];
ProgramManifest {
name: "test_vault",
version: "0.1.0",
description: "A test vault program",
layouts: LAYOUTS,
layout_metadata: &[],
instructions: INSTRUCTIONS,
events: EVENTS,
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
}
}
#[test]
fn ts_accounts_generates_interface() {
let m = test_manifest();
let output = TsAccounts(&m).to_string();
assert!(output.contains("export interface Vault {"));
assert!(output.contains("authority: PublicKey;"));
assert!(output.contains("amount: bigint;"));
assert!(output.contains("isActive: boolean;"));
}
#[test]
fn ts_accounts_generates_decoder() {
let m = test_manifest();
let output = TsAccounts(&m).to_string();
assert!(output.contains("export function decodeVault(data: Uint8Array)"));
assert!(output.contains("export function decodeVault(data: Uint8Array): \n Vault {\n assertVaultLayout(data);"));
assert!(
output.contains("throw new Error(`Data too small for vault: ${data.length} < 64`);")
);
assert!(output.contains("new PublicKey(data.slice(16, 48))"));
assert!(output.contains("view.getBigUint64(48, true)"));
assert!(output.contains("data[56] !== 0"));
}
#[test]
fn ts_accounts_generates_discriminator() {
let m = test_manifest();
let output = TsAccounts(&m).to_string();
assert!(output.contains("export const VAULT_DISC = 1;"));
}
#[test]
fn ts_instructions_generates_builder() {
let m = test_manifest();
let output = TsInstructions(&m).to_string();
assert!(output.contains("export interface DepositArgs {"));
assert!(output.contains("export interface DepositAccounts {"));
assert!(output.contains("export function createDepositInstruction("));
assert!(output.contains("data[0] = 0; // instruction discriminator"));
assert!(output.contains("view.setBigUint64(1, args.amount, true);"));
}
#[test]
fn ts_instructions_account_meta() {
let m = test_manifest();
let output = TsInstructions(&m).to_string();
assert!(output.contains("accounts.authority, isSigner: true, isWritable: false"));
assert!(output.contains("accounts.vault, isSigner: false, isWritable: true"));
}
#[test]
fn ts_events_generates_decoder() {
let m = test_manifest();
let output = TsEvents(&m).to_string();
assert!(output.contains("export interface DepositEventEvent {"));
assert!(output.contains("export function decodeDepositEventEvent(data: Uint8Array)"));
assert!(output.contains("DEPOSIT_EVENT_EVENT_DISC = 0;"));
}
#[test]
fn ts_types_generates_header() {
let m = test_manifest();
let output = TsTypes(&m).to_string();
assert!(output.contains("export interface HopperHeader {"));
assert!(output.contains("export function decodeHeader(data: Uint8Array)"));
assert!(output.contains("flags: view.getUint16(2, true),"));
assert!(output.contains(
"layoutId: data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH),"
));
assert!(output.contains("reserved: data.slice(12, 16),"));
assert!(output.contains("Vault: 1,"));
}
#[test]
fn ts_decode_header_and_assert_layout_id_share_offset() {
let m = test_manifest();
let accounts = TsAccounts(&m).to_string();
let types = TsTypes(&m).to_string();
assert!(accounts.contains("export const LAYOUT_ID_OFFSET = 4;"));
assert!(accounts.contains("data[LAYOUT_ID_OFFSET + i]"));
assert!(types.contains("const LAYOUT_ID_OFFSET = 4;"));
assert!(types.contains("data.slice(LAYOUT_ID_OFFSET, LAYOUT_ID_OFFSET + LAYOUT_ID_LENGTH)"));
assert!(!types.contains("data.slice(2, 10)"));
}
#[test]
fn ts_index_reexports_all() {
let m = test_manifest();
let output = TsIndex(&m).to_string();
assert!(output.contains("export * from \"./types\";"));
assert!(output.contains("export * from \"./accounts\";"));
assert!(output.contains("export * from \"./instructions\";"));
assert!(output.contains("export * from \"./events\";"));
}
#[test]
fn ts_full_client_gen_has_all_sections() {
let m = test_manifest();
let output = TsClientGen(&m).to_string();
assert!(output.contains("=== types.ts ==="));
assert!(output.contains("=== accounts.ts ==="));
assert!(output.contains("=== instructions.ts ==="));
assert!(output.contains("=== events.ts ==="));
assert!(output.contains("=== index.ts ==="));
}
#[test]
fn ts_accounts_emits_layout_id_constants_and_assertion_helpers() {
let m = test_manifest();
let output = TsAccounts(&m).to_string();
assert!(output.contains("export const LAYOUT_ID_OFFSET = 4;"));
assert!(output.contains("export const LAYOUT_ID_LENGTH = 8;"));
assert!(output.contains(
"export function assertLayoutId(data: Uint8Array, expectedHex: string): void"
));
assert!(output.contains("export const VAULT_LAYOUT_ID = \"aabbccdd11223344\";"));
assert!(output.contains("export function assertVaultLayout(data: Uint8Array): void"));
assert!(output.contains("assertLayoutId(data, VAULT_LAYOUT_ID);"));
}
#[test]
fn ts_assert_layout_id_handles_short_buffer_check() {
let m = test_manifest();
let output = TsAccounts(&m).to_string();
assert!(output.contains("if (data.length < HEADER_SIZE)"));
assert!(output.contains("throw new Error"));
}
#[test]
fn ts_data_size_calculation() {
static ARGS: &[ArgDescriptor] = &[
ArgDescriptor {
name: "amount",
canonical_type: "u64",
size: 8,
},
ArgDescriptor {
name: "bump",
canonical_type: "u8",
size: 1,
},
];
let ix = InstructionDescriptor {
name: "test",
tag: 0,
args: ARGS,
accounts: &[],
capabilities: &[],
policy_pack: "",
receipt_expected: false,
};
assert_eq!(instruction_data_size(&ix), 10);
}
#[test]
fn kt_accounts_generates_data_class() {
let m = test_manifest();
let output = KtAccounts(&m).to_string();
assert!(output.contains("data class Vault("));
assert!(output.contains("val authority: PublicKey"));
assert!(output.contains("val amount: ULong"));
assert!(output.contains("val isActive: Boolean"));
}
#[test]
fn kt_accounts_generates_decoder() {
let m = test_manifest();
let output = KtAccounts(&m).to_string();
assert!(output.contains("fun decodeVault(data: ByteArray): Vault {"));
assert!(output.contains("PublicKey(data.copyOfRange(16, 48))"));
assert!(output.contains("ByteBuffer.wrap(data, 48, 8)"));
assert!(output.contains("data[56] != 0.toByte()"));
}
#[test]
fn kt_accounts_generates_discriminator() {
let m = test_manifest();
let output = KtAccounts(&m).to_string();
assert!(output.contains("const val VAULT_DISC: Byte = 1"));
}
#[test]
fn kt_accounts_emits_layout_id_constants_and_assertion_helpers() {
let m = test_manifest();
let output = KtAccounts(&m).to_string();
assert!(output.contains("const val HEADER_SIZE: Int = 16"));
assert!(output.contains("const val LAYOUT_ID_OFFSET: Int = 4"));
assert!(output.contains("const val LAYOUT_ID_LENGTH: Int = 8"));
assert!(output.contains("fun assertLayoutId(data: ByteArray, expectedHex: String) {"));
assert!(output.contains("const val VAULT_LAYOUT_ID: String = \"aabbccdd11223344\""));
assert!(output.contains("fun assertVaultLayout(data: ByteArray) {"));
assert!(output.contains("assertLayoutId(data, VAULT_LAYOUT_ID)"));
assert!(output
.contains("fun decodeVault(data: ByteArray): Vault {\n assertVaultLayout(data)"));
}
#[test]
fn kt_instructions_generates_builder() {
let m = test_manifest();
let output = KtInstructions(&m).to_string();
assert!(output.contains("data class DepositArgs("));
assert!(output.contains("data class DepositAccounts("));
assert!(output.contains("fun createDepositInstruction("));
assert!(output.contains("data[0] = 0.toByte() // instruction discriminator"));
}
#[test]
fn kt_instructions_account_meta() {
let m = test_manifest();
let output = KtInstructions(&m).to_string();
assert!(output.contains("isSigner = true, isWritable = false"));
assert!(output.contains("isSigner = false, isWritable = true"));
}
#[test]
fn kt_events_generates_decoder() {
let m = test_manifest();
let output = KtEvents(&m).to_string();
assert!(output.contains("data class DepositEventEvent("));
assert!(output.contains("fun decodeDepositEventEvent(data: ByteArray)"));
assert!(output.contains("DEPOSIT_EVENT_EVENT_DISC: Byte = 0"));
}
#[test]
fn kt_types_generates_header() {
let m = test_manifest();
let output = KtTypes(&m).to_string();
assert!(output.contains("data class HopperHeader("));
assert!(output.contains("fun decodeHeader(data: ByteArray): HopperHeader {"));
assert!(output.contains(
"flags = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.LITTLE_ENDIAN).short.toUShort(),"
));
assert!(output.contains("layoutId = data.copyOfRange(TYPES_LAYOUT_ID_OFFSET, TYPES_LAYOUT_ID_OFFSET + TYPES_LAYOUT_ID_LENGTH),"));
assert!(output.contains("reserved = data.copyOfRange(12, 16)"));
assert!(!output.contains("data.copyOfRange(2, 10)"));
assert!(output.contains("VAULT: Byte = 1"));
}
#[test]
fn kt_full_client_gen_has_all_sections() {
let m = test_manifest();
let output = KtClientGen(&m).to_string();
assert!(output.contains("=== Types.kt ==="));
assert!(output.contains("=== Accounts.kt ==="));
assert!(output.contains("=== Instructions.kt ==="));
assert!(output.contains("=== Events.kt ==="));
}
}