1use crate::ast::{
11 AccountResolution, AmountDecimalsSource, IdlArrayElementSnapshot, IdlDefinedInnerSnapshot,
12 IdlErrorSnapshot, IdlInstructionSnapshot, IdlSnapshot, IdlTypeDefKindSnapshot,
13 IdlTypeDefSnapshot, IdlTypeSnapshot, InstructionAccountDef, InstructionDef, PdaDefinition,
14 PdaSeedDef,
15};
16use crate::typescript::{to_pascal_case, to_screaming_snake_case};
17use arete_idl::{IdlAmountDecimalsSource, IdlAmountHint};
18use std::collections::{BTreeMap, BTreeSet, HashSet};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct StackInstructionEntry {
23 pub program_key: Option<String>,
26 pub instruction_name: String,
28 pub runtime_program_key: Option<String>,
30 pub handler_const: String,
32 pub params_type: String,
34 pub semantic_params_type: Option<String>,
36 pub semantic_extra_params: Vec<String>,
38 pub semantic_amount_args: Vec<SemanticAmountArgEntry>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SemanticAmountArgEntry {
44 pub arg_name: String,
45 pub binding_name: String,
46 pub raw_expression: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50struct SemanticFieldSpec {
51 root_arg_name: String,
52 relative_path: Vec<String>,
53 resolution: SemanticAmountResolution,
54 decimals_override_name: Option<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum SemanticAmountResolution {
59 ArgMint {
60 arg_name: String,
61 },
62 ArgDecimals {
63 arg_name: String,
64 },
65 KnownAccount {
66 account_name: String,
67 optional: bool,
68 },
69 KnownAddress {
70 address: String,
71 },
72 Constant {
73 decimals: u8,
74 },
75}
76
77#[derive(Debug, Clone, Default)]
79pub struct InstructionsCodegen {
80 pub code: String,
83 pub stack_entries: Vec<StackInstructionEntry>,
85 pub needs_runtime_import: bool,
88 pub needs_program_runtime_extensions: bool,
90 pub needs_amount_input: bool,
92 pub needs_resolve_amount_to_raw: bool,
94 pub needs_to_raw_amount: bool,
96 pub warnings: Vec<String>,
98 pub pda_degradations: Vec<PdaDegradation>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum PdaDegradationSource {
104 Inline,
105 Registry,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct PdaDegradation {
110 pub instruction_name: String,
111 pub account_name: String,
112 pub pda_name: Option<String>,
113 pub source: PdaDegradationSource,
114 pub reason: String,
115}
116
117impl PdaDegradation {
118 pub fn warning_message(&self) -> String {
119 match (&self.source, &self.pda_name) {
120 (PdaDegradationSource::Inline, _) => format!(
121 "instruction '{}': account '{}' inline PDA degraded to userProvided ({})",
122 self.instruction_name, self.account_name, self.reason
123 ),
124 (_, Some(pda_name)) => format!(
125 "instruction '{}': account '{}' PDA '{}' degraded to userProvided ({})",
126 self.instruction_name, self.account_name, pda_name, self.reason
127 ),
128 (_, None) => format!(
129 "instruction '{}': account '{}' degraded to userProvided ({})",
130 self.instruction_name, self.account_name, self.reason
131 ),
132 }
133 }
134}
135
136struct ProgramErrorScope {
138 const_name: String,
139 type_name: String,
140 errors: Vec<IdlErrorSnapshot>,
141 used: bool,
142}
143
144fn instruction_snapshot_matches(
145 instruction: &InstructionDef,
146 snapshot: &IdlInstructionSnapshot,
147) -> bool {
148 instruction.name == snapshot.name
149 && instruction.discriminator == snapshot.discriminator
150 && instruction.args.len() == snapshot.args.len()
151 && instruction
152 .args
153 .iter()
154 .zip(snapshot.args.iter())
155 .all(|(arg, snapshot_arg)| arg.name == snapshot_arg.name)
156}
157
158fn find_instruction_snapshot<'a>(
159 instruction: &InstructionDef,
160 idls: &'a [IdlSnapshot],
161 program_index: Option<usize>,
162) -> Option<&'a IdlInstructionSnapshot> {
163 if let Some(index) = program_index {
164 return idls.get(index).and_then(|idl| {
165 idl.instructions
166 .iter()
167 .find(|candidate| instruction_snapshot_matches(instruction, candidate))
168 });
169 }
170
171 let mut matches = idls
172 .iter()
173 .filter(|idl| {
174 instruction
175 .program_id
176 .as_deref()
177 .is_none_or(|program_id| idl.program_id.as_deref() == Some(program_id))
178 })
179 .flat_map(|idl| idl.instructions.iter())
180 .filter(|candidate| instruction_snapshot_matches(instruction, candidate));
181 let first = matches.next()?;
182 if matches.next().is_some() {
183 return None;
184 }
185 Some(first)
186}
187
188pub fn generate_instructions_code(
195 stack_name: &str,
196 instructions: &[InstructionDef],
197 idls: &[IdlSnapshot],
198 pdas: &BTreeMap<String, BTreeMap<String, PdaDefinition>>,
199 program_ids: &[String],
200 reserved_type_names: &HashSet<String>,
201) -> InstructionsCodegen {
202 if instructions.is_empty() {
203 return InstructionsCodegen::default();
204 }
205
206 let multi_program = idls.len() > 1;
207 let mut defined_types = DefinedTypes::new(idls, reserved_type_names);
208
209 let mut warnings: Vec<String> = Vec::new();
213 let mut pda_degradations: Vec<PdaDegradation> = Vec::new();
214 let mut pda_lookup: BTreeMap<&str, &PdaDefinition> = BTreeMap::new();
215 for program_pdas in pdas.values() {
216 for (name, def) in program_pdas {
217 if let Some(existing) = pda_lookup.get(name.as_str()) {
218 if format!("{:?}", existing) != format!("{:?}", def) {
219 warnings.push(format!(
220 "PDA '{}' is defined differently in multiple programs; using the first definition",
221 name
222 ));
223 }
224 } else {
225 pda_lookup.insert(name.as_str(), def);
226 }
227 }
228 }
229
230 let default_program_id = program_ids.first().cloned().unwrap_or_default();
231
232 let mut blocks: Vec<String> = Vec::new();
233 let mut stack_entries: Vec<StackInstructionEntry> = Vec::new();
234 let mut needs_program_runtime_extensions = false;
235 let mut needs_amount_input = false;
236 let mut needs_resolve_amount_to_raw = false;
237 let mut needs_to_raw_amount = false;
238
239 let stack_screaming = to_screaming_snake_case(stack_name);
240 let stack_pascal = to_pascal_case(stack_name);
241
242 let mut program_scopes: Vec<ProgramErrorScope> = idls
246 .iter()
247 .map(|idl| {
248 let (const_name, type_name) = if multi_program {
249 (
250 format!(
251 "{}_{}_PROGRAM_ERRORS",
252 stack_screaming,
253 to_screaming_snake_case(&to_pascal_case(&idl.name))
254 ),
255 format!("{}{}ProgramError", stack_pascal, to_pascal_case(&idl.name)),
256 )
257 } else {
258 (
259 format!("{}_PROGRAM_ERRORS", stack_screaming),
260 format!("{}ProgramError", stack_pascal),
261 )
262 };
263 ProgramErrorScope {
264 const_name,
265 type_name,
266 errors: dedupe_errors_by_code(&idl.errors),
267 used: false,
268 }
269 })
270 .collect();
271 let mut fallback_scope = ProgramErrorScope {
272 const_name: format!("{}_PROGRAM_ERRORS", stack_screaming),
273 type_name: format!("{}ProgramError", stack_pascal),
274 errors: dedupe_errors_by_code(
275 &idls
276 .iter()
277 .flat_map(|idl| idl.errors.iter().cloned())
278 .collect::<Vec<_>>(),
279 ),
280 used: false,
281 };
282
283 for instr in instructions {
284 let program_index: Option<usize> = if multi_program {
286 instr.program_id.as_deref().and_then(|pid| {
287 idls.iter()
288 .position(|idl| idl.program_id.as_deref() == Some(pid))
289 })
290 } else if idls.len() == 1 {
291 Some(0)
292 } else {
293 None
294 };
295 if multi_program && program_index.is_none() {
296 warnings.push(format!(
297 "instruction '{}' could not be matched to a program IDL; using stack-wide error metadata and unprefixed naming",
298 instr.name
299 ));
300 }
301
302 let program_name = program_index.map(|i| idls[i].name.as_str());
305 let (pascal, handler_const, program_key) = match program_name {
306 Some(name) if multi_program => {
307 let program_pascal = to_pascal_case(name);
308 let instr_pascal = to_pascal_case(&instr.name);
309 (
310 format!("{}{}", program_pascal, instr_pascal),
311 format!("{}{}Instruction", to_camel_case(name), instr_pascal),
312 Some(to_camel_case(name)),
313 )
314 }
315 Some(name) => (
316 to_pascal_case(&instr.name),
317 format!("{}Instruction", instr.name),
318 Some(to_camel_case(name)),
319 ),
320 _ => (
321 to_pascal_case(&instr.name),
322 format!("{}Instruction", instr.name),
323 None,
324 ),
325 };
326 let runtime_program_key = program_name.map(to_camel_case);
327 let (program_errors_const, program_error_type) = match program_index {
328 Some(i) => {
329 program_scopes[i].used = true;
330 (
331 program_scopes[i].const_name.clone(),
332 program_scopes[i].type_name.clone(),
333 )
334 }
335 None => {
336 fallback_scope.used = true;
337 (
338 fallback_scope.const_name.clone(),
339 fallback_scope.type_name.clone(),
340 )
341 }
342 };
343
344 let instruction_snapshot = find_instruction_snapshot(instr, idls, program_index);
345
346 let mut parsed_args: Vec<(String, ParsedArgType)> = Vec::new();
348 let mut unsupported_arg: Option<(String, String)> = None;
349 for (index, arg) in instr.args.iter().enumerate() {
350 let parsed = instruction_snapshot
351 .and_then(|snapshot| snapshot.args.get(index))
352 .map(|snapshot_arg| defined_types.parse_snapshot_type(&snapshot_arg.type_))
353 .unwrap_or_else(|| defined_types.parse_arg_type(&arg.arg_type));
354 if !parsed.supported {
355 unsupported_arg = Some((arg.name.clone(), arg.arg_type.clone()));
356 break;
357 }
358 parsed_args.push((arg.name.clone(), parsed));
359 }
360
361 if let Some((arg_name, arg_type)) = unsupported_arg {
362 let warning = format!(
363 "skipped instruction '{}': arg '{}' has unsupported type '{}'",
364 instr.name, arg_name, arg_type
365 );
366 warnings.push(warning.clone());
367 blocks.push(format!("// [arete codegen] {}", warning));
368 continue;
369 }
370
371 let instr_account_names: HashSet<&str> =
373 instr.accounts.iter().map(|a| a.name.as_str()).collect();
374 let instr_arg_types: BTreeMap<&str, &str> = instr
377 .args
378 .iter()
379 .map(|a| (a.name.as_str(), a.arg_type.as_str()))
380 .collect();
381
382 let mut account_literals: Vec<String> = Vec::new();
383 let mut user_params: Vec<UserParam> = Vec::new();
384 let mut resolve_params: BTreeMap<String, String> = BTreeMap::new();
385 for acc in &instr.accounts {
386 let mapped = map_account(
387 acc,
388 &pda_lookup,
389 &instr_account_names,
390 &instr_arg_types,
391 &instr.name,
392 &mut warnings,
393 &mut pda_degradations,
394 );
395 account_literals.push(mapped.literal);
396 if let Some(param) = mapped.param {
397 user_params.push(param);
398 }
399 for resolve_param in mapped.resolve_params {
400 resolve_params
401 .entry(resolve_param.name)
402 .or_insert(resolve_param.ts_type);
403 }
404 }
405
406 let mut param_lines: Vec<String> = Vec::new();
408 for (name, parsed) in &parsed_args {
409 param_lines.push(format!(" {}: {};", name, parsed.ts_type));
410 }
411 for param in &user_params {
412 let optional = if param.optional { "?" } else { "" };
413 param_lines.push(format!(" {}{}: string;", param.name, optional));
414 }
415 if !resolve_params.is_empty() {
416 let resolve_lines: Vec<String> = resolve_params
417 .iter()
418 .map(|(name, ts_type)| {
419 format!(" {}?: {};", render_ts_property_name(name), ts_type)
420 })
421 .collect();
422 param_lines.push(format!(
423 " resolve?: {{\n{}\n }};",
424 resolve_lines.join("\n")
425 ));
426 }
427 let params_body = if param_lines.is_empty() {
428 " // This instruction takes no arguments or user-provided accounts.".to_string()
429 } else {
430 param_lines.join("\n")
431 };
432 let params_type = format!("{}Params", pascal);
433 let params_interface = format!("export interface {} {{\n{}\n}}", params_type, params_body);
434
435 let mut semantic_specs = collect_semantic_amount_args(
436 instr,
437 instruction_snapshot,
438 &mut defined_types,
439 &mut warnings,
440 );
441 semantic_specs.retain_mut(|spec| match spec.resolution.clone() {
442 SemanticAmountResolution::KnownAccount { account_name, .. } => {
443 let field_path = if spec.relative_path.is_empty() {
444 spec.root_arg_name.clone()
445 } else {
446 format!("{}.{}", spec.root_arg_name, spec.relative_path.join("."))
447 };
448 let Some(account) = instr
449 .accounts
450 .iter()
451 .find(|account| account.name == account_name)
452 else {
453 warnings.push(format!(
454 "instruction '{}': skipped amount-aware semantic wrapper for field '{}' because account '{}' was not found",
455 instr.name,
456 field_path,
457 account_name
458 ));
459 return false;
460 };
461 if let Some(param) = user_params.iter().find(|param| param.name == account_name) {
462 spec.resolution = SemanticAmountResolution::KnownAccount {
463 account_name,
464 optional: param.optional,
465 };
466 return true;
467 }
468 let AccountResolution::Known { address: known } = &account.resolution else {
469 warnings.push(format!(
470 "instruction '{}': skipped amount-aware semantic wrapper for field '{}' because account '{}' is neither caller-provided nor a known address",
471 instr.name,
472 field_path,
473 account_name
474 ));
475 return false;
476 };
477 spec.resolution = SemanticAmountResolution::KnownAddress {
478 address: known.clone(),
479 };
480 true
481 }
482 _ => true,
483 });
484 if runtime_program_key.is_some() {
485 needs_program_runtime_extensions = true;
486 }
487 if !semantic_specs.is_empty() && runtime_program_key.is_none() {
488 warnings.push(format!(
489 "instruction '{}': skipped amount-aware semantic wrapper because the runtime program namespace could not be determined",
490 instr.name
491 ));
492 semantic_specs.clear();
493 }
494
495 let semantic_extra_params: Vec<String> = semantic_specs
496 .iter()
497 .filter_map(|spec| spec.decimals_override_name.clone())
498 .collect::<BTreeSet<_>>()
499 .into_iter()
500 .collect();
501
502 let semantic_amount_args = if semantic_specs.is_empty() {
503 Vec::new()
504 } else {
505 needs_amount_input = true;
506 if semantic_specs.iter().any(|spec| {
507 matches!(
508 spec.resolution,
509 SemanticAmountResolution::ArgMint { .. }
510 | SemanticAmountResolution::KnownAccount { .. }
511 | SemanticAmountResolution::KnownAddress { .. }
512 )
513 }) {
514 needs_resolve_amount_to_raw = true;
515 }
516 if semantic_specs.iter().any(|spec| {
517 matches!(
518 spec.resolution,
519 SemanticAmountResolution::ArgDecimals { .. }
520 | SemanticAmountResolution::Constant { .. }
521 )
522 }) {
523 needs_to_raw_amount = true;
524 }
525
526 let mut conversions = Vec::new();
527 for (arg_name, _) in &parsed_args {
528 let arg_specs: Vec<&SemanticFieldSpec> = semantic_specs
529 .iter()
530 .filter(|spec| spec.root_arg_name == *arg_name)
531 .collect();
532 if arg_specs.is_empty() {
533 continue;
534 }
535
536 let arg_access = render_ts_property_access("params", arg_name);
537 let raw_expression = if let Some(snapshot_arg) = instruction_snapshot
538 .and_then(|snapshot| snapshot.args.iter().find(|arg| arg.name == *arg_name))
539 {
540 render_semantic_raw_expression(
541 &snapshot_arg.type_,
542 &arg_access,
543 &mut defined_types,
544 &arg_specs,
545 0,
546 )
547 .unwrap_or_else(|| arg_access.clone())
548 } else {
549 render_amount_resolution_expression(&arg_access, arg_specs[0])
550 };
551
552 conversions.push(SemanticAmountArgEntry {
553 arg_name: arg_name.clone(),
554 binding_name: semantic_raw_binding_name(arg_name),
555 raw_expression,
556 });
557 }
558 conversions
559 };
560
561 let semantic_params = if runtime_program_key.is_none() {
562 None
563 } else if semantic_specs.is_empty() {
564 Some((params_type.clone(), None))
565 } else {
566 let type_name = format!("{}SemanticParams", pascal);
567 let interface = render_semantic_params_interface(
568 &type_name,
569 &parsed_args,
570 instruction_snapshot,
571 &user_params,
572 &resolve_params,
573 &semantic_specs,
574 &mut defined_types,
575 );
576 Some((type_name, Some(interface)))
577 };
578
579 let error_type = format!("{}Error", pascal);
583 let error_decl = format!("export type {} = {};", error_type, program_error_type);
584
585 let args_literal = if parsed_args.is_empty() {
587 "[]".to_string()
588 } else {
589 let entries: Vec<String> = parsed_args
590 .iter()
591 .map(|(name, parsed)| {
592 format!(" {{ name: '{}', type: {} }},", name, parsed.schema)
593 })
594 .collect();
595 format!("[\n{}\n ]", entries.join("\n"))
596 };
597
598 let accounts_literal = if account_literals.is_empty() {
600 "[]".to_string()
601 } else {
602 format!("[\n{}\n ]", account_literals.join("\n"))
603 };
604
605 let program_id = instr
606 .program_id
607 .clone()
608 .unwrap_or_else(|| default_program_id.clone());
609 let discriminator = format!(
610 "[{}]",
611 instr
612 .discriminator
613 .iter()
614 .map(|b| b.to_string())
615 .collect::<Vec<_>>()
616 .join(", ")
617 );
618
619 let docs = render_docs(&instr.docs);
620 let handler = format!(
621 "{docs}export const {handler_const} = createInstructionHandler<{params_type}, {error_type}>({{\n programId: '{program_id}',\n discriminator: {discriminator},\n args: {args_literal},\n accounts: {accounts_literal},\n errors: {program_errors_const},\n}});",
622 docs = docs,
623 handler_const = handler_const,
624 params_type = params_type,
625 error_type = error_type,
626 program_id = program_id,
627 discriminator = discriminator,
628 args_literal = args_literal,
629 accounts_literal = accounts_literal,
630 program_errors_const = program_errors_const,
631 );
632
633 let mut block_parts = vec![params_interface];
634 if let Some((_, Some(semantic_interface))) = &semantic_params {
635 block_parts.push(semantic_interface.clone());
636 }
637 block_parts.push(error_decl);
638 block_parts.push(handler);
639 blocks.push(block_parts.join("\n\n"));
640 match program_index {
643 Some(i) => program_scopes[i].used = true,
644 None => fallback_scope.used = true,
645 }
646 stack_entries.push(StackInstructionEntry {
647 program_key,
648 instruction_name: instr.name.clone(),
649 runtime_program_key,
650 handler_const,
651 params_type: params_type.clone(),
652 semantic_params_type: semantic_params.as_ref().map(|(name, _)| name.clone()),
653 semantic_extra_params,
654 semantic_amount_args,
655 });
656 }
657
658 warnings.append(&mut defined_types.warnings);
659
660 if stack_entries.is_empty() {
661 return InstructionsCodegen {
663 code: String::new(),
664 stack_entries,
665 needs_runtime_import: false,
666 needs_program_runtime_extensions,
667 needs_amount_input,
668 needs_resolve_amount_to_raw,
669 needs_to_raw_amount,
670 warnings,
671 pda_degradations,
672 };
673 }
674
675 let mut error_blocks: Vec<String> = Vec::new();
678 for scope in program_scopes
679 .iter()
680 .chain(std::iter::once(&fallback_scope))
681 {
682 if scope.used {
683 error_blocks.push(render_program_errors(
684 &scope.const_name,
685 &scope.type_name,
686 &scope.errors,
687 ));
688 }
689 }
690 if error_blocks.is_empty() {
691 error_blocks.push(render_program_errors(
694 &fallback_scope.const_name,
695 &fallback_scope.type_name,
696 &fallback_scope.errors,
697 ));
698 }
699
700 let header = "// ============================================================================\n// Instruction Handlers\n// ============================================================================";
701
702 let type_decls = if defined_types.decls.is_empty() {
704 String::new()
705 } else {
706 format!("{}\n\n", defined_types.decls.join("\n\n"))
707 };
708
709 let code = format!(
710 "{header}\n\n{program_errors_block}\n\n{type_decls}{blocks}",
711 header = header,
712 program_errors_block = error_blocks.join("\n\n"),
713 type_decls = type_decls,
714 blocks = blocks.join("\n\n")
715 );
716
717 InstructionsCodegen {
718 code,
719 stack_entries,
720 needs_runtime_import: true,
721 needs_program_runtime_extensions,
722 needs_amount_input,
723 needs_resolve_amount_to_raw,
724 needs_to_raw_amount,
725 warnings,
726 pda_degradations,
727 }
728}
729
730pub fn render_instructions_stack_block(entries: &[StackInstructionEntry]) -> String {
735 if entries.is_empty() {
736 return String::new();
737 }
738
739 let mut lines: Vec<String> = Vec::new();
740 for entry in entries.iter().filter(|e| e.program_key.is_none()) {
742 lines.push(format!(
743 " {}: {},",
744 entry.instruction_name, entry.handler_const
745 ));
746 }
747 let mut program_order: Vec<&str> = Vec::new();
749 for entry in entries {
750 if let Some(key) = entry.program_key.as_deref() {
751 if !program_order.contains(&key) {
752 program_order.push(key);
753 }
754 }
755 }
756 for program in program_order {
757 let nested: Vec<String> = entries
758 .iter()
759 .filter(|e| e.program_key.as_deref() == Some(program))
760 .map(|e| format!(" {}: {},", e.instruction_name, e.handler_const))
761 .collect();
762 lines.push(format!(
763 " {}: {{\n{}\n }},",
764 program,
765 nested.join("\n")
766 ));
767 }
768
769 format!("\n rawInstructions: {{\n{}\n }},", lines.join("\n"))
770}
771
772fn to_camel_case(s: &str) -> String {
775 let pascal = to_pascal_case(s);
776 let mut chars = pascal.chars();
777 match chars.next() {
778 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
779 None => pascal,
780 }
781}
782
783#[derive(Debug, Clone)]
789struct ParsedArgType {
790 schema: String,
792 ts_type: String,
794 supported: bool,
796}
797
798fn unsupported() -> ParsedArgType {
799 ParsedArgType {
800 schema: "'u8'".to_string(),
801 ts_type: "unknown".to_string(),
802 supported: false,
803 }
804}
805
806#[cfg(test)]
809fn parse_arg_type(raw: &str) -> ParsedArgType {
810 DefinedTypes::empty().parse_arg_type(raw)
811}
812
813struct DefinedTypes<'a> {
819 defs: BTreeMap<String, &'a IdlTypeDefSnapshot>,
821 lower: BTreeMap<String, String>,
823 decls: Vec<String>,
825 resolved: BTreeMap<String, Option<ParsedArgType>>,
827 taken_names: HashSet<String>,
829 visiting: HashSet<String>,
831 warnings: Vec<String>,
832}
833
834impl<'a> DefinedTypes<'a> {
835 fn new(idls: &'a [IdlSnapshot], reserved_type_names: &HashSet<String>) -> Self {
836 let mut defs: BTreeMap<String, &'a IdlTypeDefSnapshot> = BTreeMap::new();
837 let mut lower: BTreeMap<String, String> = BTreeMap::new();
838 let mut warnings: Vec<String> = Vec::new();
839 for idl in idls {
840 for def in &idl.types {
841 if let Some(existing) = defs.get(def.name.as_str()) {
842 if format!("{:?}", existing.type_def) != format!("{:?}", def.type_def) {
843 warnings.push(format!(
844 "type '{}' is defined differently in multiple programs; using the first definition",
845 def.name
846 ));
847 }
848 } else {
849 defs.insert(def.name.clone(), def);
850 lower.insert(def.name.to_lowercase(), def.name.clone());
851 }
852 }
853 }
854 DefinedTypes {
855 defs,
856 lower,
857 decls: Vec::new(),
858 resolved: BTreeMap::new(),
859 taken_names: reserved_type_names.clone(),
860 visiting: HashSet::new(),
861 warnings,
862 }
863 }
864
865 #[cfg(test)]
866 fn empty() -> DefinedTypes<'static> {
867 DefinedTypes::new(&[], &HashSet::new())
868 }
869
870 fn parse_arg_type(&mut self, raw: &str) -> ParsedArgType {
873 let t = raw.trim().trim_start_matches('&').trim();
874
875 if let Some((name, inner)) = split_generic(t) {
877 match name {
878 "Option" => {
879 let inner = self.parse_arg_type(inner);
880 return ParsedArgType {
881 schema: format!("{{ option: {} }}", inner.schema),
882 ts_type: format!("{} | null", inner.ts_type),
883 supported: inner.supported,
884 };
885 }
886 "Vec" => {
887 let inner = self.parse_arg_type(inner);
888 return ParsedArgType {
889 schema: format!("{{ vec: {} }}", inner.schema),
890 ts_type: format!("{}[]", maybe_paren(&inner.ts_type)),
891 supported: inner.supported,
892 };
893 }
894 _ => return unsupported(),
895 }
896 }
897
898 if let Some(stripped) = t.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
900 if let Some((ty, n)) = stripped.rsplit_once(';') {
901 let inner = self.parse_arg_type(ty.trim());
902 let n = n.trim();
903 if n.parse::<usize>().is_ok() {
904 return ParsedArgType {
905 schema: format!("{{ array: [{}, {}] }}", inner.schema, n),
906 ts_type: format!("{}[]", maybe_paren(&inner.ts_type)),
907 supported: inner.supported,
908 };
909 }
910 }
911 }
912
913 let last = t.rsplit("::").next().unwrap_or(t);
915 match last {
916 "u8" => prim("u8", "number"),
917 "u16" => prim("u16", "number"),
918 "u32" => prim("u32", "number"),
919 "u64" => prim("u64", "bigint"),
920 "u128" => prim("u128", "bigint"),
921 "i8" => prim("i8", "number"),
922 "i16" => prim("i16", "number"),
923 "i32" => prim("i32", "number"),
924 "i64" => prim("i64", "bigint"),
925 "i128" => prim("i128", "bigint"),
926 "f32" => prim("f32", "number"),
927 "f64" => prim("f64", "number"),
928 "bool" => prim("bool", "boolean"),
929 "String" | "string" | "str" => prim("string", "string"),
930 "Pubkey" | "pubkey" | "PublicKey" | "publicKey" => prim("pubkey", "string"),
931 "bytes" => ParsedArgType {
934 schema: "'bytes'".to_string(),
935 ts_type: "Uint8Array | number[]".to_string(),
936 supported: true,
937 },
938 _ => self.resolve_defined(last).unwrap_or_else(unsupported),
939 }
940 }
941
942 fn parse_snapshot_type(&mut self, t: &IdlTypeSnapshot) -> ParsedArgType {
944 match t {
945 IdlTypeSnapshot::Simple(s) => self.parse_arg_type(s),
946 IdlTypeSnapshot::Option(o) => {
947 let inner = self.parse_snapshot_type(&o.option);
948 ParsedArgType {
949 schema: format!("{{ option: {} }}", inner.schema),
950 ts_type: format!("{} | null", inner.ts_type),
951 supported: inner.supported,
952 }
953 }
954 IdlTypeSnapshot::Vec(v) => {
955 let inner = self.parse_snapshot_type(&v.vec);
956 ParsedArgType {
957 schema: format!("{{ vec: {} }}", inner.schema),
958 ts_type: format!("{}[]", maybe_paren(&inner.ts_type)),
959 supported: inner.supported,
960 }
961 }
962 IdlTypeSnapshot::Array(arr) => {
963 let mut element: Option<ParsedArgType> = None;
964 let mut size: Option<u32> = None;
965 for part in &arr.array {
966 match part {
967 IdlArrayElementSnapshot::Type(inner) => {
968 element = Some(self.parse_snapshot_type(inner))
969 }
970 IdlArrayElementSnapshot::TypeName(name) => {
971 element = Some(self.parse_arg_type(name))
972 }
973 IdlArrayElementSnapshot::Size(n) => size = Some(*n),
974 }
975 }
976 match (element, size) {
977 (Some(inner), Some(n)) => ParsedArgType {
978 schema: format!("{{ array: [{}, {}] }}", inner.schema, n),
979 ts_type: format!("{}[]", maybe_paren(&inner.ts_type)),
980 supported: inner.supported,
981 },
982 _ => unsupported(),
983 }
984 }
985 IdlTypeSnapshot::HashMap(map) => {
986 let key = self.parse_snapshot_type(&map.hash_map.0);
987 let value = self.parse_snapshot_type(&map.hash_map.1);
988 if !key.supported || key.schema != "'string'" || !value.supported {
989 unsupported()
990 } else {
991 ParsedArgType {
992 schema: format!("{{ hashMap: [{}, {}] }}", key.schema, value.schema),
993 ts_type: format!("Record<string, {}>", value.ts_type),
994 supported: true,
995 }
996 }
997 }
998 IdlTypeSnapshot::Defined(d) => {
999 let name = match &d.defined {
1000 IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
1001 IdlDefinedInnerSnapshot::Simple(s) => s.as_str(),
1002 };
1003 self.resolve_defined(name).unwrap_or_else(unsupported)
1004 }
1005 }
1006 }
1007
1008 fn resolve_defined(&mut self, name: &str) -> Option<ParsedArgType> {
1011 if let Some(cached) = self.resolved.get(name) {
1012 return cached.clone();
1013 }
1014 if self.visiting.contains(name) {
1015 self.warnings.push(format!(
1016 "type '{}' is recursive; recursive types are not supported by instruction codegen",
1017 name
1018 ));
1019 return None;
1020 }
1021
1022 let key = if self.defs.contains_key(name) {
1023 name.to_string()
1024 } else {
1025 match self.lower.get(&name.to_lowercase()) {
1028 Some(canonical) => canonical.clone(),
1029 None => {
1030 self.resolved.insert(name.to_string(), None);
1031 return None;
1032 }
1033 }
1034 };
1035
1036 self.visiting.insert(key.clone());
1037 let def = self.defs[&key];
1038 let result = match &def.type_def {
1039 IdlTypeDefKindSnapshot::Struct { fields, .. } => {
1040 let fields = fields.clone();
1041 self.resolve_struct(&key, &fields)
1042 }
1043 IdlTypeDefKindSnapshot::TupleStruct { .. } => {
1044 self.warnings.push(format!(
1045 "type '{}' is a tuple struct, which instruction codegen does not support yet",
1046 key
1047 ));
1048 None
1049 }
1050 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
1051 let variants = variants.clone();
1052 self.resolve_enum(&key, &variants)
1053 }
1054 };
1055 self.visiting.remove(&key);
1056 self.resolved.insert(name.to_string(), result.clone());
1057 if name != key {
1058 self.resolved.insert(key, result.clone());
1059 }
1060 result
1061 }
1062
1063 fn find_definition(&self, name: &str) -> Option<&'a IdlTypeDefSnapshot> {
1064 if let Some(def) = self.defs.get(name) {
1065 return Some(*def);
1066 }
1067 self.lower
1068 .get(&name.to_lowercase())
1069 .and_then(|canonical| self.defs.get(canonical).copied())
1070 }
1071
1072 fn resolve_struct(
1073 &mut self,
1074 name: &str,
1075 fields: &[crate::ast::IdlFieldSnapshot],
1076 ) -> Option<ParsedArgType> {
1077 let mut schema_fields: Vec<String> = Vec::new();
1078 let mut ts_fields: Vec<String> = Vec::new();
1079 for field in fields {
1080 let parsed = self.parse_snapshot_type(&field.type_);
1081 if !parsed.supported {
1082 self.warnings.push(format!(
1083 "type '{}': field '{}' has an unsupported type",
1084 name, field.name
1085 ));
1086 return None;
1087 }
1088 schema_fields.push(format!(
1089 "{{ name: '{}', type: {} }}",
1090 field.name, parsed.schema
1091 ));
1092 ts_fields.push(format!(" {}: {};", field.name, parsed.ts_type));
1093 }
1094
1095 let ts_name = self.claim_ts_name(name);
1096 self.decls.push(format!(
1097 "export interface {} {{\n{}\n}}",
1098 ts_name,
1099 ts_fields.join("\n")
1100 ));
1101 Some(ParsedArgType {
1102 schema: format!("{{ struct: [{}] }}", schema_fields.join(", ")),
1103 ts_type: ts_name,
1104 supported: true,
1105 })
1106 }
1107
1108 fn resolve_enum(
1109 &mut self,
1110 name: &str,
1111 variants: &[crate::ast::IdlEnumVariantSnapshot],
1112 ) -> Option<ParsedArgType> {
1113 use crate::ast::IdlEnumVariantFieldSnapshot;
1114
1115 let mut schema_variants: Vec<String> = Vec::new();
1116 let mut ts_variants: Vec<String> = Vec::new();
1117 for variant in variants {
1118 if variant.fields.is_empty() {
1119 schema_variants.push(format!("'{}'", variant.name));
1120 ts_variants.push(format!("'{}'", variant.name));
1121 continue;
1122 }
1123
1124 let named: Vec<_> = variant
1125 .fields
1126 .iter()
1127 .filter_map(|f| match f {
1128 IdlEnumVariantFieldSnapshot::Named(field) => Some(field),
1129 IdlEnumVariantFieldSnapshot::Tuple(_) => None,
1130 })
1131 .collect();
1132
1133 if named.len() == variant.fields.len() {
1134 let mut field_schemas: Vec<String> = Vec::new();
1136 let mut field_ts: Vec<String> = Vec::new();
1137 for field in named {
1138 let parsed = self.parse_snapshot_type(&field.type_);
1139 if !parsed.supported {
1140 self.warnings.push(format!(
1141 "enum '{}': variant '{}' field '{}' has an unsupported type",
1142 name, variant.name, field.name
1143 ));
1144 return None;
1145 }
1146 field_schemas.push(format!(
1147 "{{ name: '{}', type: {} }}",
1148 field.name, parsed.schema
1149 ));
1150 field_ts.push(format!("{}: {}", field.name, parsed.ts_type));
1151 }
1152 schema_variants.push(format!(
1153 "{{ name: '{}', fields: [{}] }}",
1154 variant.name,
1155 field_schemas.join(", ")
1156 ));
1157 ts_variants.push(format!(
1158 "{{ {}: {{ {} }} }}",
1159 variant.name,
1160 field_ts.join("; ")
1161 ));
1162 } else if named.is_empty() {
1163 let mut element_schemas: Vec<String> = Vec::new();
1165 let mut element_ts: Vec<String> = Vec::new();
1166 for field in &variant.fields {
1167 let IdlEnumVariantFieldSnapshot::Tuple(ty) = field else {
1168 unreachable!("named.is_empty() guarantees tuple fields");
1169 };
1170 let parsed = self.parse_snapshot_type(ty);
1171 if !parsed.supported {
1172 self.warnings.push(format!(
1173 "enum '{}': variant '{}' has an unsupported tuple element type",
1174 name, variant.name
1175 ));
1176 return None;
1177 }
1178 element_schemas.push(parsed.schema);
1179 element_ts.push(parsed.ts_type);
1180 }
1181 schema_variants.push(format!(
1182 "{{ name: '{}', tuple: [{}] }}",
1183 variant.name,
1184 element_schemas.join(", ")
1185 ));
1186 ts_variants.push(format!(
1187 "{{ {}: [{}] }}",
1188 variant.name,
1189 element_ts.join(", ")
1190 ));
1191 } else {
1192 self.warnings.push(format!(
1193 "enum '{}': variant '{}' mixes named and tuple fields, which is not supported",
1194 name, variant.name
1195 ));
1196 return None;
1197 }
1198 }
1199
1200 let ts_name = self.claim_ts_name(name);
1201 self.decls.push(format!(
1202 "export type {} =\n | {};",
1203 ts_name,
1204 ts_variants.join("\n | ")
1205 ));
1206 Some(ParsedArgType {
1207 schema: format!("{{ enum: [{}] }}", schema_variants.join(", ")),
1208 ts_type: ts_name,
1209 supported: true,
1210 })
1211 }
1212
1213 fn claim_ts_name(&mut self, name: &str) -> String {
1217 let base = to_pascal_case(name);
1218 let mut candidate = base.clone();
1219 if self.taken_names.contains(&candidate) {
1220 candidate = format!("{}Input", base);
1221 let mut counter = 2;
1222 while self.taken_names.contains(&candidate) {
1223 candidate = format!("{}Input{}", base, counter);
1224 counter += 1;
1225 }
1226 self.warnings.push(format!(
1227 "type '{}' collides with an existing interface; emitted as '{}'",
1228 name, candidate
1229 ));
1230 }
1231 self.taken_names.insert(candidate.clone());
1232 candidate
1233 }
1234}
1235
1236fn prim(schema: &str, ts: &str) -> ParsedArgType {
1237 ParsedArgType {
1238 schema: format!("'{}'", schema),
1239 ts_type: ts.to_string(),
1240 supported: true,
1241 }
1242}
1243
1244fn split_generic(t: &str) -> Option<(&str, &str)> {
1246 let open = t.find('<')?;
1247 if !t.ends_with('>') {
1248 return None;
1249 }
1250 let name = t[..open].rsplit("::").next().unwrap_or(&t[..open]).trim();
1251 let inner = t[open + 1..t.len() - 1].trim();
1252 Some((name, inner))
1253}
1254
1255fn maybe_paren(ts: &str) -> String {
1257 if ts.contains('|') {
1258 format!("({})", ts)
1259 } else {
1260 ts.to_string()
1261 }
1262}
1263
1264#[derive(Debug, Clone)]
1270struct UserParam {
1271 name: String,
1272 optional: bool,
1273}
1274
1275#[derive(Debug, Clone)]
1277struct ResolveParam {
1278 name: String,
1279 ts_type: String,
1280}
1281
1282struct MappedAccount {
1284 literal: String,
1286 param: Option<UserParam>,
1288 resolve_params: Vec<ResolveParam>,
1290}
1291
1292fn map_account(
1293 acc: &InstructionAccountDef,
1294 pda_lookup: &BTreeMap<&str, &PdaDefinition>,
1295 instr_account_names: &HashSet<&str>,
1296 instr_arg_types: &BTreeMap<&str, &str>,
1297 instr_name: &str,
1298 warnings: &mut Vec<String>,
1299 degradations: &mut Vec<PdaDegradation>,
1300) -> MappedAccount {
1301 let base = format!(
1302 "name: '{}', isSigner: {}, isWritable: {}",
1303 acc.name, acc.is_signer, acc.is_writable
1304 );
1305 let optional_suffix = if acc.is_optional {
1306 ", isOptional: true".to_string()
1307 } else {
1308 String::new()
1309 };
1310
1311 let user_provided = |degradation: Option<PdaDegradation>,
1312 warnings: &mut Vec<String>,
1313 degradations: &mut Vec<PdaDegradation>|
1314 -> MappedAccount {
1315 let comment = match °radation {
1319 Some(degradation) => {
1320 format!(" // [arete codegen] {}\n", degradation.warning_message())
1321 }
1322 None => String::new(),
1323 };
1324 if let Some(degradation) = degradation {
1325 warnings.push(degradation.warning_message());
1326 degradations.push(degradation);
1327 }
1328 MappedAccount {
1329 literal: format!(
1330 "{} {{ {}, category: 'userProvided'{} }},",
1331 comment, base, optional_suffix
1332 ),
1333 param: Some(UserParam {
1334 name: acc.name.clone(),
1335 optional: acc.is_optional,
1336 }),
1337 resolve_params: Vec::new(),
1338 }
1339 };
1340
1341 match &acc.resolution {
1342 AccountResolution::Signer => MappedAccount {
1343 literal: format!(" {{ {}, category: 'signer'{} }},", base, optional_suffix),
1344 param: None,
1345 resolve_params: Vec::new(),
1346 },
1347 AccountResolution::Known { address } => MappedAccount {
1348 literal: format!(
1349 " {{ {}, category: 'known', knownAddress: '{}'{} }},",
1350 base, address, optional_suffix
1351 ),
1352 param: None,
1353 resolve_params: Vec::new(),
1354 },
1355 AccountResolution::UserProvided => user_provided(None, warnings, degradations),
1356 AccountResolution::PdaInline { seeds, program_id } => {
1357 match build_pda_config(
1358 seeds,
1359 program_id.as_deref(),
1360 instr_account_names,
1361 instr_arg_types,
1362 ) {
1363 Ok((pda_config, seed_warnings, resolve_params)) => {
1364 for w in seed_warnings {
1365 warnings.push(format!(
1366 "instruction '{}': account '{}': {}",
1367 instr_name, acc.name, w
1368 ));
1369 }
1370 MappedAccount {
1371 literal: format!(
1372 " {{ {}, category: 'pda', pdaConfig: {}{} }},",
1373 base, pda_config, optional_suffix
1374 ),
1375 param: Some(UserParam {
1376 name: acc.name.clone(),
1377 optional: true,
1378 }),
1379 resolve_params,
1380 }
1381 }
1382 Err(reason) => user_provided(
1383 Some(PdaDegradation {
1384 instruction_name: instr_name.to_string(),
1385 account_name: acc.name.clone(),
1386 pda_name: None,
1387 source: PdaDegradationSource::Inline,
1388 reason,
1389 }),
1390 warnings,
1391 degradations,
1392 ),
1393 }
1394 }
1395 AccountResolution::PdaRef { pda_name } => match pda_lookup.get(pda_name.as_str()) {
1396 Some(def) => match build_pda_config(
1397 &def.seeds,
1398 def.program_id.as_deref(),
1399 instr_account_names,
1400 instr_arg_types,
1401 ) {
1402 Ok((pda_config, seed_warnings, resolve_params)) => {
1403 for w in seed_warnings {
1404 warnings.push(format!(
1405 "instruction '{}': account '{}': {}",
1406 instr_name, acc.name, w
1407 ));
1408 }
1409 MappedAccount {
1410 literal: format!(
1411 " {{ {}, category: 'pda', pdaConfig: {}{} }},",
1412 base, pda_config, optional_suffix
1413 ),
1414 param: Some(UserParam {
1415 name: acc.name.clone(),
1416 optional: true,
1417 }),
1418 resolve_params,
1419 }
1420 }
1421 Err(reason) => user_provided(
1422 Some(PdaDegradation {
1423 instruction_name: instr_name.to_string(),
1424 account_name: acc.name.clone(),
1425 pda_name: Some(pda_name.clone()),
1426 source: PdaDegradationSource::Registry,
1427 reason,
1428 }),
1429 warnings,
1430 degradations,
1431 ),
1432 },
1433 None => user_provided(
1434 Some(PdaDegradation {
1435 instruction_name: instr_name.to_string(),
1436 account_name: acc.name.clone(),
1437 pda_name: Some(pda_name.clone()),
1438 source: PdaDegradationSource::Registry,
1439 reason: format!("references unknown PDA '{}'", pda_name),
1440 }),
1441 warnings,
1442 degradations,
1443 ),
1444 },
1445 }
1446}
1447
1448fn build_pda_config(
1456 seeds: &[PdaSeedDef],
1457 program_id: Option<&str>,
1458 instr_account_names: &HashSet<&str>,
1459 instr_arg_types: &BTreeMap<&str, &str>,
1460) -> Result<(String, Vec<String>, Vec<ResolveParam>), String> {
1461 let mut seed_literals: Vec<String> = Vec::new();
1462 let mut soft_warnings: Vec<String> = Vec::new();
1463 let mut resolve_params: Vec<ResolveParam> = Vec::new();
1464 for seed in seeds {
1465 match seed {
1466 PdaSeedDef::Literal { value } => {
1467 seed_literals.push(format!(
1468 "{{ type: 'literal', value: '{}' }}",
1469 escape_single_quotes(value)
1470 ));
1471 }
1472 PdaSeedDef::AccountRef { account_name } => {
1473 if account_name.contains('.') {
1474 return Err(format!(
1475 "seed references account field '{}' which is not supported for low-level auto-resolution; encode it as a typed helper arg instead",
1476 account_name
1477 ));
1478 }
1479 if !instr_account_names.contains(account_name.as_str()) {
1480 return Err(format!(
1481 "seed references account '{}' not present in this instruction",
1482 account_name
1483 ));
1484 }
1485 seed_literals.push(format!(
1486 "{{ type: 'accountRef', accountName: '{}' }}",
1487 account_name
1488 ));
1489 }
1490 PdaSeedDef::ArgRef { arg_name, arg_type } => {
1491 let arg_root = arg_name.split('.').next().unwrap_or(arg_name.as_str());
1492 let present_in_args = instr_arg_types.contains_key(arg_name.as_str())
1493 || instr_arg_types.contains_key(arg_root);
1494 let raw_type = arg_type
1497 .as_deref()
1498 .or_else(|| instr_arg_types.get(arg_name.as_str()).copied())
1499 .or_else(|| instr_arg_types.get(arg_root).copied());
1500 if !present_in_args {
1501 let helper_type = match raw_type {
1502 Some(raw) => {
1503 let Some(ts_type) = seed_arg_ts_type(raw) else {
1504 return Err(format!(
1505 "seed helper arg '{}' needs an explicit primitive type; found unsupported type '{}'",
1506 arg_name, raw
1507 ));
1508 };
1509 ts_type
1510 }
1511 None => {
1512 return Err(format!(
1513 "seed helper arg '{}' is not present in this instruction and has no type information",
1514 arg_name
1515 ))
1516 }
1517 };
1518 resolve_params.push(ResolveParam {
1519 name: arg_name.clone(),
1520 ts_type: helper_type,
1521 });
1522 }
1523 match raw_type.and_then(normalize_seed_arg_type) {
1524 Some(canonical) => seed_literals.push(format!(
1525 "{{ type: 'argRef', argName: '{}', argType: '{}' }}",
1526 arg_name, canonical
1527 )),
1528 None => {
1529 soft_warnings.push(format!(
1530 "seed arg '{}' has non-primitive type '{}'; runtime will use heuristic encoding",
1531 arg_name,
1532 raw_type.unwrap_or("<unknown>")
1533 ));
1534 seed_literals
1535 .push(format!("{{ type: 'argRef', argName: '{}' }}", arg_name));
1536 }
1537 }
1538 }
1539 PdaSeedDef::Bytes { value } => {
1540 let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
1541 seed_literals.push(format!(
1542 "{{ type: 'bytes', value: [{}] }}",
1543 bytes.join(", ")
1544 ));
1545 }
1546 }
1547 }
1548
1549 let seeds_str = seed_literals.join(", ");
1550 let config = match program_id {
1551 Some(pid) => format!("{{ programId: '{}', seeds: [{}] }}", pid, seeds_str),
1552 None => format!("{{ seeds: [{}] }}", seeds_str),
1553 };
1554 Ok((config, soft_warnings, resolve_params))
1555}
1556
1557fn render_ts_property_name(name: &str) -> String {
1558 if name
1559 .chars()
1560 .next()
1561 .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
1562 .unwrap_or(false)
1563 && name
1564 .chars()
1565 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
1566 {
1567 name.to_string()
1568 } else {
1569 format!("'{}'", escape_single_quotes(name))
1570 }
1571}
1572
1573fn lower_first(value: &str) -> String {
1574 let mut chars = value.chars();
1575 match chars.next() {
1576 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1577 None => String::new(),
1578 }
1579}
1580
1581fn semantic_path_identifier(path: &[String], suffix: &str) -> String {
1582 let joined = path
1583 .iter()
1584 .map(|segment| to_pascal_case(segment))
1585 .collect::<String>();
1586 format!("{}{}", lower_first(&joined), suffix)
1587}
1588
1589fn semantic_decimals_override_name(path: &[String]) -> String {
1590 semantic_path_identifier(path, "Decimals")
1591}
1592
1593fn semantic_raw_binding_name(arg_name: &str) -> String {
1594 semantic_path_identifier(&[arg_name.to_string()], "Raw")
1595}
1596
1597fn is_valid_ts_identifier(name: &str) -> bool {
1598 name.chars()
1599 .next()
1600 .map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
1601 .unwrap_or(false)
1602 && name
1603 .chars()
1604 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
1605}
1606
1607fn render_ts_property_access(target: &str, property: &str) -> String {
1608 if is_valid_ts_identifier(property) {
1609 format!("{}.{}", target, property)
1610 } else {
1611 format!("{}['{}']", target, escape_single_quotes(property))
1612 }
1613}
1614
1615fn render_ts_path_access(target: &str, path: &str) -> String {
1616 path.split('.')
1617 .filter(|segment| !segment.is_empty())
1618 .fold(target.to_string(), |acc, segment| {
1619 render_ts_property_access(&acc, segment)
1620 })
1621}
1622
1623fn semantic_resolution_from_amount_source(
1624 source: &AmountDecimalsSource,
1625) -> SemanticAmountResolution {
1626 match source {
1627 AmountDecimalsSource::ArgMint { arg_name } => SemanticAmountResolution::ArgMint {
1628 arg_name: arg_name.clone(),
1629 },
1630 AmountDecimalsSource::ArgDecimals { arg_name } => SemanticAmountResolution::ArgDecimals {
1631 arg_name: arg_name.clone(),
1632 },
1633 AmountDecimalsSource::KnownAccount { account_name } => {
1634 SemanticAmountResolution::KnownAccount {
1635 account_name: account_name.clone(),
1636 optional: false,
1637 }
1638 }
1639 AmountDecimalsSource::Constant { decimals } => SemanticAmountResolution::Constant {
1640 decimals: *decimals,
1641 },
1642 }
1643}
1644
1645fn semantic_resolution_from_idl_hint(hint: &IdlAmountHint) -> SemanticAmountResolution {
1646 match &hint.decimals_source {
1647 IdlAmountDecimalsSource::ArgMint { arg_name } => SemanticAmountResolution::ArgMint {
1648 arg_name: arg_name.clone(),
1649 },
1650 IdlAmountDecimalsSource::ArgDecimals { arg_name } => {
1651 SemanticAmountResolution::ArgDecimals {
1652 arg_name: arg_name.clone(),
1653 }
1654 }
1655 IdlAmountDecimalsSource::KnownAccount { account_name } => {
1656 SemanticAmountResolution::KnownAccount {
1657 account_name: account_name.clone(),
1658 optional: false,
1659 }
1660 }
1661 IdlAmountDecimalsSource::Constant { decimals } => SemanticAmountResolution::Constant {
1662 decimals: *decimals,
1663 },
1664 }
1665}
1666
1667fn semantic_needs_decimals_override(resolution: &SemanticAmountResolution) -> bool {
1668 matches!(
1669 resolution,
1670 SemanticAmountResolution::ArgMint { .. }
1671 | SemanticAmountResolution::KnownAccount { .. }
1672 | SemanticAmountResolution::KnownAddress { .. }
1673 )
1674}
1675
1676fn collect_semantic_specs_from_type(
1677 root_arg_name: &str,
1678 relative_path: &[String],
1679 ty: &IdlTypeSnapshot,
1680 defined_types: &mut DefinedTypes<'_>,
1681 warnings: &mut Vec<String>,
1682 specs: &mut Vec<SemanticFieldSpec>,
1683) {
1684 match ty {
1685 IdlTypeSnapshot::Vec(vec_type) => {
1686 collect_semantic_specs_from_type(
1687 root_arg_name,
1688 relative_path,
1689 &vec_type.vec,
1690 defined_types,
1691 warnings,
1692 specs,
1693 );
1694 return;
1695 }
1696 IdlTypeSnapshot::Array(array_type) => {
1697 for part in &array_type.array {
1698 match part {
1699 IdlArrayElementSnapshot::Type(element) => collect_semantic_specs_from_type(
1700 root_arg_name,
1701 relative_path,
1702 element,
1703 defined_types,
1704 warnings,
1705 specs,
1706 ),
1707 IdlArrayElementSnapshot::TypeName(name) => {
1708 collect_semantic_specs_from_type(
1709 root_arg_name,
1710 relative_path,
1711 &IdlTypeSnapshot::Simple(name.clone()),
1712 defined_types,
1713 warnings,
1714 specs,
1715 );
1716 }
1717 IdlArrayElementSnapshot::Size(_) => {}
1718 }
1719 }
1720 return;
1721 }
1722 _ => {}
1723 }
1724
1725 let IdlTypeSnapshot::Defined(defined) = ty else {
1726 return;
1727 };
1728 let type_name = match &defined.defined {
1729 IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
1730 IdlDefinedInnerSnapshot::Simple(simple) => simple.as_str(),
1731 };
1732 let Some(def) = defined_types.find_definition(type_name) else {
1733 return;
1734 };
1735
1736 match &def.type_def {
1737 IdlTypeDefKindSnapshot::Struct { fields, .. } => {
1738 let fields = fields.clone();
1739 for field in &fields {
1740 let mut child_path = relative_path.to_vec();
1741 child_path.push(field.name.clone());
1742 collect_semantic_specs_from_field(
1743 root_arg_name,
1744 &child_path,
1745 field,
1746 defined_types,
1747 warnings,
1748 specs,
1749 );
1750 }
1751 }
1752 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
1753 let variants = variants.clone();
1754 for variant in &variants {
1755 for field in &variant.fields {
1756 let crate::ast::IdlEnumVariantFieldSnapshot::Named(named) = field else {
1757 continue;
1758 };
1759 let mut child_path = relative_path.to_vec();
1760 child_path.push(variant.name.clone());
1761 child_path.push(named.name.clone());
1762 collect_semantic_specs_from_field(
1763 root_arg_name,
1764 &child_path,
1765 named,
1766 defined_types,
1767 warnings,
1768 specs,
1769 );
1770 }
1771 }
1772 }
1773 IdlTypeDefKindSnapshot::TupleStruct { .. } => {}
1774 }
1775}
1776
1777fn collect_semantic_specs_from_field(
1778 root_arg_name: &str,
1779 relative_path: &[String],
1780 field: &crate::ast::IdlFieldSnapshot,
1781 defined_types: &mut DefinedTypes<'_>,
1782 warnings: &mut Vec<String>,
1783 specs: &mut Vec<SemanticFieldSpec>,
1784) {
1785 if let Some(hint) = &field.amount_hint {
1786 let parsed = defined_types.parse_snapshot_type(&field.type_);
1787 if parsed.ts_type != "bigint" {
1788 let full_path = std::iter::once(root_arg_name)
1789 .chain(relative_path.iter().map(String::as_str))
1790 .collect::<Vec<_>>()
1791 .join(".");
1792 warnings.push(format!(
1793 "instruction semantic wrapper: skipped amount-aware field '{}' because only bigint-backed raw fields are currently supported",
1794 full_path
1795 ));
1796 return;
1797 }
1798
1799 let resolution = semantic_resolution_from_idl_hint(hint);
1800 let mut full_path = vec![root_arg_name.to_string()];
1801 full_path.extend(relative_path.iter().cloned());
1802 let decimals_override_name = if semantic_needs_decimals_override(&resolution) {
1803 Some(semantic_decimals_override_name(&full_path))
1804 } else {
1805 None
1806 };
1807 specs.push(SemanticFieldSpec {
1808 root_arg_name: root_arg_name.to_string(),
1809 relative_path: relative_path.to_vec(),
1810 resolution,
1811 decimals_override_name,
1812 });
1813 return;
1814 }
1815
1816 collect_semantic_specs_from_type(
1817 root_arg_name,
1818 relative_path,
1819 &field.type_,
1820 defined_types,
1821 warnings,
1822 specs,
1823 );
1824}
1825
1826fn collect_semantic_amount_args(
1827 instr: &InstructionDef,
1828 instruction_snapshot: Option<&IdlInstructionSnapshot>,
1829 defined_types: &mut DefinedTypes<'_>,
1830 warnings: &mut Vec<String>,
1831) -> Vec<SemanticFieldSpec> {
1832 if let Some(snapshot) = instruction_snapshot {
1833 let mut specs = Vec::new();
1834 for arg in &snapshot.args {
1835 collect_semantic_specs_from_field(
1836 &arg.name,
1837 &[],
1838 arg,
1839 defined_types,
1840 warnings,
1841 &mut specs,
1842 );
1843 }
1844 if !specs.is_empty() {
1845 return specs;
1846 }
1847 }
1848
1849 let mut specs = Vec::new();
1850 for arg in &instr.args {
1851 let Some(hint) = &arg.amount_hint else {
1852 continue;
1853 };
1854 let parsed = defined_types.parse_arg_type(&arg.arg_type);
1855 if parsed.ts_type != "bigint" {
1856 warnings.push(format!(
1857 "instruction '{}': skipped amount-aware semantic wrapper for arg '{}' because only bigint-backed raw args are currently supported",
1858 instr.name, arg.name
1859 ));
1860 continue;
1861 }
1862 let resolution = semantic_resolution_from_amount_source(&hint.decimals_source);
1863 let path = vec![arg.name.clone()];
1864 specs.push(SemanticFieldSpec {
1865 root_arg_name: arg.name.clone(),
1866 relative_path: Vec::new(),
1867 decimals_override_name: semantic_needs_decimals_override(&resolution)
1868 .then(|| semantic_decimals_override_name(&path)),
1869 resolution,
1870 });
1871 }
1872
1873 specs
1874}
1875
1876fn render_amount_resolution_expression(input_expr: &str, spec: &SemanticFieldSpec) -> String {
1877 let render_source_path = |path: &str| {
1878 if let Some((_, element_path)) = path.rsplit_once("[]") {
1879 let element_path = element_path.trim_start_matches('.');
1880 if element_path.is_empty() {
1881 "entry".to_string()
1882 } else {
1883 render_ts_path_access("entry", element_path)
1884 }
1885 } else {
1886 render_ts_path_access("params", path)
1887 }
1888 };
1889 match &spec.resolution {
1890 SemanticAmountResolution::ArgMint { arg_name } => format!(
1891 "await resolveAmountToRaw(context.chain, {{ mint: {}, amount: {}, decimals: {} }})",
1892 render_source_path(arg_name),
1893 input_expr,
1894 render_ts_property_access(
1895 "params",
1896 spec.decimals_override_name
1897 .as_deref()
1898 .expect("mint-based amount hints should declare an override field")
1899 )
1900 ),
1901 SemanticAmountResolution::ArgDecimals { arg_name } => format!(
1902 "toRawAmount({}, {})",
1903 input_expr,
1904 render_source_path(arg_name)
1905 ),
1906 SemanticAmountResolution::KnownAccount {
1907 account_name,
1908 optional,
1909 } => format!(
1910 "await resolveAmountToRaw(context.chain, {{ mint: {}, amount: {}, decimals: {} }})",
1911 if *optional {
1912 format!(
1913 "({} ?? '')",
1914 render_ts_property_access("params", account_name)
1915 )
1916 } else {
1917 render_ts_property_access("params", account_name)
1918 },
1919 input_expr,
1920 render_ts_property_access(
1921 "params",
1922 spec.decimals_override_name
1923 .as_deref()
1924 .expect("known-account amount hints should declare an override field")
1925 )
1926 ),
1927 SemanticAmountResolution::KnownAddress { address } => format!(
1928 "await resolveAmountToRaw(context.chain, {{ mint: '{}', amount: {}, decimals: {} }})",
1929 escape_single_quotes(address),
1930 input_expr,
1931 render_ts_property_access(
1932 "params",
1933 spec.decimals_override_name
1934 .as_deref()
1935 .expect("known-account amount hints should declare an override field")
1936 )
1937 ),
1938 SemanticAmountResolution::Constant { decimals } => {
1939 format!("toRawAmount({}, {})", input_expr, decimals)
1940 }
1941 }
1942}
1943
1944fn render_semantic_ts_type(
1945 ty: &IdlTypeSnapshot,
1946 defined_types: &mut DefinedTypes<'_>,
1947 specs: &[&SemanticFieldSpec],
1948 depth: usize,
1949) -> Option<String> {
1950 if specs.iter().any(|spec| spec.relative_path.len() == depth) {
1951 return Some("AmountInput".to_string());
1952 }
1953 if !specs.iter().any(|spec| spec.relative_path.len() > depth) {
1954 return Some(defined_types.parse_snapshot_type(ty).ts_type);
1955 }
1956
1957 match ty {
1958 IdlTypeSnapshot::Vec(vec_type) => {
1959 let inner = render_semantic_ts_type(&vec_type.vec, defined_types, specs, depth)?;
1960 return Some(format!("{}[]", maybe_paren(&inner)));
1961 }
1962 IdlTypeSnapshot::Array(array_type) => {
1963 for part in &array_type.array {
1964 let inner = match part {
1965 IdlArrayElementSnapshot::Type(element) => {
1966 render_semantic_ts_type(element, defined_types, specs, depth)?
1967 }
1968 IdlArrayElementSnapshot::TypeName(name) => render_semantic_ts_type(
1969 &IdlTypeSnapshot::Simple(name.clone()),
1970 defined_types,
1971 specs,
1972 depth,
1973 )?,
1974 IdlArrayElementSnapshot::Size(_) => continue,
1975 };
1976 return Some(format!("{}[]", maybe_paren(&inner)));
1977 }
1978 return Some(defined_types.parse_snapshot_type(ty).ts_type);
1979 }
1980 _ => {}
1981 }
1982
1983 let IdlTypeSnapshot::Defined(defined) = ty else {
1984 return Some(defined_types.parse_snapshot_type(ty).ts_type);
1985 };
1986 let type_name = match &defined.defined {
1987 IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
1988 IdlDefinedInnerSnapshot::Simple(simple) => simple.as_str(),
1989 };
1990 let Some(def) = defined_types.find_definition(type_name) else {
1991 return Some(defined_types.parse_snapshot_type(ty).ts_type);
1992 };
1993
1994 match &def.type_def {
1995 IdlTypeDefKindSnapshot::Struct { fields, .. } => {
1996 render_semantic_struct_type(fields, defined_types, specs, depth)
1997 }
1998 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
1999 render_semantic_enum_type(variants, defined_types, specs, depth)
2000 }
2001 IdlTypeDefKindSnapshot::TupleStruct { .. } => {
2002 Some(defined_types.parse_snapshot_type(ty).ts_type)
2003 }
2004 }
2005}
2006
2007fn render_semantic_struct_type(
2008 fields: &[crate::ast::IdlFieldSnapshot],
2009 defined_types: &mut DefinedTypes<'_>,
2010 specs: &[&SemanticFieldSpec],
2011 depth: usize,
2012) -> Option<String> {
2013 let mut field_entries: Vec<String> = Vec::new();
2014 for field in fields {
2015 let child_specs: Vec<&SemanticFieldSpec> = specs
2016 .iter()
2017 .copied()
2018 .filter(|spec| spec.relative_path.get(depth) == Some(&field.name))
2019 .collect();
2020 let field_ts = if child_specs.is_empty() {
2021 defined_types.parse_snapshot_type(&field.type_).ts_type
2022 } else {
2023 render_semantic_ts_type(&field.type_, defined_types, &child_specs, depth + 1)?
2024 };
2025 field_entries.push(format!(
2026 "{}: {};",
2027 render_ts_property_name(&field.name),
2028 field_ts
2029 ));
2030 }
2031 Some(format!("{{ {} }}", field_entries.join(" ")))
2032}
2033
2034fn render_semantic_enum_type(
2035 variants: &[crate::ast::IdlEnumVariantSnapshot],
2036 defined_types: &mut DefinedTypes<'_>,
2037 specs: &[&SemanticFieldSpec],
2038 depth: usize,
2039) -> Option<String> {
2040 use crate::ast::IdlEnumVariantFieldSnapshot;
2041
2042 let mut rendered_variants: Vec<String> = Vec::new();
2043 for variant in variants {
2044 if variant.fields.is_empty() {
2045 rendered_variants.push(format!("'{}'", variant.name));
2046 continue;
2047 }
2048
2049 let named: Vec<_> = variant
2050 .fields
2051 .iter()
2052 .filter_map(|field| match field {
2053 IdlEnumVariantFieldSnapshot::Named(named) => Some(named),
2054 IdlEnumVariantFieldSnapshot::Tuple(_) => None,
2055 })
2056 .collect();
2057 if named.len() == variant.fields.len() {
2058 let mut field_entries: Vec<String> = Vec::new();
2059 for field in named {
2060 let child_specs: Vec<&SemanticFieldSpec> = specs
2061 .iter()
2062 .copied()
2063 .filter(|spec| {
2064 spec.relative_path.get(depth) == Some(&variant.name)
2065 && spec.relative_path.get(depth + 1) == Some(&field.name)
2066 })
2067 .collect();
2068 let field_ts = if child_specs.is_empty() {
2069 defined_types.parse_snapshot_type(&field.type_).ts_type
2070 } else {
2071 render_semantic_ts_type(&field.type_, defined_types, &child_specs, depth + 2)?
2072 };
2073 field_entries.push(format!(
2074 "{}: {};",
2075 render_ts_property_name(&field.name),
2076 field_ts
2077 ));
2078 }
2079 rendered_variants.push(format!(
2080 "{{ {}: {{ {} }} }}",
2081 render_ts_property_name(&variant.name),
2082 field_entries.join(" ")
2083 ));
2084 continue;
2085 }
2086
2087 let tuple: Vec<_> = variant
2088 .fields
2089 .iter()
2090 .filter_map(|field| match field {
2091 IdlEnumVariantFieldSnapshot::Tuple(ty) => {
2092 Some(defined_types.parse_snapshot_type(ty).ts_type)
2093 }
2094 IdlEnumVariantFieldSnapshot::Named(_) => None,
2095 })
2096 .collect();
2097 rendered_variants.push(format!(
2098 "{{ {}: [{}] }}",
2099 render_ts_property_name(&variant.name),
2100 tuple.join(", ")
2101 ));
2102 }
2103 Some(rendered_variants.join(" | "))
2104}
2105
2106fn render_semantic_raw_expression(
2107 ty: &IdlTypeSnapshot,
2108 value_expr: &str,
2109 defined_types: &mut DefinedTypes<'_>,
2110 specs: &[&SemanticFieldSpec],
2111 depth: usize,
2112) -> Option<String> {
2113 if let Some(spec) = specs.iter().find(|spec| spec.relative_path.len() == depth) {
2114 return Some(render_amount_resolution_expression(value_expr, spec));
2115 }
2116 if !specs.iter().any(|spec| spec.relative_path.len() > depth) {
2117 return Some(value_expr.to_string());
2118 }
2119
2120 match ty {
2121 IdlTypeSnapshot::Vec(vec_type) => {
2122 let element_expr = render_semantic_raw_expression(
2123 &vec_type.vec,
2124 "entry",
2125 defined_types,
2126 specs,
2127 depth,
2128 )?;
2129 return Some(format!(
2130 "await Promise.all({}.map(async (entry) => ({})))",
2131 value_expr, element_expr
2132 ));
2133 }
2134 IdlTypeSnapshot::Array(array_type) => {
2135 for part in &array_type.array {
2136 let element_expr = match part {
2137 IdlArrayElementSnapshot::Type(element) => render_semantic_raw_expression(
2138 element,
2139 "entry",
2140 defined_types,
2141 specs,
2142 depth,
2143 )?,
2144 IdlArrayElementSnapshot::TypeName(name) => render_semantic_raw_expression(
2145 &IdlTypeSnapshot::Simple(name.clone()),
2146 "entry",
2147 defined_types,
2148 specs,
2149 depth,
2150 )?,
2151 IdlArrayElementSnapshot::Size(_) => continue,
2152 };
2153 return Some(format!(
2154 "await Promise.all({}.map(async (entry) => ({})))",
2155 value_expr, element_expr
2156 ));
2157 }
2158 return Some(value_expr.to_string());
2159 }
2160 _ => {}
2161 }
2162
2163 let IdlTypeSnapshot::Defined(defined) = ty else {
2164 return Some(value_expr.to_string());
2165 };
2166 let type_name = match &defined.defined {
2167 IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
2168 IdlDefinedInnerSnapshot::Simple(simple) => simple.as_str(),
2169 };
2170 let Some(def) = defined_types.find_definition(type_name) else {
2171 return Some(value_expr.to_string());
2172 };
2173
2174 match &def.type_def {
2175 IdlTypeDefKindSnapshot::Struct { fields, .. } => {
2176 render_semantic_raw_struct_expression(fields, value_expr, defined_types, specs, depth)
2177 }
2178 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
2179 render_semantic_raw_enum_expression(variants, value_expr, defined_types, specs, depth)
2180 }
2181 IdlTypeDefKindSnapshot::TupleStruct { .. } => Some(value_expr.to_string()),
2182 }
2183}
2184
2185fn render_semantic_raw_struct_expression(
2186 fields: &[crate::ast::IdlFieldSnapshot],
2187 value_expr: &str,
2188 defined_types: &mut DefinedTypes<'_>,
2189 specs: &[&SemanticFieldSpec],
2190 depth: usize,
2191) -> Option<String> {
2192 let mut overrides: Vec<String> = Vec::new();
2193 for field in fields {
2194 let child_specs: Vec<&SemanticFieldSpec> = specs
2195 .iter()
2196 .copied()
2197 .filter(|spec| spec.relative_path.get(depth) == Some(&field.name))
2198 .collect();
2199 if child_specs.is_empty() {
2200 continue;
2201 }
2202 let child_expr = render_semantic_raw_expression(
2203 &field.type_,
2204 &render_ts_property_access(value_expr, &field.name),
2205 defined_types,
2206 &child_specs,
2207 depth + 1,
2208 )?;
2209 overrides.push(format!(
2210 "{}: {}",
2211 render_ts_property_name(&field.name),
2212 child_expr
2213 ));
2214 }
2215 if overrides.is_empty() {
2216 Some(value_expr.to_string())
2217 } else {
2218 Some(format!("{{ ...{}, {} }}", value_expr, overrides.join(", ")))
2219 }
2220}
2221
2222fn render_semantic_raw_enum_expression(
2223 variants: &[crate::ast::IdlEnumVariantSnapshot],
2224 value_expr: &str,
2225 defined_types: &mut DefinedTypes<'_>,
2226 specs: &[&SemanticFieldSpec],
2227 depth: usize,
2228) -> Option<String> {
2229 use crate::ast::IdlEnumVariantFieldSnapshot;
2230
2231 let mut branches: Vec<String> = Vec::new();
2232 for variant in variants {
2233 let variant_specs: Vec<&SemanticFieldSpec> = specs
2234 .iter()
2235 .copied()
2236 .filter(|spec| spec.relative_path.get(depth) == Some(&variant.name))
2237 .collect();
2238 if variant_specs.is_empty() {
2239 continue;
2240 }
2241
2242 let named: Vec<_> = variant
2243 .fields
2244 .iter()
2245 .filter_map(|field| match field {
2246 IdlEnumVariantFieldSnapshot::Named(named) => Some(named),
2247 IdlEnumVariantFieldSnapshot::Tuple(_) => None,
2248 })
2249 .collect();
2250 if named.len() != variant.fields.len() {
2251 return Some(value_expr.to_string());
2252 }
2253
2254 let variant_value_expr = render_ts_property_access(value_expr, &variant.name);
2255 let mut overrides: Vec<String> = Vec::new();
2256 for field in named {
2257 let child_specs: Vec<&SemanticFieldSpec> = variant_specs
2258 .iter()
2259 .copied()
2260 .filter(|spec| spec.relative_path.get(depth + 1) == Some(&field.name))
2261 .collect();
2262 if child_specs.is_empty() {
2263 continue;
2264 }
2265 let child_expr = render_semantic_raw_expression(
2266 &field.type_,
2267 &render_ts_property_access(&variant_value_expr, &field.name),
2268 defined_types,
2269 &child_specs,
2270 depth + 2,
2271 )?;
2272 overrides.push(format!(
2273 "{}: {}",
2274 render_ts_property_name(&field.name),
2275 child_expr
2276 ));
2277 }
2278 let payload_expr = if overrides.is_empty() {
2279 variant_value_expr.clone()
2280 } else {
2281 format!("{{ ...{}, {} }}", variant_value_expr, overrides.join(", "))
2282 };
2283 branches.push(format!(
2284 "('{}' in {value_expr} ? {{ {}: {} }} : __ELSE__)",
2285 escape_single_quotes(&variant.name),
2286 render_ts_property_name(&variant.name),
2287 payload_expr,
2288 value_expr = value_expr,
2289 ));
2290 }
2291
2292 let mut rendered = value_expr.to_string();
2293 for branch in branches.into_iter().rev() {
2294 rendered = branch.replace("__ELSE__", &rendered);
2295 }
2296 Some(rendered)
2297}
2298
2299fn render_semantic_params_interface(
2300 name: &str,
2301 parsed_args: &[(String, ParsedArgType)],
2302 instruction_snapshot: Option<&IdlInstructionSnapshot>,
2303 user_params: &[UserParam],
2304 resolve_params: &BTreeMap<String, String>,
2305 semantic_specs: &[SemanticFieldSpec],
2306 defined_types: &mut DefinedTypes<'_>,
2307) -> String {
2308 let mut lines: Vec<String> = Vec::new();
2309
2310 for (arg_name, parsed) in parsed_args {
2311 let arg_specs: Vec<&SemanticFieldSpec> = semantic_specs
2312 .iter()
2313 .filter(|spec| spec.root_arg_name == *arg_name)
2314 .collect();
2315 let ts_type = if arg_specs.is_empty() {
2316 parsed.ts_type.clone()
2317 } else if let Some(snapshot_arg) = instruction_snapshot
2318 .and_then(|snapshot| snapshot.args.iter().find(|arg| arg.name == *arg_name))
2319 {
2320 render_semantic_ts_type(&snapshot_arg.type_, defined_types, &arg_specs, 0)
2321 .unwrap_or_else(|| parsed.ts_type.clone())
2322 } else {
2323 "AmountInput".to_string()
2324 };
2325 lines.push(format!(
2326 " {}: {};",
2327 render_ts_property_name(arg_name),
2328 ts_type
2329 ));
2330 }
2331
2332 let mut extra_params: BTreeSet<String> = BTreeSet::new();
2333 for spec in semantic_specs {
2334 if let Some(extra) = &spec.decimals_override_name {
2335 extra_params.insert(extra.clone());
2336 }
2337 }
2338 for extra_param in extra_params {
2339 lines.push(format!(
2340 " {}?: number;",
2341 render_ts_property_name(&extra_param)
2342 ));
2343 }
2344
2345 for param in user_params {
2346 let optional = if param.optional { "?" } else { "" };
2347 lines.push(format!(
2348 " {}{}: string;",
2349 render_ts_property_name(¶m.name),
2350 optional
2351 ));
2352 }
2353
2354 if !resolve_params.is_empty() {
2355 let resolve_lines: Vec<String> = resolve_params
2356 .iter()
2357 .map(|(name, ts_type)| format!(" {}?: {};", render_ts_property_name(name), ts_type))
2358 .collect();
2359 lines.push(format!(
2360 " resolve?: {{\n{}\n }};",
2361 resolve_lines.join("\n")
2362 ));
2363 }
2364
2365 lines.push(" build?: BuildOptions;".to_string());
2366
2367 let body = if lines.is_empty() {
2368 " // This instruction takes no arguments or user-provided accounts.".to_string()
2369 } else {
2370 lines.join("\n")
2371 };
2372
2373 format!("export interface {} {{\n{}\n}}", name, body)
2374}
2375
2376fn normalize_seed_arg_type(raw: &str) -> Option<String> {
2380 let t = raw.rsplit("::").next().unwrap_or(raw).trim();
2381 if let Some(width) = t.strip_prefix('u').or_else(|| t.strip_prefix('i')) {
2382 if matches!(width, "8" | "16" | "32" | "64" | "128") {
2383 return Some(t.to_string());
2384 }
2385 return None;
2386 }
2387 match t {
2388 "Pubkey" | "pubkey" | "publicKey" | "PublicKey" => Some("pubkey".to_string()),
2389 "String" | "string" | "str" => Some("string".to_string()),
2390 _ => None,
2391 }
2392}
2393
2394fn seed_arg_ts_type(raw: &str) -> Option<String> {
2395 let canonical = normalize_seed_arg_type(raw)?;
2396 let ts_type = match canonical.as_str() {
2397 "u8" | "u16" | "u32" | "i8" | "i16" | "i32" => "number",
2398 "u64" | "u128" | "i64" | "i128" => "bigint",
2399 "pubkey" => "string",
2400 "string" => "string",
2401 _ => return None,
2402 };
2403 Some(ts_type.to_string())
2404}
2405
2406fn dedupe_errors_by_code(errors: &[IdlErrorSnapshot]) -> Vec<IdlErrorSnapshot> {
2412 let mut seen: BTreeSet<u32> = BTreeSet::new();
2413 let mut by_code: BTreeMap<u32, IdlErrorSnapshot> = BTreeMap::new();
2414 for err in errors {
2415 if seen.insert(err.code) {
2416 by_code.insert(err.code, err.clone());
2417 }
2418 }
2419 by_code.into_values().collect()
2420}
2421
2422fn render_program_errors(const_name: &str, type_name: &str, errors: &[IdlErrorSnapshot]) -> String {
2423 if errors.is_empty() {
2424 return format!(
2425 "/** Program errors for this stack (none declared in the IDL). */\nexport type {} = never;\n\nconst {}: ErrorMetadata[] = [];",
2426 type_name, const_name
2427 );
2428 }
2429
2430 let type_decl = format!(
2431 "/** Union of all program errors declared across this stack's instructions. */\nexport type {} =\n{};",
2432 type_name,
2433 error_union_variants(errors)
2434 );
2435
2436 let entries: Vec<String> = errors
2437 .iter()
2438 .map(|err| {
2439 format!(
2440 " {{ code: {}, name: '{}', msg: '{}' }},",
2441 err.code,
2442 err.name,
2443 escape_single_quotes(err.msg.as_deref().unwrap_or(""))
2444 )
2445 })
2446 .collect();
2447 let const_decl = format!(
2448 "const {}: ErrorMetadata[] = [\n{}\n];",
2449 const_name,
2450 entries.join("\n")
2451 );
2452
2453 format!("{}\n\n{}", type_decl, const_decl)
2454}
2455
2456fn error_union_variants(errors: &[IdlErrorSnapshot]) -> String {
2458 errors
2459 .iter()
2460 .map(|err| {
2461 format!(
2462 " | {{ code: {}; name: '{}'; msg: string }}",
2463 err.code, err.name
2464 )
2465 })
2466 .collect::<Vec<_>>()
2467 .join("\n")
2468}
2469
2470fn escape_single_quotes(s: &str) -> String {
2475 s.replace('\\', "\\\\")
2476 .replace('\'', "\\'")
2477 .replace(['\n', '\r'], " ")
2478}
2479
2480fn render_docs(docs: &[String]) -> String {
2481 if docs.is_empty() {
2482 return String::new();
2483 }
2484 let lines: Vec<String> = docs
2485 .iter()
2486 .map(|line| format!(" * {}", line.trim()))
2487 .collect();
2488 format!("/**\n{}\n */\n", lines.join("\n"))
2489}
2490
2491#[cfg(test)]
2492mod tests {
2493 use super::*;
2494 use crate::ast::{
2495 AmountDecimalsSource, InstructionAccountDef, InstructionAmountHint, InstructionArgDef,
2496 };
2497
2498 fn arg(name: &str, ty: &str) -> InstructionArgDef {
2499 InstructionArgDef {
2500 name: name.to_string(),
2501 arg_type: ty.to_string(),
2502 docs: vec![],
2503 amount_hint: None,
2504 }
2505 }
2506
2507 fn amount_arg(
2508 name: &str,
2509 ty: &str,
2510 decimals_source: AmountDecimalsSource,
2511 ) -> InstructionArgDef {
2512 InstructionArgDef {
2513 name: name.to_string(),
2514 arg_type: ty.to_string(),
2515 docs: vec![],
2516 amount_hint: Some(InstructionAmountHint { decimals_source }),
2517 }
2518 }
2519
2520 fn user_account(name: &str) -> InstructionAccountDef {
2521 InstructionAccountDef {
2522 name: name.to_string(),
2523 is_signer: false,
2524 is_writable: false,
2525 resolution: AccountResolution::UserProvided,
2526 is_optional: false,
2527 docs: vec![],
2528 }
2529 }
2530
2531 fn idl(name: &str, program_id: &str, errors: Vec<IdlErrorSnapshot>) -> IdlSnapshot {
2532 IdlSnapshot {
2533 name: name.to_string(),
2534 program_id: Some(program_id.to_string()),
2535 version: "0.1.0".to_string(),
2536 accounts: vec![],
2537 instructions: vec![],
2538 types: vec![],
2539 events: vec![],
2540 errors,
2541 discriminant_size: 1,
2542 }
2543 }
2544
2545 #[test]
2546 fn parses_primitive_and_wrapper_arg_types() {
2547 let u64 = parse_arg_type("u64");
2548 assert_eq!(u64.schema, "'u64'");
2549 assert_eq!(u64.ts_type, "bigint");
2550 assert!(u64.supported);
2551
2552 let pk = parse_arg_type("solana_pubkey::Pubkey");
2553 assert_eq!(pk.schema, "'pubkey'");
2554 assert_eq!(pk.ts_type, "string");
2555
2556 let opt = parse_arg_type("Option<u64>");
2557 assert_eq!(opt.schema, "{ option: 'u64' }");
2558 assert_eq!(opt.ts_type, "bigint | null");
2559
2560 let vec = parse_arg_type("Vec<u8>");
2561 assert_eq!(vec.schema, "{ vec: 'u8' }");
2562 assert_eq!(vec.ts_type, "number[]");
2563
2564 let arr = parse_arg_type("[u8; 32]");
2565 assert_eq!(arr.schema, "{ array: ['u8', 32] }");
2566 assert_eq!(arr.ts_type, "number[]");
2567
2568 let opt_vec = parse_arg_type("Vec<Option<u64>>");
2569 assert_eq!(opt_vec.ts_type, "(bigint | null)[]");
2570 }
2571
2572 #[test]
2573 fn defined_types_are_unsupported_without_a_lookup() {
2574 let defined = parse_arg_type("createFixedDelegationData");
2575 assert!(!defined.supported);
2576 }
2577
2578 fn struct_def(name: &str, fields: Vec<(&str, IdlTypeSnapshot)>) -> IdlTypeDefSnapshot {
2579 IdlTypeDefSnapshot {
2580 name: name.to_string(),
2581 docs: vec![],
2582 serialization: None,
2583 type_def: IdlTypeDefKindSnapshot::Struct {
2584 kind: "struct".to_string(),
2585 fields: fields
2586 .into_iter()
2587 .map(|(n, t)| crate::ast::IdlFieldSnapshot {
2588 name: n.to_string(),
2589 type_: t,
2590 amount_hint: None,
2591 })
2592 .collect(),
2593 },
2594 }
2595 }
2596
2597 fn simple(t: &str) -> IdlTypeSnapshot {
2598 IdlTypeSnapshot::Simple(t.to_string())
2599 }
2600
2601 fn defined(name: &str) -> IdlTypeSnapshot {
2602 IdlTypeSnapshot::Defined(crate::ast::IdlDefinedTypeSnapshot {
2603 defined: IdlDefinedInnerSnapshot::Named {
2604 name: name.to_string(),
2605 },
2606 })
2607 }
2608
2609 fn field(name: &str, type_: IdlTypeSnapshot) -> crate::ast::IdlFieldSnapshot {
2610 crate::ast::IdlFieldSnapshot {
2611 name: name.to_string(),
2612 type_,
2613 amount_hint: None,
2614 }
2615 }
2616
2617 fn hinted_field(
2618 name: &str,
2619 type_: IdlTypeSnapshot,
2620 amount_hint: arete_idl::IdlAmountHint,
2621 ) -> crate::ast::IdlFieldSnapshot {
2622 crate::ast::IdlFieldSnapshot {
2623 name: name.to_string(),
2624 type_,
2625 amount_hint: Some(amount_hint),
2626 }
2627 }
2628
2629 fn option(type_: IdlTypeSnapshot) -> IdlTypeSnapshot {
2630 IdlTypeSnapshot::Option(crate::ast::IdlOptionTypeSnapshot {
2631 option: Box::new(type_),
2632 })
2633 }
2634
2635 fn vec_type(type_: IdlTypeSnapshot) -> IdlTypeSnapshot {
2636 IdlTypeSnapshot::Vec(crate::ast::IdlVecTypeSnapshot {
2637 vec: Box::new(type_),
2638 })
2639 }
2640
2641 fn array_type(type_: IdlTypeSnapshot, size: u32) -> IdlTypeSnapshot {
2642 IdlTypeSnapshot::Array(crate::ast::IdlArrayTypeSnapshot {
2643 array: vec![
2644 IdlArrayElementSnapshot::Type(type_),
2645 IdlArrayElementSnapshot::Size(size),
2646 ],
2647 })
2648 }
2649
2650 fn hash_map(key: IdlTypeSnapshot, value: IdlTypeSnapshot) -> IdlTypeSnapshot {
2651 IdlTypeSnapshot::HashMap(crate::ast::IdlHashMapTypeSnapshot {
2652 hash_map: (Box::new(key), Box::new(value)),
2653 })
2654 }
2655
2656 fn instruction_snapshot(
2657 name: &str,
2658 discriminator: Vec<u8>,
2659 args: Vec<crate::ast::IdlFieldSnapshot>,
2660 ) -> IdlInstructionSnapshot {
2661 IdlInstructionSnapshot {
2662 name: name.to_string(),
2663 discriminator,
2664 discriminant: None,
2665 docs: vec![],
2666 accounts: vec![],
2667 args,
2668 }
2669 }
2670
2671 #[test]
2672 fn parses_top_level_args_from_idl_snapshots_before_lossy_strings() {
2673 let mut idl = idl("demo", "Prog111", vec![]);
2674 idl.instructions = vec![instruction_snapshot(
2675 "deposit",
2676 vec![9],
2677 vec![field("amount", simple("u64"))],
2678 )];
2679 let idls = vec![idl];
2680
2681 let instr = InstructionDef {
2682 name: "deposit".to_string(),
2683 discriminator: vec![9],
2684 discriminator_size: 1,
2685 accounts: vec![],
2686 args: vec![arg("amount", "DefinitelyUnsupported")],
2687 errors: vec![],
2688 program_id: Some("Prog111".to_string()),
2689 docs: vec![],
2690 };
2691
2692 let out = generate_instructions_code(
2693 "Demo",
2694 std::slice::from_ref(&instr),
2695 &idls,
2696 &BTreeMap::new(),
2697 &["Prog111".to_string()],
2698 &HashSet::new(),
2699 );
2700
2701 assert_eq!(out.stack_entries.len(), 1, "warnings: {:?}", out.warnings);
2702 assert!(out.code.contains("amount: bigint;"));
2703 assert!(out.code.contains("{ name: 'amount', type: 'u64' }"));
2704 }
2705
2706 #[test]
2707 fn resolves_struct_args_with_nesting_and_enums() {
2708 let mut idl = idl("demo", "Prog111", vec![]);
2709 idl.types = vec![
2710 struct_def(
2711 "transferData",
2712 vec![
2713 ("amount", simple("u64")),
2714 ("terms", defined("planTerms")),
2715 ("status", defined("planStatus")),
2716 ],
2717 ),
2718 struct_def("planTerms", vec![("periodHours", simple("u64"))]),
2719 IdlTypeDefSnapshot {
2720 name: "planStatus".to_string(),
2721 docs: vec![],
2722 serialization: None,
2723 type_def: IdlTypeDefKindSnapshot::Enum {
2724 kind: "enum".to_string(),
2725 variants: vec![
2726 crate::ast::IdlEnumVariantSnapshot {
2727 name: "Active".to_string(),
2728 fields: vec![],
2729 },
2730 crate::ast::IdlEnumVariantSnapshot {
2731 name: "Sunset".to_string(),
2732 fields: vec![crate::ast::IdlEnumVariantFieldSnapshot::Named(
2733 crate::ast::IdlFieldSnapshot {
2734 name: "endTs".to_string(),
2735 type_: simple("i64"),
2736 amount_hint: None,
2737 },
2738 )],
2739 },
2740 ],
2741 },
2742 },
2743 ];
2744 let idls = vec![idl];
2745
2746 let instr = InstructionDef {
2747 name: "transfer".to_string(),
2748 discriminator: vec![4],
2749 discriminator_size: 1,
2750 accounts: vec![],
2751 args: vec![arg("transferData", "transferData")],
2752 errors: vec![],
2753 program_id: Some("Prog111".to_string()),
2754 docs: vec![],
2755 };
2756
2757 let out = generate_instructions_code(
2758 "Demo",
2759 std::slice::from_ref(&instr),
2760 &idls,
2761 &BTreeMap::new(),
2762 &["Prog111".to_string()],
2763 &HashSet::new(),
2764 );
2765
2766 assert_eq!(out.stack_entries.len(), 1, "warnings: {:?}", out.warnings);
2767 let code = &out.code;
2768 assert!(code.contains("export interface TransferData"));
2770 assert!(code.contains("export interface PlanTerms"));
2771 assert!(code.contains("export type PlanStatus"));
2772 assert!(code.contains("'Active'"));
2773 assert!(code.contains("{ Sunset: { endTs: bigint } }"));
2774 assert!(code.contains("{ name: 'periodHours', type: 'u64' }"));
2776 assert!(code.contains(
2777 "{ name: 'status', type: { enum: ['Active', { name: 'Sunset', fields: [{ name: 'endTs', type: 'i64' }] }] } }"
2778 ));
2779 assert!(code.contains("transferData: TransferData;"));
2781 }
2782
2783 #[test]
2784 fn resolves_string_key_maps_inside_instruction_arg_types() {
2785 let mut idl = idl("demo", "Prog111", vec![]);
2786 idl.types = vec![
2787 struct_def("authorizationData", vec![("payload", defined("payload"))]),
2788 struct_def(
2789 "payload",
2790 vec![("map", hash_map(simple("string"), defined("payloadType")))],
2791 ),
2792 IdlTypeDefSnapshot {
2793 name: "payloadType".to_string(),
2794 docs: vec![],
2795 serialization: None,
2796 type_def: IdlTypeDefKindSnapshot::Enum {
2797 kind: "enum".to_string(),
2798 variants: vec![
2799 crate::ast::IdlEnumVariantSnapshot {
2800 name: "Pubkey".to_string(),
2801 fields: vec![crate::ast::IdlEnumVariantFieldSnapshot::Tuple(simple(
2802 "publicKey",
2803 ))],
2804 },
2805 crate::ast::IdlEnumVariantSnapshot {
2806 name: "Number".to_string(),
2807 fields: vec![crate::ast::IdlEnumVariantFieldSnapshot::Tuple(simple(
2808 "u64",
2809 ))],
2810 },
2811 ],
2812 },
2813 },
2814 IdlTypeDefSnapshot {
2815 name: "mintArgs".to_string(),
2816 docs: vec![],
2817 serialization: None,
2818 type_def: IdlTypeDefKindSnapshot::Enum {
2819 kind: "enum".to_string(),
2820 variants: vec![crate::ast::IdlEnumVariantSnapshot {
2821 name: "V1".to_string(),
2822 fields: vec![
2823 crate::ast::IdlEnumVariantFieldSnapshot::Named(field(
2824 "amount",
2825 simple("u64"),
2826 )),
2827 crate::ast::IdlEnumVariantFieldSnapshot::Named(field(
2828 "authorization_data",
2829 option(defined("authorizationData")),
2830 )),
2831 ],
2832 }],
2833 },
2834 },
2835 ];
2836 idl.instructions = vec![instruction_snapshot(
2837 "Mint",
2838 vec![4],
2839 vec![field("mintArgs", defined("mintArgs"))],
2840 )];
2841 let idls = vec![idl];
2842
2843 let instr = InstructionDef {
2844 name: "Mint".to_string(),
2845 discriminator: vec![4],
2846 discriminator_size: 1,
2847 accounts: vec![],
2848 args: vec![arg("mintArgs", "mintArgs")],
2849 errors: vec![],
2850 program_id: Some("Prog111".to_string()),
2851 docs: vec![],
2852 };
2853
2854 let out = generate_instructions_code(
2855 "Demo",
2856 std::slice::from_ref(&instr),
2857 &idls,
2858 &BTreeMap::new(),
2859 &["Prog111".to_string()],
2860 &HashSet::new(),
2861 );
2862
2863 assert_eq!(out.stack_entries.len(), 1, "warnings: {:?}", out.warnings);
2864 assert!(
2865 out.warnings.is_empty(),
2866 "map-backed args should emit cleanly: {:?}",
2867 out.warnings
2868 );
2869 let code = &out.code;
2870 assert!(code.contains("export interface AuthorizationData"));
2871 assert!(code.contains("export interface Payload"));
2872 assert!(code.contains("export type PayloadType"));
2873 assert!(code.contains("map: Record<string, PayloadType>;"));
2874 assert!(code.contains(
2875 "{ name: 'map', type: { hashMap: ['string', { enum: [{ name: 'Pubkey', tuple: ['pubkey'] }, { name: 'Number', tuple: ['u64'] }] }] } }"
2876 ));
2877 assert!(code.contains("mintArgs: MintArgs;"));
2878 }
2879
2880 #[test]
2881 fn resolves_string_to_string_maps() {
2882 let mut idl = idl("demo", "Prog111", vec![]);
2883 idl.types = vec![struct_def(
2884 "tokenMetadata",
2885 vec![(
2886 "additionalMetadata",
2887 hash_map(simple("string"), simple("string")),
2888 )],
2889 )];
2890 idl.instructions = vec![instruction_snapshot(
2891 "updateTokenMetadata",
2892 vec![5],
2893 vec![field("metadata", defined("tokenMetadata"))],
2894 )];
2895 let idls = vec![idl];
2896
2897 let instr = InstructionDef {
2898 name: "updateTokenMetadata".to_string(),
2899 discriminator: vec![5],
2900 discriminator_size: 1,
2901 accounts: vec![],
2902 args: vec![arg("metadata", "tokenMetadata")],
2903 errors: vec![],
2904 program_id: Some("Prog111".to_string()),
2905 docs: vec![],
2906 };
2907
2908 let out = generate_instructions_code(
2909 "Demo",
2910 std::slice::from_ref(&instr),
2911 &idls,
2912 &BTreeMap::new(),
2913 &["Prog111".to_string()],
2914 &HashSet::new(),
2915 );
2916
2917 assert_eq!(out.stack_entries.len(), 1, "warnings: {:?}", out.warnings);
2918 assert!(out
2919 .code
2920 .contains("additionalMetadata: Record<string, string>;"));
2921 assert!(out.code.contains("{ hashMap: ['string', 'string'] }"));
2922 }
2923
2924 #[test]
2925 fn non_string_key_maps_remain_unsupported() {
2926 let mut idl = idl("demo", "Prog111", vec![]);
2927 idl.types = vec![struct_def(
2928 "tokenMetadata",
2929 vec![(
2930 "additionalMetadata",
2931 hash_map(simple("u64"), simple("string")),
2932 )],
2933 )];
2934 idl.instructions = vec![instruction_snapshot(
2935 "updateTokenMetadata",
2936 vec![6],
2937 vec![field("metadata", defined("tokenMetadata"))],
2938 )];
2939 let idls = vec![idl];
2940
2941 let instr = InstructionDef {
2942 name: "updateTokenMetadata".to_string(),
2943 discriminator: vec![6],
2944 discriminator_size: 1,
2945 accounts: vec![],
2946 args: vec![arg("metadata", "tokenMetadata")],
2947 errors: vec![],
2948 program_id: Some("Prog111".to_string()),
2949 docs: vec![],
2950 };
2951
2952 let out = generate_instructions_code(
2953 "Demo",
2954 std::slice::from_ref(&instr),
2955 &idls,
2956 &BTreeMap::new(),
2957 &["Prog111".to_string()],
2958 &HashSet::new(),
2959 );
2960
2961 assert!(out.stack_entries.is_empty());
2962 assert!(out
2963 .warnings
2964 .iter()
2965 .any(|warning| warning.contains("unsupported type")));
2966 }
2967
2968 #[test]
2969 fn recursive_defined_types_skip_with_warning() {
2970 let mut idl_snap = idl("demo", "Prog111", vec![]);
2971 idl_snap.types = vec![struct_def("node", vec![("next", defined("node"))])];
2972 let idls = vec![idl_snap];
2973
2974 let instr = InstructionDef {
2975 name: "insert".to_string(),
2976 discriminator: vec![1],
2977 discriminator_size: 1,
2978 accounts: vec![],
2979 args: vec![arg("node", "node")],
2980 errors: vec![],
2981 program_id: Some("Prog111".to_string()),
2982 docs: vec![],
2983 };
2984 let out = generate_instructions_code(
2985 "Demo",
2986 std::slice::from_ref(&instr),
2987 &idls,
2988 &BTreeMap::new(),
2989 &["Prog111".to_string()],
2990 &HashSet::new(),
2991 );
2992 assert!(out.stack_entries.is_empty());
2993 assert!(out.warnings.iter().any(|w| w.contains("recursive")));
2994 }
2995
2996 #[test]
2997 fn defined_type_name_collisions_get_input_suffix() {
2998 let mut idl_snap = idl("demo", "Prog111", vec![]);
2999 idl_snap.types = vec![struct_def("planTerms", vec![("amount", simple("u64"))])];
3000 let idls = vec![idl_snap];
3001
3002 let instr = InstructionDef {
3003 name: "setTerms".to_string(),
3004 discriminator: vec![2],
3005 discriminator_size: 1,
3006 accounts: vec![],
3007 args: vec![arg("terms", "planTerms")],
3008 errors: vec![],
3009 program_id: Some("Prog111".to_string()),
3010 docs: vec![],
3011 };
3012
3013 let reserved: HashSet<String> = ["PlanTerms".to_string()].into_iter().collect();
3015 let out = generate_instructions_code(
3016 "Demo",
3017 std::slice::from_ref(&instr),
3018 &idls,
3019 &BTreeMap::new(),
3020 &["Prog111".to_string()],
3021 &reserved,
3022 );
3023 assert!(out.code.contains("export interface PlanTermsInput"));
3024 assert!(out.code.contains("terms: PlanTermsInput;"));
3025 assert!(out.warnings.iter().any(|w| w.contains("collides")));
3026 }
3027
3028 #[test]
3029 fn skips_instructions_with_unsupported_args() {
3030 let instr = InstructionDef {
3031 name: "subscribe".to_string(),
3032 discriminator: vec![3],
3033 discriminator_size: 1,
3034 accounts: vec![],
3035 args: vec![arg("data", "subscribeData")],
3036 errors: vec![],
3037 program_id: None,
3038 docs: vec![],
3039 };
3040 let out = generate_instructions_code(
3041 "Subscriptions",
3042 std::slice::from_ref(&instr),
3043 &[],
3044 &BTreeMap::new(),
3045 &["Prog111".to_string()],
3046 &HashSet::new(),
3047 );
3048 assert!(out.stack_entries.is_empty());
3049 assert!(out.warnings.iter().any(|w| w.contains("subscribe")));
3050 }
3051
3052 #[test]
3053 fn emits_handler_with_signer_known_and_user_provided_accounts() {
3054 let instr = InstructionDef {
3055 name: "closeSubscriptionAuthority".to_string(),
3056 discriminator: vec![6],
3057 discriminator_size: 1,
3058 accounts: vec![
3059 InstructionAccountDef {
3060 name: "user".to_string(),
3061 is_signer: true,
3062 is_writable: true,
3063 resolution: AccountResolution::Signer,
3064 is_optional: false,
3065 docs: vec![],
3066 },
3067 InstructionAccountDef {
3068 name: "subscriptionAuthority".to_string(),
3069 is_signer: false,
3070 is_writable: true,
3071 resolution: AccountResolution::UserProvided,
3072 is_optional: false,
3073 docs: vec![],
3074 },
3075 ],
3076 args: vec![],
3077 errors: vec![],
3078 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3079 docs: vec![],
3080 };
3081
3082 let idls = vec![idl(
3083 "subscriptions",
3084 "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44",
3085 vec![IdlErrorSnapshot {
3086 code: 130,
3087 name: "unauthorized".to_string(),
3088 msg: Some("Caller not authorized".to_string()),
3089 }],
3090 )];
3091 let out = generate_instructions_code(
3092 "Subscriptions",
3093 std::slice::from_ref(&instr),
3094 &idls,
3095 &BTreeMap::new(),
3096 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3097 &HashSet::new(),
3098 );
3099
3100 assert_eq!(
3101 out.stack_entries,
3102 vec![StackInstructionEntry {
3103 program_key: Some("subscriptions".to_string()),
3104 instruction_name: "closeSubscriptionAuthority".to_string(),
3105 runtime_program_key: Some("subscriptions".to_string()),
3106 handler_const: "closeSubscriptionAuthorityInstruction".to_string(),
3107 params_type: "CloseSubscriptionAuthorityParams".to_string(),
3108 semantic_params_type: Some("CloseSubscriptionAuthorityParams".to_string()),
3109 semantic_extra_params: vec![],
3110 semantic_amount_args: vec![],
3111 }]
3112 );
3113 assert!(out.needs_runtime_import);
3114 let code = &out.code;
3115 assert!(code.contains("export interface CloseSubscriptionAuthorityParams"));
3116 assert!(code.contains("subscriptionAuthority: string;"));
3117 assert!(code.contains("category: 'signer'"));
3118 assert!(code.contains("category: 'userProvided'"));
3119 assert!(code.contains(
3120 "export const closeSubscriptionAuthorityInstruction = createInstructionHandler<CloseSubscriptionAuthorityParams, CloseSubscriptionAuthorityError>"
3121 ));
3122 assert!(code.contains("SUBSCRIPTIONS_PROGRAM_ERRORS: ErrorMetadata[]"));
3123 assert!(code.contains("code: 130, name: 'unauthorized'"));
3124 }
3125
3126 #[test]
3127 fn inlines_pda_ref_seeds_including_raw_bytes() {
3128 let mut program_pdas: BTreeMap<String, PdaDefinition> = BTreeMap::new();
3129 program_pdas.insert(
3130 "subscriptionAuthority".to_string(),
3131 PdaDefinition {
3132 name: "subscriptionAuthority".to_string(),
3133 seeds: vec![
3134 PdaSeedDef::Literal {
3135 value: "SubscriptionAuthority".to_string(),
3136 },
3137 PdaSeedDef::Bytes {
3138 value: vec![1, 2, 255],
3139 },
3140 PdaSeedDef::AccountRef {
3141 account_name: "owner".to_string(),
3142 },
3143 PdaSeedDef::AccountRef {
3144 account_name: "tokenMint".to_string(),
3145 },
3146 ],
3147 program_id: None,
3148 },
3149 );
3150 let mut pdas = BTreeMap::new();
3151 pdas.insert("subscriptions".to_string(), program_pdas);
3152
3153 let instr = InstructionDef {
3154 name: "initSubscriptionAuthority".to_string(),
3155 discriminator: vec![0],
3156 discriminator_size: 1,
3157 accounts: vec![
3158 InstructionAccountDef {
3159 name: "owner".to_string(),
3160 is_signer: true,
3161 is_writable: true,
3162 resolution: AccountResolution::Signer,
3163 is_optional: false,
3164 docs: vec![],
3165 },
3166 InstructionAccountDef {
3167 name: "subscriptionAuthority".to_string(),
3168 is_signer: false,
3169 is_writable: true,
3170 resolution: AccountResolution::PdaRef {
3171 pda_name: "subscriptionAuthority".to_string(),
3172 },
3173 is_optional: false,
3174 docs: vec![],
3175 },
3176 InstructionAccountDef {
3177 name: "tokenMint".to_string(),
3178 is_signer: false,
3179 is_writable: false,
3180 resolution: AccountResolution::UserProvided,
3181 is_optional: false,
3182 docs: vec![],
3183 },
3184 ],
3185 args: vec![],
3186 errors: vec![],
3187 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3188 docs: vec![],
3189 };
3190
3191 let out = generate_instructions_code(
3192 "Subscriptions",
3193 std::slice::from_ref(&instr),
3194 &[],
3195 &pdas,
3196 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3197 &HashSet::new(),
3198 );
3199 let code = &out.code;
3200 assert!(code.contains("category: 'pda'"));
3201 assert!(code.contains("{ type: 'literal', value: 'SubscriptionAuthority' }"));
3202 assert!(code.contains("{ type: 'bytes', value: [1, 2, 255] }"));
3203 assert!(code.contains("{ type: 'accountRef', accountName: 'owner' }"));
3204 assert!(code.contains("tokenMint: string;"));
3206 assert!(code.contains("subscriptionAuthority?: string;"));
3207 assert!(
3208 out.warnings.is_empty(),
3209 "no degradation expected: {:?}",
3210 out.warnings
3211 );
3212 }
3213
3214 #[test]
3215 fn emits_typed_arg_seeds_with_instr_args_fallback() {
3216 let mut program_pdas: BTreeMap<String, PdaDefinition> = BTreeMap::new();
3217 program_pdas.insert(
3218 "round".to_string(),
3219 PdaDefinition {
3220 name: "round".to_string(),
3221 seeds: vec![
3222 PdaSeedDef::Literal {
3223 value: "round".to_string(),
3224 },
3225 PdaSeedDef::ArgRef {
3227 arg_name: "roundId".to_string(),
3228 arg_type: Some("u32".to_string()),
3229 },
3230 PdaSeedDef::ArgRef {
3232 arg_name: "owner".to_string(),
3233 arg_type: None,
3234 },
3235 ],
3236 program_id: None,
3237 },
3238 );
3239 let mut pdas = BTreeMap::new();
3240 pdas.insert("demo".to_string(), program_pdas);
3241
3242 let instr = InstructionDef {
3243 name: "commit".to_string(),
3244 discriminator: vec![1],
3245 discriminator_size: 1,
3246 accounts: vec![InstructionAccountDef {
3247 name: "round".to_string(),
3248 is_signer: false,
3249 is_writable: true,
3250 resolution: AccountResolution::PdaRef {
3251 pda_name: "round".to_string(),
3252 },
3253 is_optional: false,
3254 docs: vec![],
3255 }],
3256 args: vec![arg("roundId", "u32"), arg("owner", "solana_pubkey::Pubkey")],
3257 errors: vec![],
3258 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3259 docs: vec![],
3260 };
3261
3262 let out = generate_instructions_code(
3263 "Demo",
3264 std::slice::from_ref(&instr),
3265 &[],
3266 &pdas,
3267 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3268 &HashSet::new(),
3269 );
3270 let code = &out.code;
3271 assert!(code.contains("{ type: 'argRef', argName: 'roundId', argType: 'u32' }"));
3272 assert!(
3273 code.contains("{ type: 'argRef', argName: 'owner', argType: 'pubkey' }"),
3274 "path-qualified Pubkey arg type should normalize via instr.args fallback: {}",
3275 code
3276 );
3277 }
3278
3279 #[test]
3280 fn untypeable_arg_seed_emits_without_arg_type_and_warns() {
3281 let mut program_pdas: BTreeMap<String, PdaDefinition> = BTreeMap::new();
3282 program_pdas.insert(
3283 "vault".to_string(),
3284 PdaDefinition {
3285 name: "vault".to_string(),
3286 seeds: vec![PdaSeedDef::ArgRef {
3287 arg_name: "data".to_string(),
3288 arg_type: None,
3289 }],
3290 program_id: None,
3291 },
3292 );
3293 let mut pdas = BTreeMap::new();
3294 pdas.insert("demo".to_string(), program_pdas);
3295
3296 let instr = InstructionDef {
3297 name: "store".to_string(),
3298 discriminator: vec![2],
3299 discriminator_size: 1,
3300 accounts: vec![InstructionAccountDef {
3301 name: "vault".to_string(),
3302 is_signer: false,
3303 is_writable: true,
3304 resolution: AccountResolution::PdaRef {
3305 pda_name: "vault".to_string(),
3306 },
3307 is_optional: false,
3308 docs: vec![],
3309 }],
3310 args: vec![arg("data", "Vec<u8>")],
3311 errors: vec![],
3312 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3313 docs: vec![],
3314 };
3315
3316 let out = generate_instructions_code(
3317 "Demo",
3318 std::slice::from_ref(&instr),
3319 &[],
3320 &pdas,
3321 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3322 &HashSet::new(),
3323 );
3324 assert!(out.code.contains("{ type: 'argRef', argName: 'data' }"));
3325 assert!(
3326 out.warnings
3327 .iter()
3328 .any(|w| w.contains("heuristic encoding")),
3329 "expected soft warning, got {:?}",
3330 out.warnings
3331 );
3332 }
3333
3334 #[test]
3335 fn nested_arg_seed_uses_struct_root_without_degrading() {
3336 let mut program_pdas: BTreeMap<String, PdaDefinition> = BTreeMap::new();
3337 program_pdas.insert(
3338 "proposal".to_string(),
3339 PdaDefinition {
3340 name: "proposal".to_string(),
3341 seeds: vec![PdaSeedDef::ArgRef {
3342 arg_name: "args.transactionIndex".to_string(),
3343 arg_type: Some("u64".to_string()),
3344 }],
3345 program_id: None,
3346 },
3347 );
3348 let mut pdas = BTreeMap::new();
3349 pdas.insert("demo".to_string(), program_pdas);
3350
3351 let instr = InstructionDef {
3352 name: "proposalCreate".to_string(),
3353 discriminator: vec![3],
3354 discriminator_size: 1,
3355 accounts: vec![InstructionAccountDef {
3356 name: "proposal".to_string(),
3357 is_signer: false,
3358 is_writable: true,
3359 resolution: AccountResolution::PdaRef {
3360 pda_name: "proposal".to_string(),
3361 },
3362 is_optional: false,
3363 docs: vec![],
3364 }],
3365 args: vec![arg("args", "u64")],
3366 errors: vec![],
3367 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3368 docs: vec![],
3369 };
3370
3371 let out = generate_instructions_code(
3372 "Demo",
3373 std::slice::from_ref(&instr),
3374 &[],
3375 &pdas,
3376 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3377 &HashSet::new(),
3378 );
3379
3380 assert!(out
3381 .code
3382 .contains("{ type: 'argRef', argName: 'args.transactionIndex', argType: 'u64' }"));
3383 assert!(out.code.contains("proposal?: string;"));
3384 assert!(
3385 out.warnings.is_empty(),
3386 "unexpected warnings: {:?}",
3387 out.warnings
3388 );
3389 }
3390
3391 #[test]
3392 fn helper_only_arg_seed_emits_resolve_namespace() {
3393 let mut program_pdas: BTreeMap<String, PdaDefinition> = BTreeMap::new();
3394 program_pdas.insert(
3395 "proposal".to_string(),
3396 PdaDefinition {
3397 name: "proposal".to_string(),
3398 seeds: vec![PdaSeedDef::ArgRef {
3399 arg_name: "transactionIndex".to_string(),
3400 arg_type: Some("u64".to_string()),
3401 }],
3402 program_id: None,
3403 },
3404 );
3405 let mut pdas = BTreeMap::new();
3406 pdas.insert("demo".to_string(), program_pdas);
3407
3408 let instr = InstructionDef {
3409 name: "proposalActivate".to_string(),
3410 discriminator: vec![4],
3411 discriminator_size: 1,
3412 accounts: vec![InstructionAccountDef {
3413 name: "proposal".to_string(),
3414 is_signer: false,
3415 is_writable: true,
3416 resolution: AccountResolution::PdaRef {
3417 pda_name: "proposal".to_string(),
3418 },
3419 is_optional: false,
3420 docs: vec![],
3421 }],
3422 args: vec![],
3423 errors: vec![],
3424 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3425 docs: vec![],
3426 };
3427
3428 let out = generate_instructions_code(
3429 "Demo",
3430 std::slice::from_ref(&instr),
3431 &[],
3432 &pdas,
3433 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3434 &HashSet::new(),
3435 );
3436
3437 assert!(
3438 out.code.contains("resolve?: {"),
3439 "code missing resolve namespace: {}",
3440 out.code
3441 );
3442 assert!(
3443 out.code.contains("transactionIndex?: bigint;"),
3444 "code missing helper input: {}",
3445 out.code
3446 );
3447 assert!(out.code.contains("proposal?: string;"));
3448 assert!(
3449 out.warnings.is_empty(),
3450 "unexpected warnings: {:?}",
3451 out.warnings
3452 );
3453 }
3454
3455 #[test]
3456 fn account_field_seed_still_degrades_with_actionable_warning() {
3457 let mut program_pdas: BTreeMap<String, PdaDefinition> = BTreeMap::new();
3458 program_pdas.insert(
3459 "proposal".to_string(),
3460 PdaDefinition {
3461 name: "proposal".to_string(),
3462 seeds: vec![PdaSeedDef::AccountRef {
3463 account_name: "transaction.index".to_string(),
3464 }],
3465 program_id: None,
3466 },
3467 );
3468 let mut pdas = BTreeMap::new();
3469 pdas.insert("demo".to_string(), program_pdas);
3470
3471 let instr = InstructionDef {
3472 name: "configTransactionExecute".to_string(),
3473 discriminator: vec![5],
3474 discriminator_size: 1,
3475 accounts: vec![
3476 InstructionAccountDef {
3477 name: "transaction".to_string(),
3478 is_signer: false,
3479 is_writable: false,
3480 resolution: AccountResolution::UserProvided,
3481 is_optional: false,
3482 docs: vec![],
3483 },
3484 InstructionAccountDef {
3485 name: "proposal".to_string(),
3486 is_signer: false,
3487 is_writable: true,
3488 resolution: AccountResolution::PdaRef {
3489 pda_name: "proposal".to_string(),
3490 },
3491 is_optional: false,
3492 docs: vec![],
3493 },
3494 ],
3495 args: vec![],
3496 errors: vec![],
3497 program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()),
3498 docs: vec![],
3499 };
3500
3501 let out = generate_instructions_code(
3502 "Demo",
3503 std::slice::from_ref(&instr),
3504 &[],
3505 &pdas,
3506 &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()],
3507 &HashSet::new(),
3508 );
3509
3510 assert!(out.code.contains("proposal: string;"));
3511 assert!(out.warnings.iter().any(|w| w.contains("typed helper arg")));
3512 assert_eq!(out.pda_degradations.len(), 1);
3513 assert_eq!(
3514 out.pda_degradations[0],
3515 PdaDegradation {
3516 instruction_name: "configTransactionExecute".to_string(),
3517 account_name: "proposal".to_string(),
3518 pda_name: Some("proposal".to_string()),
3519 source: PdaDegradationSource::Registry,
3520 reason: "seed references account field 'transaction.index' which is not supported for low-level auto-resolution; encode it as a typed helper arg instead".to_string(),
3521 }
3522 );
3523 }
3524
3525 #[test]
3530 fn golden_ore_stack_json_emits_pda_handlers() {
3531 let path = concat!(
3532 env!("CARGO_MANIFEST_DIR"),
3533 "/../stacks/ore/.arete/OreStream.stack.json"
3534 );
3535 let json = match std::fs::read_to_string(path) {
3536 Ok(c) => c,
3537 Err(_) => return,
3539 };
3540 let spec: crate::ast::SerializableStackSpec =
3541 serde_json::from_str(&json).expect("ore stack json should deserialize");
3542
3543 let out = generate_instructions_code(
3544 &to_pascal_case(&spec.stack_name),
3545 &spec.instructions,
3546 &spec.idls,
3547 &spec.pdas,
3548 &spec.program_ids,
3549 &HashSet::new(),
3550 );
3551
3552 assert!(
3553 !out.stack_entries.is_empty(),
3554 "expected at least one emitted ore handler"
3555 );
3556 let code = &out.code;
3557 assert!(code.contains("createInstructionHandler"));
3558 assert!(
3560 code.contains("{ type: 'literal', value: 'treasury' }"),
3561 "treasury PDA seed should be inlined"
3562 );
3563 assert!(
3564 code.contains("{ type: 'literal', value: 'miner' }")
3565 && code.contains("{ type: 'accountRef', accountName: 'authority' }"),
3566 "miner PDA seeds should be inlined with an authority accountRef"
3567 );
3568 assert!(code.contains("category: 'pda'"));
3569
3570 assert!(spec.idls.len() > 1, "ore stack should bundle two programs");
3574 assert!(code.contains("export const oreCloseInstruction"));
3575 assert!(code.contains("export const entropyCloseInstruction"));
3576 assert!(!code.contains("export const closeInstruction"));
3577 assert!(code.contains("ORE_STREAM_ORE_PROGRAM_ERRORS"));
3578 assert!(code.contains("ORE_STREAM_ENTROPY_PROGRAM_ERRORS"));
3579 let block = render_instructions_stack_block(&out.stack_entries);
3580 assert!(block.contains(" ore: {"));
3581 assert!(block.contains(" entropy: {"));
3582 assert!(block.contains(" close: oreCloseInstruction,"));
3583 assert!(block.contains(" close: entropyCloseInstruction,"));
3584
3585 let expected_path = concat!(
3593 env!("CARGO_MANIFEST_DIR"),
3594 "/tests/golden/ore-close-instruction.expected.ts"
3595 );
3596 let expected = std::fs::read_to_string(expected_path)
3597 .expect("golden fixture should exist")
3598 .trim_end()
3599 .to_string();
3600 let start = code
3601 .find("export interface OreCloseParams")
3602 .expect("OreCloseParams block present");
3603 let end_marker = "});";
3604 let end = code[start..]
3605 .find(&format!("export const oreCloseInstruction"))
3606 .and_then(|handler_offset| {
3607 code[start + handler_offset..]
3608 .find(end_marker)
3609 .map(|e| start + handler_offset + e + end_marker.len())
3610 })
3611 .expect("oreCloseInstruction block terminates");
3612 let actual = code[start..end].trim_end();
3613 assert_eq!(
3614 actual, expected,
3615 "generated oreClose block diverged from the golden fixture"
3616 );
3617 }
3618
3619 #[test]
3620 fn multi_program_scopes_errors_and_prefixes_names() {
3621 let idls = vec![
3622 idl(
3623 "ore",
3624 "Prog111111111111111111111111111111111111111",
3625 vec![IdlErrorSnapshot {
3626 code: 0,
3627 name: "OreBroke".to_string(),
3628 msg: Some("ore broke".to_string()),
3629 }],
3630 ),
3631 idl(
3632 "entropy",
3633 "Prog222222222222222222222222222222222222222",
3634 vec![IdlErrorSnapshot {
3635 code: 0,
3636 name: "EntropyBroke".to_string(),
3637 msg: Some("entropy broke".to_string()),
3638 }],
3639 ),
3640 ];
3641
3642 let close = |program_id: &str| InstructionDef {
3643 name: "close".to_string(),
3644 discriminator: vec![9],
3645 discriminator_size: 1,
3646 accounts: vec![InstructionAccountDef {
3647 name: "signer".to_string(),
3648 is_signer: true,
3649 is_writable: true,
3650 resolution: AccountResolution::Signer,
3651 is_optional: false,
3652 docs: vec![],
3653 }],
3654 args: vec![],
3655 errors: vec![],
3656 program_id: Some(program_id.to_string()),
3657 docs: vec![],
3658 };
3659 let instructions = vec![
3660 close("Prog111111111111111111111111111111111111111"),
3661 close("Prog222222222222222222222222222222222222222"),
3662 ];
3663
3664 let out = generate_instructions_code(
3665 "Demo",
3666 &instructions,
3667 &idls,
3668 &BTreeMap::new(),
3669 &[
3670 "Prog111111111111111111111111111111111111111".to_string(),
3671 "Prog222222222222222222222222222222222222222".to_string(),
3672 ],
3673 &HashSet::new(),
3674 );
3675
3676 let code = &out.code;
3677 assert!(code.contains("export const oreCloseInstruction"));
3679 assert!(code.contains("export const entropyCloseInstruction"));
3680 assert!(code.contains("export interface OreCloseParams"));
3681 assert!(code.contains("export interface EntropyCloseParams"));
3682 assert!(code.contains("DEMO_ORE_PROGRAM_ERRORS"));
3684 assert!(code.contains("DEMO_ENTROPY_PROGRAM_ERRORS"));
3685 assert!(code.contains("name: 'OreBroke'"));
3686 assert!(code.contains("name: 'EntropyBroke'"));
3687 assert!(code.contains("errors: DEMO_ORE_PROGRAM_ERRORS"));
3689 assert!(code.contains("errors: DEMO_ENTROPY_PROGRAM_ERRORS"));
3690
3691 assert_eq!(out.stack_entries.len(), 2);
3692 assert_eq!(out.stack_entries[0].program_key.as_deref(), Some("ore"));
3693 assert_eq!(out.stack_entries[1].program_key.as_deref(), Some("entropy"));
3694
3695 let block = render_instructions_stack_block(&out.stack_entries);
3696 assert!(block.contains(" ore: {\n close: oreCloseInstruction,\n },"));
3697 assert!(block.contains(" entropy: {\n close: entropyCloseInstruction,\n },"));
3698 }
3699
3700 #[test]
3701 fn emits_amount_aware_semantic_params_without_changing_raw_params() {
3702 let out = generate_instructions_code(
3703 "Demo",
3704 &[InstructionDef {
3705 name: "deposit".to_string(),
3706 discriminator: vec![9],
3707 discriminator_size: 1,
3708 accounts: vec![],
3709 args: vec![
3710 amount_arg(
3711 "amount",
3712 "u64",
3713 AmountDecimalsSource::ArgMint {
3714 arg_name: "mint".to_string(),
3715 },
3716 ),
3717 arg("mint", "solana_pubkey::Pubkey"),
3718 ],
3719 errors: vec![],
3720 program_id: Some("Prog111".to_string()),
3721 docs: vec![],
3722 }],
3723 &[idl("demo", "Prog111", vec![])],
3724 &BTreeMap::new(),
3725 &["Prog111".to_string()],
3726 &HashSet::new(),
3727 );
3728
3729 assert!(out.code.contains("export interface DepositParams"));
3730 assert!(out.code.contains("amount: bigint;"));
3731 assert!(out.code.contains("export interface DepositSemanticParams"));
3732 assert!(out.code.contains("amount: AmountInput;"));
3733 assert!(out.code.contains("amountDecimals?: number;"));
3734 assert!(out.code.contains("build?: BuildOptions;"));
3735 assert!(out.needs_amount_input);
3736 assert!(out.needs_program_runtime_extensions);
3737 assert!(out.needs_resolve_amount_to_raw);
3738 assert_eq!(
3739 out.stack_entries[0].semantic_params_type.as_deref(),
3740 Some("DepositSemanticParams")
3741 );
3742 assert_eq!(
3743 out.stack_entries[0].runtime_program_key.as_deref(),
3744 Some("demo")
3745 );
3746 }
3747
3748 #[test]
3749 fn emits_nested_amount_aware_semantic_params_and_root_conversions() {
3750 let mut idl = idl("demo", "Prog111", vec![]);
3751 idl.types = vec![IdlTypeDefSnapshot {
3752 name: "depositParams".to_string(),
3753 docs: vec![],
3754 serialization: None,
3755 type_def: IdlTypeDefKindSnapshot::Struct {
3756 kind: "struct".to_string(),
3757 fields: vec![
3758 hinted_field(
3759 "maxAmount",
3760 simple("u64"),
3761 arete_idl::IdlAmountHint {
3762 decimals_source: arete_idl::IdlAmountDecimalsSource::ArgMint {
3763 arg_name: "params.quoteMint".to_string(),
3764 },
3765 },
3766 ),
3767 field("quoteMint", simple("publicKey")),
3768 field("memo", simple("string")),
3769 ],
3770 },
3771 }];
3772 idl.instructions = vec![instruction_snapshot(
3773 "deposit",
3774 vec![9],
3775 vec![field("params", defined("depositParams"))],
3776 )];
3777
3778 let out = generate_instructions_code(
3779 "Demo",
3780 &[InstructionDef {
3781 name: "deposit".to_string(),
3782 discriminator: vec![9],
3783 discriminator_size: 1,
3784 accounts: vec![],
3785 args: vec![arg("params", "depositParams")],
3786 errors: vec![],
3787 program_id: Some("Prog111".to_string()),
3788 docs: vec![],
3789 }],
3790 &[idl],
3791 &BTreeMap::new(),
3792 &["Prog111".to_string()],
3793 &HashSet::new(),
3794 );
3795
3796 assert!(out.code.contains("params: DepositParams;"));
3797 assert!(out.code.contains("export interface DepositSemanticParams"));
3798 assert!(out
3799 .code
3800 .contains("params: { maxAmount: AmountInput; quoteMint: string; memo: string; };"));
3801 assert!(out.code.contains("paramsMaxAmountDecimals?: number;"));
3802 assert!(out.code.contains("build?: BuildOptions;"));
3803 assert_eq!(
3804 out.stack_entries[0].semantic_params_type.as_deref(),
3805 Some("DepositSemanticParams")
3806 );
3807 assert_eq!(
3808 out.stack_entries[0].semantic_extra_params,
3809 vec!["paramsMaxAmountDecimals".to_string()]
3810 );
3811 assert_eq!(out.stack_entries[0].semantic_amount_args.len(), 1);
3812 assert_eq!(
3813 out.stack_entries[0].semantic_amount_args[0].arg_name,
3814 "params"
3815 );
3816 assert!(out.stack_entries[0].semantic_amount_args[0]
3817 .raw_expression
3818 .contains("params.params.maxAmount"));
3819 assert!(out.stack_entries[0].semantic_amount_args[0]
3820 .raw_expression
3821 .contains("params.params.quoteMint"));
3822 }
3823
3824 #[test]
3825 fn emits_amount_aware_semantic_params_inside_vectors_and_arrays() {
3826 for (raw_type, snapshot_type) in [
3827 ("Vec<registry>", vec_type(defined("registry"))),
3828 ("[registry; 2]", array_type(defined("registry"), 2)),
3829 ] {
3830 let mut idl = idl("demo", "Prog111", vec![]);
3831 idl.types = vec![IdlTypeDefSnapshot {
3832 name: "registry".to_string(),
3833 docs: vec![],
3834 serialization: None,
3835 type_def: IdlTypeDefKindSnapshot::Struct {
3836 kind: "struct".to_string(),
3837 fields: vec![
3838 hinted_field(
3839 "buyerCap",
3840 simple("u64"),
3841 arete_idl::IdlAmountHint {
3842 decimals_source: arete_idl::IdlAmountDecimalsSource::ArgMint {
3843 arg_name: "registries[].quoteMint".to_string(),
3844 },
3845 },
3846 ),
3847 field("quoteMint", simple("publicKey")),
3848 hinted_field(
3849 "supply",
3850 simple("u64"),
3851 arete_idl::IdlAmountHint {
3852 decimals_source: arete_idl::IdlAmountDecimalsSource::ArgMint {
3853 arg_name: "baseMint".to_string(),
3854 },
3855 },
3856 ),
3857 field("memo", simple("string")),
3858 ],
3859 },
3860 }];
3861 idl.instructions = vec![instruction_snapshot(
3862 "initialize",
3863 vec![9],
3864 vec![
3865 field("registries", snapshot_type),
3866 field("quoteMint", simple("publicKey")),
3867 field("baseMint", simple("publicKey")),
3868 ],
3869 )];
3870
3871 let out = generate_instructions_code(
3872 "Demo",
3873 &[InstructionDef {
3874 name: "initialize".to_string(),
3875 discriminator: vec![9],
3876 discriminator_size: 1,
3877 accounts: vec![],
3878 args: vec![
3879 arg("registries", raw_type),
3880 arg("quoteMint", "publicKey"),
3881 arg("baseMint", "publicKey"),
3882 ],
3883 errors: vec![],
3884 program_id: Some("Prog111".to_string()),
3885 docs: vec![],
3886 }],
3887 &[idl],
3888 &BTreeMap::new(),
3889 &["Prog111".to_string()],
3890 &HashSet::new(),
3891 );
3892
3893 assert!(out.code.contains(
3894 "registries: { buyerCap: AmountInput; quoteMint: string; supply: AmountInput; memo: string; }[];"
3895 ));
3896 let raw_expression = &out.stack_entries[0].semantic_amount_args[0].raw_expression;
3897 assert!(raw_expression
3898 .contains("await Promise.all(params.registries.map(async (entry) => ("));
3899 assert!(
3900 raw_expression.contains("mint: entry.quoteMint"),
3901 "unexpected raw expression: {raw_expression}"
3902 );
3903 assert!(raw_expression.contains("mint: params.baseMint"));
3904 assert!(raw_expression.contains("...entry"));
3905 assert!(raw_expression.contains("memo") == false);
3906 }
3907 }
3908
3909 #[test]
3910 fn emits_known_account_semantic_params_for_user_provided_accounts() {
3911 let out = generate_instructions_code(
3912 "Demo",
3913 &[InstructionDef {
3914 name: "mintTo".to_string(),
3915 discriminator: vec![7],
3916 discriminator_size: 1,
3917 accounts: vec![user_account("mint"), user_account("destination")],
3918 args: vec![amount_arg(
3919 "amount",
3920 "u64",
3921 AmountDecimalsSource::KnownAccount {
3922 account_name: "mint".to_string(),
3923 },
3924 )],
3925 errors: vec![],
3926 program_id: Some("Prog111".to_string()),
3927 docs: vec![],
3928 }],
3929 &[idl("demo", "Prog111", vec![])],
3930 &BTreeMap::new(),
3931 &["Prog111".to_string()],
3932 &HashSet::new(),
3933 );
3934
3935 assert!(out.code.contains("export interface MintToSemanticParams"));
3936 assert!(out.code.contains("amount: AmountInput;"));
3937 assert!(out.code.contains("amountDecimals?: number;"));
3938 assert!(out.code.contains("build?: BuildOptions;"));
3939 assert_eq!(out.stack_entries[0].semantic_amount_args.len(), 1);
3940 assert!(out.stack_entries[0].semantic_amount_args[0]
3941 .raw_expression
3942 .contains("mint: params.mint"));
3943 assert!(out.stack_entries[0].semantic_amount_args[0]
3944 .raw_expression
3945 .contains("amount: params.amount"));
3946 }
3947
3948 #[test]
3949 fn emits_nested_known_account_semantic_params_for_user_provided_accounts() {
3950 let mut idl = idl("demo", "Prog111", vec![]);
3951 idl.types = vec![IdlTypeDefSnapshot {
3952 name: "depositParams".to_string(),
3953 docs: vec![],
3954 serialization: None,
3955 type_def: IdlTypeDefKindSnapshot::Struct {
3956 kind: "struct".to_string(),
3957 fields: vec![
3958 hinted_field(
3959 "maxAmount",
3960 simple("u64"),
3961 arete_idl::IdlAmountHint {
3962 decimals_source: arete_idl::IdlAmountDecimalsSource::KnownAccount {
3963 account_name: "quoteMint".to_string(),
3964 },
3965 },
3966 ),
3967 field("memo", simple("string")),
3968 ],
3969 },
3970 }];
3971 idl.instructions = vec![instruction_snapshot(
3972 "deposit",
3973 vec![9],
3974 vec![field("params", defined("depositParams"))],
3975 )];
3976
3977 let out = generate_instructions_code(
3978 "Demo",
3979 &[InstructionDef {
3980 name: "deposit".to_string(),
3981 discriminator: vec![9],
3982 discriminator_size: 1,
3983 accounts: vec![user_account("quoteMint")],
3984 args: vec![arg("params", "depositParams")],
3985 errors: vec![],
3986 program_id: Some("Prog111".to_string()),
3987 docs: vec![],
3988 }],
3989 &[idl],
3990 &BTreeMap::new(),
3991 &["Prog111".to_string()],
3992 &HashSet::new(),
3993 );
3994
3995 assert!(out.code.contains("export interface DepositSemanticParams"));
3996 assert!(out
3997 .code
3998 .contains("params: { maxAmount: AmountInput; memo: string; };"));
3999 assert!(out.code.contains("paramsMaxAmountDecimals?: number;"));
4000 assert!(out.code.contains("build?: BuildOptions;"));
4001 assert_eq!(out.stack_entries[0].semantic_amount_args.len(), 1);
4002 assert!(out.stack_entries[0].semantic_amount_args[0]
4003 .raw_expression
4004 .contains("params.params.maxAmount"));
4005 assert!(out.stack_entries[0].semantic_amount_args[0]
4006 .raw_expression
4007 .contains("mint: params.quoteMint"));
4008 }
4009
4010 #[test]
4011 fn multi_program_unmatched_instruction_falls_back_with_warning() {
4012 let idls = vec![
4013 idl("ore", "Prog111111111111111111111111111111111111111", vec![]),
4014 idl(
4015 "entropy",
4016 "Prog222222222222222222222222222222222222222",
4017 vec![],
4018 ),
4019 ];
4020 let instr = InstructionDef {
4021 name: "mystery".to_string(),
4022 discriminator: vec![1],
4023 discriminator_size: 1,
4024 accounts: vec![],
4025 args: vec![],
4026 errors: vec![],
4027 program_id: None,
4028 docs: vec![],
4029 };
4030
4031 let out = generate_instructions_code(
4032 "Demo",
4033 std::slice::from_ref(&instr),
4034 &idls,
4035 &BTreeMap::new(),
4036 &["Prog111111111111111111111111111111111111111".to_string()],
4037 &HashSet::new(),
4038 );
4039
4040 assert!(out
4041 .warnings
4042 .iter()
4043 .any(|w| w.contains("could not be matched to a program IDL")));
4044 assert!(out.code.contains("export const mysteryInstruction"));
4046 assert!(out.code.contains("errors: DEMO_PROGRAM_ERRORS"));
4047 assert_eq!(out.stack_entries[0].program_key, None);
4048 }
4049}