Skip to main content

arete_interpreter/
typescript_instructions.rs

1//! TypeScript codegen for instruction-construction handlers.
2//!
3//! This module consumes the `InstructionDef[]` serialized into a stack spec and
4//! emits data-driven instruction handlers that target the core SDK's
5//! [`createInstructionHandler`] factory. No imperative serialization code is
6//! generated: the output is metadata (discriminator, ordered accounts, arg
7//! schema, errors) plus typed `Params`/`Error` shapes the core runtime
8//! interprets.
9
10use 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/// One entry in the stack definition's `instructions` block.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct StackInstructionEntry {
23    /// Program namespace key (camelCase IDL name) for multi-program stacks.
24    /// `None` for single-program stacks, where the block stays flat.
25    pub program_key: Option<String>,
26    /// Key inside the (possibly nested) instructions block.
27    pub instruction_name: String,
28    /// Program namespace key used at runtime on `client.programs.<key>`.
29    pub runtime_program_key: Option<String>,
30    /// Name of the generated handler const.
31    pub handler_const: String,
32    /// Name of the generated params interface for the handler.
33    pub params_type: String,
34    /// Name of the generated semantic params interface when amount hints are present.
35    pub semantic_params_type: Option<String>,
36    /// Extra params added only for semantic wrappers (for example decimals overrides).
37    pub semantic_extra_params: Vec<String>,
38    /// Root-arg conversions applied before delegating to the raw connected plan.
39    pub semantic_amount_args: Vec<SemanticAmountArgEntry>,
40    /// Whether the emitted semantic conversions reference the operation context.
41    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/// Result of generating instruction handler code for a stack.
90#[derive(Debug, Clone, Default)]
91pub struct InstructionsCodegen {
92    /// Generated TypeScript: program-error consts, per-instruction param/error
93    /// types and handler consts. Empty when there are no emittable handlers.
94    pub code: String,
95    /// Entries to wire into the stack definition's `instructions` block.
96    pub stack_entries: Vec<StackInstructionEntry>,
97    /// Whether the generated code references the `@usearete/sdk` runtime
98    /// (`createInstructionHandler` / `ErrorMetadata`).
99    pub needs_runtime_import: bool,
100    /// Whether generated program definitions need runtime semantic wrappers.
101    pub needs_program_runtime_extensions: bool,
102    /// Whether generated semantic wrappers reference `AmountInput`.
103    pub needs_amount_input: bool,
104    /// Whether generated semantic wrappers reference `resolveAmountToRaw`.
105    pub needs_resolve_amount_to_raw: bool,
106    /// Whether generated semantic wrappers reference `toRawAmount`.
107    pub needs_to_raw_amount: bool,
108    /// Whether an emitted semantic parameter interface references `BuildOptions`.
109    pub needs_build_options: bool,
110    /// Whether an emitted operation factory references `ProgramOperationContext`.
111    pub needs_operation_context: bool,
112    /// Human-readable warnings (skipped instructions, degraded PDAs).
113    pub warnings: Vec<String>,
114    /// Structured PDA degradation metadata for CLI summaries.
115    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
152/// Per-program error const/type names plus the rendered declarations.
153struct 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
204/// Generate instruction handler code for a stack.
205///
206/// `idls` are the stack's IDL snapshots; each handler's errors are scoped to
207/// its own program (matched via `InstructionDef.program_id`). Single-program
208/// stacks keep flat naming; multi-program stacks prefix handler/type names
209/// with the program name and namespace the `instructions` block per program.
210pub 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    // Flatten the PDA registry into a name -> definition lookup. PDA names are
226    // expected to be unique across programs within a stack; conflicting
227    // definitions keep the first and warn.
228    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    // Per-program error scopes. The fallback scope (stack-level naming, all
261    // errors flattened) serves single-program stacks, stacks without IDL
262    // snapshots, and instructions that cannot be matched to a program.
263    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        // Match the instruction to its program for error scoping and naming.
303        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        // Naming: multi-program handlers are prefixed with their program name
321        // so duplicate instruction names across programs cannot collide.
322        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        // --- Parse args; skip the whole instruction on unsupported types. ---
365        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        // --- Map accounts. ---
390        let instr_account_names: HashSet<&str> =
391            instr.accounts.iter().map(|a| a.name.as_str()).collect();
392        // name -> raw type string, used both for arg-existence checks and to
393        // type PDA seeds that reference args.
394        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        // --- Params interface. ---
425        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        // --- Error type. Program errors are stack-wide (IDLs do not scope
608        // errors to instructions), so each handler's typed error is an alias of
609        // the program-wide union. ---
610        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        // --- Args schema literal. ---
617        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        // --- Accounts literal. ---
630        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        // Mark the error scope as referenced only when a handler is emitted,
672        // so fully-skipped programs do not produce dangling consts.
673        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        // Nothing emittable (all instructions skipped). Still surface warnings.
694        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    // Program-level error metadata blocks, one per referenced scope. Errors
710    // live on the stack's IDL snapshots (not duplicated onto instructions).
711    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        // Stacks without IDL snapshots still need the fallback scope that
726        // every handler references.
727        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    // Defined-type declarations referenced by arg schemas, in dependency order.
737    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
766/// Render the `rawInstructions: { ... }` block for the stack definition const.
767///
768/// Entries without a `program_key` render flat; entries with one are grouped
769/// under their program's key (multi-program stacks).
770pub 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    // Flat entries first (single-program stacks, or unmatched instructions).
777    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    // Then one nested block per program, preserving first-seen program order.
784    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
808/// Convert a program name to camelCase for use as a namespace key / const
809/// prefix (e.g. "ore_boost" -> "oreBoost").
810fn 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// ============================================================================
820// Argument type parsing
821// ============================================================================
822
823/// A parsed instruction argument type.
824#[derive(Debug, Clone)]
825struct ParsedArgType {
826    /// TypeScript literal for the core `ArgType` (e.g. `'u64'`, `{ option: 'u64' }`).
827    schema: String,
828    /// TypeScript parameter type (e.g. `bigint`, `string`, `number[]`).
829    ts_type: String,
830    /// Whether the type is representable by the core serializer.
831    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/// Parse an arg type without any defined-type lookup (primitives and wrappers
843/// only). Defined types come back unsupported.
844#[cfg(test)]
845fn parse_arg_type(raw: &str) -> ParsedArgType {
846    DefinedTypes::empty().parse_arg_type(raw)
847}
848
849/// Resolver for IDL-defined types (structs/enums) referenced by instruction
850/// args. Resolved types are emitted as `export interface` / `export type`
851/// declarations (collected in `decls`) and inlined into arg schemas as
852/// `{ struct: [...] }` / `{ enum: [...] }` literals, so the runtime needs no
853/// type registry.
854struct DefinedTypes<'a> {
855    /// IDL type definitions by name, first-wins across programs.
856    defs: BTreeMap<String, &'a IdlTypeDefSnapshot>,
857    /// lowercase name -> canonical key, for case-insensitive fallback lookup.
858    lower: BTreeMap<String, String>,
859    /// Emitted TS declarations, in dependency order.
860    decls: Vec<String>,
861    /// Memoized resolutions by original IDL name (None = unsupported).
862    resolved: BTreeMap<String, Option<ParsedArgType>>,
863    /// TS identifiers already in use (entity interfaces + emitted types).
864    taken_names: HashSet<String>,
865    /// Names currently being resolved (cycle guard).
866    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    /// Parse a stringified Rust-ish arg type (what `to_rust_type_string`
937    /// produces), resolving bare names against the IDL type definitions.
938    fn parse_arg_type(&mut self, raw: &str) -> ParsedArgType {
939        let t = raw.trim().trim_start_matches('&').trim();
940
941        // Generic wrappers: Option<T>, Vec<T>.
942        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        // Fixed-size array: [T; N].
965        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        // Primitive (possibly path-qualified, e.g. solana_pubkey::Pubkey).
980        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            // IDL `bytes` (instruction args reach here as Vec<u8> instead and
998            // keep the wire-identical `{ vec: 'u8' }` schema).
999            "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    /// Parse an IDL snapshot type (used inside struct fields / enum variants).
1009    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    /// Resolve a bare type name against the IDL type definitions, emitting a
1075    /// TS declaration on first use. Returns `None` when unsupported.
1076    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            // `to_rust_type_string` passes IDL names through verbatim, but the
1092            // referencing spelling occasionally differs in case.
1093            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                // Struct variant: { name: 'x', fields: [...] }.
1201                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                // Tuple variant: { name: 'x', tuple: [...] }.
1230                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    /// Pick a unique TS identifier for a defined type, suffixing `Input` (then
1280    /// a counter) when the pascal-cased name collides with an entity interface
1281    /// or another emitted type.
1282    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
1310/// Split `Name<inner>` into `(Name, inner)`, ignoring path qualifiers on `Name`.
1311pub(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
1321/// Wrap union types in parentheses so `T | null` arrays read as `(T | null)[]`.
1322fn maybe_paren(ts: &str) -> String {
1323    if ts.contains('|') {
1324        format!("({})", ts)
1325    } else {
1326        ts.to_string()
1327    }
1328}
1329
1330// ============================================================================
1331// Account mapping
1332// ============================================================================
1333
1334/// A user-provided account that must surface as a `Params` field.
1335#[derive(Debug, Clone)]
1336struct UserParam {
1337    name: String,
1338    optional: bool,
1339}
1340
1341/// A helper-only PDA seed input exposed under `resolve`.
1342#[derive(Debug, Clone)]
1343struct ResolveParam {
1344    name: String,
1345    ts_type: String,
1346}
1347
1348/// Result of mapping a single instruction account.
1349struct MappedAccount {
1350    /// TypeScript `AccountMeta` object literal.
1351    literal: String,
1352    /// Set when the account is caller-supplied (`userProvided`).
1353    param: Option<UserParam>,
1354    /// Helper-only PDA seed inputs needed for derivation.
1355    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        // Degradations are surfaced both to the compiler caller (warnings) and
1382        // in the generated code, so SDK readers can see why an account that
1383        // looks derivable must be passed in manually.
1384        let comment = match &degradation {
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
1526/// Build a TypeScript `PdaConfig` literal from seed definitions.
1527///
1528/// Returns `Err(reason)` when the PDA cannot be represented by the core
1529/// resolver (e.g. seeds referencing accounts/args that do not exist in this
1530/// instruction), so the caller can degrade to `userProvided`. On success the
1531/// second tuple element carries soft warnings (e.g. an arg seed whose type
1532/// could not be determined, leaving the runtime to encode heuristically).
1533fn 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                // Prefer the seed's declared type; fall back to the
1574                // instruction arg's type (Anchor seeds carry no type info).
1575                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(&param.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
2485/// Normalize a raw arg-type string (IDL or `pdas!` DSL spelling) to the
2486/// canonical seed type the runtime's `serializeSeedValue` understands.
2487/// Returns `None` for types that cannot be encoded as a seed.
2488pub(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
2515// ============================================================================
2516// Errors
2517// ============================================================================
2518
2519/// Dedupe errors by code, preserving first-seen definitions, sorted ascending.
2520pub(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
2565/// Render the `| { code; name; msg } | ...` body of an error union type.
2566fn 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
2579// ============================================================================
2580// Helpers
2581// ============================================================================
2582
2583fn 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        // Emitted TS declarations for every referenced defined type.
2878        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        // Inlined schemas, including the nested struct and fielded enum.
2884        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        // Params reference the generated interface type.
2889        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        // Simulate an entity interface already named PlanTerms.
3123        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        // PDA accounts remain derivable, but callers can still pass explicit overrides.
3317        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                    // Declared type on the seed itself (pdas! DSL style).
3405                    PdaSeedDef::ArgRef {
3406                        arg_name: "roundId".to_string(),
3407                        arg_type: Some("u32".to_string()),
3408                    },
3409                    // No declared type: must fall back to the instruction arg.
3410                    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    /// Golden test: drive the codegen from the real, compiler-produced ore
3710    /// stack JSON and assert the expected handlers and PDA configs appear. This
3711    /// exercises the full `stack.json -> TypeScript` path against actual data
3712    /// (the Steel `pdas!` registry resolving instruction accounts to `PdaRef`).
3713    #[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            // Stack JSON is generated by the macro build; skip if not present.
3722            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        // Pure-literal PDA (treasury) and authority-keyed PDA (miner) both appear.
3743        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        // The ore stack bundles two programs (ore + entropy) that BOTH define
3755        // a `close` instruction: handlers must be program-prefixed, the stack
3756        // block namespaced, and errors scoped per program.
3757        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        // Exact-string golden: the full oreClose block (params interface,
3770        // error alias, docs, handler) must match the checked-in fixture. This
3771        // catches naming/formatting churn before the CI regenerate-diff does.
3772        // To update intentionally: regenerate the examples, then copy the
3773        // block: awk '/^export interface OreCloseParams/,/^}\);$/' \
3774        //   examples/ore-typescript/src/generated/ore-stack.ts \
3775        //   > interpreter/tests/golden/ore-close-instruction.expected.ts
3776        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        // Names prefixed per program; no collisions.
3862        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        // Overlapping error code 0 is attributed per program, not deduped away.
3867        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        // Each handler references its own program's errors.
3872        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        // Unmatched: unprefixed name, flat stack entry, stack-wide errors.
4279        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}