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