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