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