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