Skip to main content

anchor_cli/
lib.rs

1use {
2    crate::config::{
3        get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, HookType,
4        Manifest, PackageManager, ProgramDeployment, ProgramWorkspace, ScriptsConfig,
5        SurfnetInfoResponse, SurfpoolConfig, TestValidator, Validator, ValidatorType, WithPath,
6        SHUTDOWN_WAIT, STARTUP_WAIT, SURFPOOL_HOST,
7    },
8    abs_path::AbsolutePath,
9    anchor_cli_macros::AbsolutePath,
10    anchor_client::Cluster,
11    anchor_lang::{
12        prelude::UpgradeableLoaderState, solana_program::bpf_loader_upgradeable, AnchorDeserialize,
13    },
14    anchor_lang_idl::{
15        convert::{convert_idl, convert_idl_to_legacy},
16        types::{Idl, IdlArrayLen, IdlDefinedFields, IdlType, IdlTypeDefTy},
17    },
18    anyhow::{anyhow, bail, Context, Result},
19    base64::{engine::general_purpose::STANDARD, Engine},
20    cargo_metadata::{DependencyKind, MetadataCommand},
21    checks::{check_anchor_version, check_deps, check_idl_build_feature, check_overflow},
22    clap::{CommandFactory, Parser},
23    dirs::home_dir,
24    heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase},
25    regex::{Regex, RegexBuilder},
26    semver::{Version, VersionReq},
27    serde::Deserialize,
28    serde_json::{json, Map, Value as JsonValue},
29    solana_cli_config::Config as SolanaCliConfig,
30    solana_commitment_config::CommitmentConfig,
31    solana_compute_budget_interface::ComputeBudgetInstruction,
32    solana_instruction::Instruction,
33    solana_keypair::Keypair,
34    solana_pubkey::Pubkey,
35    solana_pubsub_client::pubsub_client::{PubsubClient, PubsubClientSubscription},
36    solana_rpc_client::rpc_client::RpcClient,
37    solana_rpc_client_api::{
38        config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter},
39        request::RpcRequest,
40        response::{Response as RpcResponse, RpcLogsResponse},
41    },
42    solana_signer::{EncodableKey, Signer},
43    std::{
44        collections::{BTreeMap, HashMap, HashSet},
45        ffi::OsString,
46        fs::{self, File},
47        io::{self, prelude::*},
48        path::{Path, PathBuf},
49        process::{Child, ExitStatus, Stdio},
50        string::ToString,
51        sync::{LazyLock, OnceLock},
52    },
53    template::{AnchorVersion, ProgramTemplate, TestTemplate},
54};
55
56mod abs_path;
57mod account;
58mod checks;
59pub mod codama;
60pub mod config;
61#[cfg(not(windows))]
62pub mod coverage;
63#[cfg(not(windows))]
64pub mod debugger;
65pub mod fetch;
66#[cfg(not(windows))]
67mod flamegraph;
68mod keygen;
69mod legacy_idl;
70mod metadata;
71#[cfg(not(windows))]
72mod profile;
73mod program;
74pub mod template;
75
76// Version of the docker image.
77pub const VERSION: &str = env!("CARGO_PKG_VERSION");
78pub const DOCKER_BUILDER_VERSION: &str = VERSION;
79/// Default RPC port
80pub const DEFAULT_RPC_PORT: u16 = 8899;
81const DEFAULT_FAUCET_PORT: u16 = 9900;
82const DEFAULT_TOOLS_VERSION: &str = "v1.57";
83const DEFAULT_BUILD_ARCH: &str = "v3";
84const BUILD_ARCH_ENV: &str = "ANCHOR_BUILD_SBF_ARCH";
85
86/// WebSocket port offset for solana-test-validator (RPC port + 1)
87pub const WEBSOCKET_PORT_OFFSET: u16 = 1;
88
89pub static AVM_HOME: LazyLock<PathBuf> = LazyLock::new(|| {
90    if let Ok(avm_home) = std::env::var("AVM_HOME") {
91        PathBuf::from(avm_home)
92    } else {
93        let mut user_home = dirs::home_dir().expect("Could not find home directory");
94        user_home.push(".avm");
95        user_home
96    }
97});
98
99pub fn support_version_report() -> String {
100    let mut lines = vec![format!("anchor-cli {VERSION}")];
101
102    lines.push(command_version_line("solana-cli", "solana"));
103    lines.push(command_version_line("cargo", "cargo"));
104    lines.push(format!("OS: {}", os_version()));
105
106    lines.join("\n") + "\n"
107}
108
109fn command_version_line(label: &str, command: &str) -> String {
110    match command_output(command, &["--version"]) {
111        Some(version) if version.starts_with(label) => version,
112        Some(version) => format!("{label} {version}"),
113        None => format!("{label} unavailable"),
114    }
115}
116
117fn command_output(command: &str, args: &[&str]) -> Option<String> {
118    let output = std::process::Command::new(command)
119        .args(args)
120        .output()
121        .ok()?;
122    if !output.status.success() {
123        return None;
124    }
125
126    String::from_utf8(output.stdout)
127        .ok()
128        .and_then(|output| output.lines().next().map(str::trim).map(str::to_owned))
129        .filter(|line| !line.is_empty())
130}
131
132fn os_version() -> String {
133    #[cfg(target_os = "macos")]
134    if let Some(version) = macos_version() {
135        return version;
136    }
137
138    #[cfg(target_os = "linux")]
139    {
140        if let Some(version) = command_output("lsb_release", &["-ds"]) {
141            return version.trim_matches('"').to_owned();
142        }
143        if let Some(version) = linux_os_release() {
144            return version;
145        }
146    }
147
148    #[cfg(target_os = "windows")]
149    if let Some(version) = command_output("cmd", &["/C", "ver"]) {
150        return version;
151    }
152
153    std::env::consts::OS.to_owned()
154}
155
156#[cfg(target_os = "macos")]
157fn macos_version() -> Option<String> {
158    let name = command_output("sw_vers", &["-productName"])?;
159    let version = command_output("sw_vers", &["-productVersion"])?;
160    let build = command_output("sw_vers", &["-buildVersion"])?;
161    Some(format!("{name} {version} {build}"))
162}
163
164#[cfg(target_os = "linux")]
165fn linux_os_release() -> Option<String> {
166    fs::read_to_string("/etc/os-release")
167        .ok()?
168        .lines()
169        .find_map(|line| line.strip_prefix("PRETTY_NAME="))
170        .map(|value| value.trim_matches('"').to_owned())
171}
172
173#[derive(Debug, Parser, AbsolutePath)]
174#[clap(version = VERSION)]
175pub struct Opts {
176    #[clap(flatten)]
177    pub cfg_override: ConfigOverride,
178    #[clap(subcommand)]
179    pub command: Command,
180}
181
182#[derive(Debug, Parser, AbsolutePath)]
183pub enum Command {
184    /// Initializes a workspace.
185    Init {
186        /// Workspace name
187        name: String,
188        /// Use JavaScript instead of TypeScript
189        #[clap(short, long)]
190        javascript: bool,
191        /// Don't install JavaScript dependencies
192        #[clap(long)]
193        no_install: bool,
194        /// Package Manager to use. If omitted, detection cascades
195        /// `pnpm` -> `yarn` -> `npm` and picks the first one on PATH. When
196        /// set explicitly, the chosen binary must be installed.
197        #[clap(value_enum, long)]
198        package_manager: Option<PackageManager>,
199        /// Don't initialize git
200        #[clap(long)]
201        no_git: bool,
202        /// Rust program template to use
203        #[clap(value_enum, short, long, default_value = "multiple")]
204        template: ProgramTemplate,
205        /// Anchor template version to generate
206        #[clap(value_enum, long, default_value = "v1")]
207        anchor_version: AnchorVersion,
208        /// Test template to use
209        #[clap(value_enum, long, default_value = "litesvm")]
210        test_template: TestTemplate,
211        /// Initialize even if there are files
212        #[clap(long, action)]
213        force: bool,
214        /// Install Solana agent skills
215        #[clap(long)]
216        install_agent_skills: bool,
217    },
218    /// Builds the workspace.
219    #[clap(name = "build", alias = "b")]
220    Build {
221        /// True if the build should not fail even if there are no "CHECK" comments
222        #[clap(long)]
223        skip_lint: bool,
224        /// Skip checking for program ID mismatch between keypair and declare_id
225        #[clap(long)]
226        ignore_keys: bool,
227        /// Do not build the IDL
228        #[clap(long)]
229        no_idl: bool,
230        /// Output directory for the IDL.
231        #[clap(short, long)]
232        idl: Option<String>,
233        /// Output directory for the TypeScript IDL.
234        #[clap(short = 't', long)]
235        idl_ts: Option<String>,
236        /// True if the build artifact needs to be deterministic and verifiable.
237        #[clap(short, long)]
238        verifiable: bool,
239        /// Name of the program to build
240        #[clap(short, long)]
241        program_name: Option<String>,
242        /// Version of the Solana toolchain to use. For --verifiable builds
243        /// only.
244        #[clap(short, long)]
245        solana_version: Option<String>,
246        /// Platform tools version to pass to `cargo build-sbf`.
247        #[clap(long, default_value = DEFAULT_TOOLS_VERSION)]
248        tools_version: String,
249        /// SBPF architecture to pass to `cargo build-sbf`.
250        #[clap(long, default_value_t = default_build_arch())]
251        arch: String,
252        /// Docker image to use. For --verifiable builds only.
253        #[clap(short, long)]
254        docker_image: Option<String>,
255        /// Bootstrap docker image from scratch, installing all requirements for
256        /// verifiable builds. Only works for debian-based images.
257        #[clap(value_enum, short, long, default_value = "none")]
258        bootstrap: BootstrapMode,
259        /// Environment variables to pass into the docker container
260        #[clap(short, long, required = false)]
261        env: Vec<String>,
262        /// Arguments to pass to the underlying `cargo build-sbf` command
263        #[clap(required = false, last = true)]
264        cargo_args: Vec<String>,
265        /// Suppress doc strings in IDL output
266        #[clap(long)]
267        no_docs: bool,
268    },
269    /// Expands macros (wrapper around cargo expand)
270    ///
271    /// Use it in a program folder to expand program
272    ///
273    /// Use it in a workspace but outside a program
274    /// folder to expand the entire workspace
275    Expand {
276        /// Expand only this program
277        #[clap(short, long)]
278        program_name: Option<String>,
279        /// Write to stdout
280        #[clap(long)]
281        stdout: bool,
282        /// Arguments to pass to the underlying `cargo expand` command
283        #[clap(required = false, last = true)]
284        cargo_args: Vec<String>,
285    },
286    /// Verifies the on-chain bytecode matches the locally compiled artifact.
287    /// Run this command inside a program subdirectory, i.e., in the dir
288    /// containing the program's Cargo.toml.
289    Verify {
290        /// The program ID to verify.
291        program_id: Pubkey,
292        /// The URL of the repository to verify against. Conflicts with `--current-dir`.
293        #[clap(long, conflicts_with = "current_dir")]
294        repo_url: Option<String>,
295        /// The commit hash to verify against. Requires `--repo-url`.
296        #[clap(long, requires = "repo_url")]
297        commit_hash: Option<String>,
298        /// Verify against the source code in the current directory. Conflicts with `--repo-url`.
299        #[clap(long)]
300        current_dir: bool,
301        /// Name of the program to run the command on. Defaults to the package name.
302        #[clap(long)]
303        program_name: Option<String>,
304        /// Any additional arguments to pass to `solana-verify`.
305        #[clap(raw = true)]
306        args: Vec<String>,
307    },
308    #[clap(name = "test", alias = "t")]
309    /// Runs integration tests.
310    Test {
311        /// Build and test only this program
312        #[clap(short, long)]
313        program_name: Option<String>,
314        /// Use this flag if you want to run tests against previously deployed
315        /// programs.
316        #[clap(long)]
317        skip_deploy: bool,
318        /// True if the build should not fail even if there are
319        /// no "CHECK" comments where normally required
320        #[clap(long)]
321        skip_lint: bool,
322        /// Flag to skip starting a local validator, if the configured cluster
323        /// url is a localnet.
324        #[clap(long)]
325        skip_local_validator: bool,
326        /// Flag to skip building the program in the workspace,
327        /// use this to save time when running test and the program code is not altered.
328        #[clap(long)]
329        skip_build: bool,
330        /// Do not build the IDL
331        #[clap(long)]
332        no_idl: bool,
333        /// Flag to keep the local validator running after tests
334        /// to be able to check the transactions.
335        #[clap(long)]
336        detach: bool,
337        /// Run the test suites under the specified path
338        #[clap(long)]
339        run: Vec<String>,
340        /// Name of the script to run from [scripts] section (defaults to "test")
341        #[clap(long)]
342        script: Option<String>,
343        /// Validator type to use for local testing
344        #[clap(value_enum, long, default_value = "surfpool")]
345        validator: ValidatorType,
346        /// Profile each test: record per-test SBF register traces and render flamegraph SVGs under target/anchor-v2-profile.
347        #[clap(long)]
348        profile: bool,
349        args: Vec<String>,
350        /// Environment variables to pass into the docker container
351        #[clap(short, long, required = false)]
352        env: Vec<String>,
353        /// Arguments to pass to the underlying `cargo build-sbf` command.
354        #[clap(required = false, last = true)]
355        cargo_args: Vec<String>,
356    },
357    /// Coverage-guided fuzzing for Solana programs (powered by Crucible).
358    Fuzz(crucible_fuzz_cli::Cli),
359    /// Creates a new program.
360    New {
361        /// Program name
362        name: String,
363        /// Rust program template to use
364        #[clap(value_enum, short, long, default_value = "multiple")]
365        template: ProgramTemplate,
366        /// Anchor template version to generate
367        #[clap(value_enum, long, default_value = "v1")]
368        anchor_version: AnchorVersion,
369        /// Create new program even if there is already one
370        #[clap(long, action)]
371        force: bool,
372    },
373    /// Run tests under an instruction-level debugger.
374    #[cfg(not(windows))]
375    Debugger {
376        /// Filter captured traces to tests whose name contains this substring.
377        test_name: Option<String>,
378        /// Skip the build+test phase and open the TUI over existing traces.
379        #[clap(long)]
380        skip_run: bool,
381        /// Skip `cargo build-sbf`.
382        #[clap(long)]
383        skip_build: bool,
384        /// Forwarded to the underlying `anchor test` invocation.
385        #[clap(long)]
386        skip_lint: bool,
387        /// Drive tests over sbpf's gdb-stub instead of reading dumped trace files.
388        #[clap(long)]
389        gdb: bool,
390        /// Arguments to pass to the underlying `cargo build-sbf` command.
391        #[clap(required = false, last = true)]
392        cargo_args: Vec<String>,
393    },
394    /// Generate source-level coverage from SBF register traces.
395    #[cfg(not(windows))]
396    Coverage {
397        /// Skip the build+test phase and generate coverage from existing traces.
398        #[clap(long)]
399        skip_run: bool,
400        /// Skip `cargo build-sbf`.
401        #[clap(long)]
402        skip_build: bool,
403        /// Output path for the LCOV file.
404        #[clap(long, default_value = "target/coverage/sbf.lcov")]
405        output: String,
406        /// Directory containing register trace files.
407        #[clap(long, default_value = "target/coverage/traces")]
408        trace_dir: String,
409        /// Arguments to pass to the underlying `cargo build-sbf` command.
410        #[clap(required = false, last = true)]
411        cargo_args: Vec<String>,
412    },
413    /// Commands for interacting with interface definitions.
414    Idl {
415        #[clap(subcommand)]
416        subcmd: IdlCommand,
417    },
418    /// Remove all artifacts from the generated directories except program keypairs.
419    Clean,
420    /// Deploys each program in the workspace.
421    #[clap(hide = true)]
422    #[deprecated(since = "0.32.0", note = "use `anchor program deploy` instead")]
423    Deploy {
424        /// Only deploy this program
425        #[clap(short, long)]
426        program_name: Option<String>,
427        /// Keypair of the program (filepath) (requires program-name)
428        #[clap(long, requires = "program_name")]
429        program_keypair: Option<PathBuf>,
430        /// If true, deploy from path target/verifiable
431        #[clap(short, long)]
432        verifiable: bool,
433        /// Don't upload IDL during deployment (IDL is uploaded by default)
434        #[clap(long)]
435        no_idl: bool,
436        /// Arguments to pass to the underlying `solana program deploy` command.
437        #[clap(required = false, last = true)]
438        solana_args: Vec<String>,
439    },
440    /// Runs the deploy migration script.
441    Migrate,
442    /// Deploys, initializes an IDL, and migrates all in one command.
443    /// Upgrades a single program. The configured wallet must be the upgrade
444    /// authority.
445    #[clap(hide = true)]
446    #[deprecated(since = "0.32.0", note = "use `anchor program upgrade` instead")]
447    Upgrade {
448        /// The program to upgrade.
449        #[clap(short, long)]
450        program_id: Pubkey,
451        /// Filepath to the new program binary.
452        program_filepath: PathBuf,
453        /// Max times to retry on failure.
454        #[clap(long, default_value = "0")]
455        max_retries: u32,
456        /// Arguments to pass to the underlying `solana program deploy` command.
457        #[clap(required = false, last = true)]
458        solana_args: Vec<String>,
459    },
460    /// Request an airdrop of SOL
461    Airdrop {
462        /// Amount of SOL to airdrop
463        amount: f64,
464        /// Recipient address (defaults to configured wallet)
465        pubkey: Option<Pubkey>,
466    },
467    /// Cluster commands.
468    Cluster {
469        #[clap(subcommand)]
470        subcmd: ClusterCommand,
471    },
472    /// Configuration management commands.
473    Config {
474        #[clap(subcommand)]
475        subcmd: ConfigCommand,
476    },
477    /// Starts a node shell with an Anchor client setup according to the local
478    /// config.
479    Shell,
480    /// Runs the script defined by the current workspace's Anchor.toml.
481    #[clap(alias = "r")]
482    Run {
483        /// The name of the script to run.
484        script: String,
485        /// Argument to pass to the underlying script.
486        #[clap(required = false, last = true)]
487        script_args: Vec<String>,
488    },
489    /// Program keypair commands.
490    Keys {
491        #[clap(subcommand)]
492        subcmd: KeysCommand,
493    },
494    /// Localnet commands.
495    Localnet {
496        /// Flag to skip building the program in the workspace,
497        /// use this to save time when running test and the program code is not altered.
498        #[clap(long)]
499        skip_build: bool,
500        /// Use this flag if you want to run tests against previously deployed
501        /// programs.
502        #[clap(long)]
503        skip_deploy: bool,
504        /// True if the build should not fail even if there are
505        /// no "CHECK" comments where normally required
506        #[clap(long)]
507        skip_lint: bool,
508        /// Skip checking for program ID mismatch between keypair and declare_id
509        #[clap(long)]
510        ignore_keys: bool,
511        /// Validator type to use for local testing
512        #[clap(value_enum, long, default_value = "surfpool")]
513        validator: ValidatorType,
514        /// Environment variables to pass into the docker container
515        #[clap(short, long, required = false)]
516        env: Vec<String>,
517        /// Arguments to pass to the underlying `cargo build-sbf` command.
518        #[clap(required = false, last = true)]
519        cargo_args: Vec<String>,
520    },
521    /// Fetch and deserialize an account using the IDL provided.
522    Account {
523        /// Account struct to deserialize (format: <program_name>.<Account>)
524        account_type: String,
525        /// Address of the account to deserialize
526        address: Pubkey,
527        /// Path of IDL to use (defaults to workspace IDL)
528        #[clap(long)]
529        idl: Option<PathBuf>,
530    },
531    /// Generates shell completions.
532    Completions {
533        #[clap(value_enum)]
534        shell: clap_complete::Shell,
535    },
536    /// Get your public key
537    Address,
538    /// Get your balance
539    Balance {
540        /// Account to check balance for (defaults to configured wallet)
541        pubkey: Option<Pubkey>,
542        /// Display balance in lamports instead of SOL
543        #[clap(long)]
544        lamports: bool,
545    },
546    /// Get current epoch
547    Epoch,
548    /// Get information about the current epoch
549    #[clap(name = "epoch-info")]
550    EpochInfo,
551    /// Stream transaction logs
552    Logs {
553        /// Include vote transactions when monitoring all transactions
554        #[clap(long)]
555        include_votes: bool,
556        /// Addresses to filter logs by
557        #[clap(long)]
558        address: Option<Vec<Pubkey>>,
559    },
560    /// Show the contents of an account
561    ShowAccount {
562        #[clap(flatten)]
563        cmd: account::ShowAccountCommand,
564    },
565    /// Keypair generation and management
566    Keygen {
567        #[clap(subcommand)]
568        subcmd: KeygenCommand,
569    },
570    /// Program deployment and management commands
571    Program {
572        #[clap(subcommand)]
573        subcmd: ProgramCommand,
574    },
575    /// Codama IDL integration commands
576    Codama {
577        #[clap(subcommand)]
578        subcmd: codama::CodamaCommand,
579    },
580    /// [DEPRECATED] Manage legacy on-chain IDL accounts.
581    /// These commands interact with the old Anchor IDL instruction protocol and will be removed
582    /// in a future release. Migrate to Program Metadata-based IDL management (`anchor idl`).
583    LegacyIdl {
584        #[clap(subcommand)]
585        subcmd: legacy_idl::LegacyIdlCommand,
586    },
587}
588
589#[derive(Debug, Parser, AbsolutePath)]
590pub enum KeygenCommand {
591    /// Generate a new keypair
592    New {
593        /// Path to generated keypair file
594        #[clap(short = 'o', long)]
595        outfile: Option<PathBuf>,
596        /// Overwrite the output file if it exists
597        #[clap(short, long)]
598        force: bool,
599        /// Do not prompt for a passphrase
600        #[clap(long)]
601        no_passphrase: bool,
602        /// Do not display the generated pubkey
603        #[clap(long)]
604        silent: bool,
605        /// Number of words in the mnemonic phrase [possible values: 12, 15, 18, 21, 24]
606        #[clap(short = 'w', long, default_value = "12")]
607        word_count: usize,
608    },
609    /// Display the pubkey for a given keypair
610    Pubkey {
611        /// Keypair filepath
612        keypair: Option<PathBuf>,
613    },
614    /// Recover a keypair from a seed phrase
615    Recover {
616        /// Path to recovered keypair file
617        #[clap(short = 'o', long)]
618        outfile: Option<PathBuf>,
619        /// Overwrite the output file if it exists
620        #[clap(short, long)]
621        force: bool,
622        /// Skip seed phrase validation
623        #[clap(long)]
624        skip_seed_phrase_validation: bool,
625        /// Do not prompt for a passphrase
626        #[clap(long)]
627        no_passphrase: bool,
628    },
629    /// Verify a keypair can sign and verify a message
630    Verify {
631        /// Public key to verify
632        pubkey: Pubkey,
633        /// Keypair filepath (defaults to configured wallet)
634        keypair: Option<PathBuf>,
635    },
636}
637
638#[derive(Debug, Parser, AbsolutePath)]
639pub enum KeysCommand {
640    /// List all of the program keys.
641    List,
642    /// Sync program `declare_id!` pubkeys with the program's actual pubkey.
643    Sync {
644        /// Only sync the given program instead of all programs
645        #[clap(short, long)]
646        program_name: Option<String>,
647    },
648}
649
650#[derive(Debug, Parser, AbsolutePath)]
651pub enum ProgramCommand {
652    /// Deploy an upgradeable program
653    Deploy {
654        /// Program filepath (e.g., target/deploy/my_program.so).
655        /// If not provided, discovers programs from workspace
656        program_filepath: Option<PathBuf>,
657        /// Program name to deploy (from workspace). Used when program_filepath is not provided
658        #[clap(short, long)]
659        program_name: Option<String>,
660        /// Program keypair filepath (defaults to target/deploy/{program_name}-keypair.json)
661        #[clap(long)]
662        program_keypair: Option<PathBuf>,
663        /// Upgrade authority keypair (defaults to configured wallet)
664        #[clap(long)]
665        upgrade_authority: Option<String>,
666        /// Program id to deploy to (derived from program-keypair if not specified)
667        #[clap(long)]
668        program_id: Option<Pubkey>,
669        /// Buffer account to use for deployment
670        #[clap(long)]
671        buffer: Option<Pubkey>,
672        /// Maximum transaction length (BPF loader upgradeable limit)
673        #[clap(long)]
674        max_len: Option<usize>,
675        /// Send write transactions through RPC instead of TPU.
676        #[clap(long)]
677        use_rpc: bool,
678        /// Don't upload IDL during deployment (IDL is uploaded by default)
679        #[clap(long)]
680        no_idl: bool,
681        /// Make the program immutable after deployment (cannot be upgraded)
682        #[clap(long = "final")]
683        make_final: bool,
684        /// Additional arguments to configure deployment (e.g., --with-compute-unit-price 1000)
685        #[clap(required = false, last = true)]
686        solana_args: Vec<String>,
687    },
688    /// Write a program into a buffer account
689    WriteBuffer {
690        /// Program filepath (e.g., target/deploy/my_program.so).
691        /// If not provided, discovers program from workspace using program_name
692        program_filepath: Option<PathBuf>,
693        /// Program name to write (from workspace). Used when program_filepath is not provided
694        #[clap(short, long)]
695        program_name: Option<String>,
696        /// Buffer account keypair (defaults to new keypair)
697        #[clap(long)]
698        buffer: Option<String>,
699        /// Buffer authority (defaults to configured wallet)
700        #[clap(long)]
701        buffer_authority: Option<String>,
702        /// Maximum transaction length
703        #[clap(long)]
704        max_len: Option<usize>,
705    },
706    /// Set a new buffer authority
707    SetBufferAuthority {
708        /// Buffer account address
709        buffer: Pubkey,
710        /// New buffer authority
711        new_buffer_authority: Pubkey,
712    },
713    /// Set a new program authority
714    SetUpgradeAuthority {
715        /// Program id
716        program_id: Pubkey,
717        /// New upgrade authority pubkey
718        #[clap(long)]
719        new_upgrade_authority: Option<Pubkey>,
720        /// New upgrade authority signer (keypair file). Required unless --skip-new-upgrade-authority-signer-check is used.
721        /// When provided, both current and new authority will sign (checked mode, recommended)
722        #[clap(long)]
723        new_upgrade_authority_signer: Option<String>,
724        /// Skip new upgrade authority signer check. Allows setting authority with only current authority signature.
725        /// WARNING: Less safe - use only if you're confident the pubkey is correct
726        #[clap(long)]
727        skip_new_upgrade_authority_signer_check: bool,
728        /// Make the program immutable (cannot be upgraded)
729        #[clap(long = "final")]
730        make_final: bool,
731        /// Current upgrade authority keypair (defaults to configured wallet)
732        #[clap(long)]
733        upgrade_authority: Option<String>,
734    },
735    /// Display information about a buffer or program
736    Show {
737        /// Account address (buffer or program)
738        account: Pubkey,
739        /// Get account information from the Solana config file
740        #[clap(long)]
741        get_programs: bool,
742        /// Get account information from the Solana config file
743        #[clap(long)]
744        get_buffers: bool,
745        /// Show all accounts
746        #[clap(long)]
747        all: bool,
748    },
749    /// Upgrade an upgradeable program
750    Upgrade {
751        /// Program id to upgrade
752        program_id: Pubkey,
753        /// Program filepath (e.g., target/deploy/my_program.so). If not provided, discovers from workspace
754        #[clap(long)]
755        program_filepath: Option<PathBuf>,
756        /// Program name to upgrade (from workspace). Used when program_filepath is not provided
757        #[clap(short, long)]
758        program_name: Option<String>,
759        /// Existing buffer account to upgrade from. If not provided, auto-discovers program from workspace
760        #[clap(long)]
761        buffer: Option<Pubkey>,
762        /// Upgrade authority (defaults to configured wallet)
763        #[clap(long)]
764        upgrade_authority: Option<String>,
765        /// Max times to retry on failure
766        #[clap(long, default_value = "0")]
767        max_retries: u32,
768        /// Send write transactions through RPC instead of TPU.
769        #[clap(long)]
770        use_rpc: bool,
771        /// Additional arguments to configure deployment (e.g., --with-compute-unit-price 1000)
772        #[clap(required = false, last = true)]
773        solana_args: Vec<String>,
774    },
775    /// Write the program data to a file
776    Dump {
777        /// Program account address
778        account: Pubkey,
779        /// Output file path
780        output_file: String,
781    },
782    /// Close a program or buffer account and withdraw all lamports
783    Close {
784        /// Account address to close (buffer or program).
785        /// If not provided, discovers program from workspace using program_name
786        account: Option<Pubkey>,
787        /// Program name to close (from workspace). Used when account is not provided
788        #[clap(short, long)]
789        program_name: Option<String>,
790        /// Authority keypair (defaults to configured wallet)
791        #[clap(long)]
792        authority: Option<String>,
793        /// Recipient address for reclaimed lamports (defaults to authority)
794        #[clap(long)]
795        recipient: Option<Pubkey>,
796        /// Bypass warning prompts
797        #[clap(long)]
798        bypass_warning: bool,
799    },
800    /// Extend the length of an upgradeable program
801    Extend {
802        /// Program id to extend.
803        /// If not provided, discovers program from workspace using program_name
804        program_id: Option<Pubkey>,
805        /// Program name to extend (from workspace). Used when program_id is not provided
806        #[clap(short, long)]
807        program_name: Option<String>,
808        /// Additional bytes to allocate
809        additional_bytes: usize,
810    },
811}
812
813#[derive(Debug, Parser, AbsolutePath)]
814pub enum IdlCommand {
815    /// Initializes a program's IDL account. Can only be run once.
816    Init {
817        /// Program id to initialize IDL for.
818        /// If not provided, discovers program ID from IDL.
819        program_id: Option<Pubkey>,
820        #[clap(short, long)]
821        filepath: PathBuf,
822        #[clap(long)]
823        priority_fee: Option<u64>,
824        /// Create non-canonical metadata account (third-party metadata)
825        #[clap(long)]
826        non_canonical: bool,
827        /// Allow running against a localnet cluster (disabled by default)
828        #[clap(long)]
829        #[cfg(feature = "idl-localnet-testing")]
830        allow_localnet: bool,
831    },
832    /// Upgrades the IDL to the new file. An alias for first writing and then
833    /// then setting the idl buffer account.
834    Upgrade {
835        /// Program id to upgrade IDL for.
836        /// If not provided, discovers program ID from IDL.
837        program_id: Option<Pubkey>,
838        #[clap(short, long)]
839        filepath: PathBuf,
840        #[clap(long)]
841        priority_fee: Option<u64>,
842        /// Allow running against a localnet cluster (disabled by default)
843        #[clap(long)]
844        #[cfg(feature = "idl-localnet-testing")]
845        allow_localnet: bool,
846    },
847    /// Generates the IDL for the program using the compilation method.
848    #[clap(alias = "b")]
849    Build {
850        // Program name to build the IDL of(current dir's program if not specified)
851        #[clap(short, long)]
852        program_name: Option<String>,
853        /// Output file for the IDL (stdout if not specified)
854        #[clap(short, long)]
855        out: Option<String>,
856        /// Output file for the TypeScript IDL
857        #[clap(short = 't', long)]
858        out_ts: Option<String>,
859        /// Suppress doc strings in output
860        #[clap(long)]
861        no_docs: bool,
862        /// Do not check for safety comments
863        #[clap(long)]
864        skip_lint: bool,
865        /// Arguments to pass to the underlying `cargo test` command
866        #[clap(required = false, last = true)]
867        cargo_args: Vec<String>,
868    },
869    /// Fetches an IDL for the given program from a cluster.
870    Fetch {
871        program_id: Pubkey,
872        /// Output file for the IDL (stdout if not specified).
873        #[clap(short, long)]
874        out: Option<String>,
875        /// Fetch non-canonical metadata account (third-party metadata)
876        #[clap(long)]
877        non_canonical: bool,
878    },
879    /// Fetches historical IDL versions for the given program from a cluster.
880    ///
881    /// With no filters, fetches all historical versions.
882    FetchHistorical {
883        program_id: Pubkey,
884        /// Fetch authority-scoped PMP metadata account history for this authority
885        #[clap(long)]
886        authority: Option<Pubkey>,
887        /// Fetch IDL at specific slot
888        #[clap(long, conflicts_with_all = ["before", "after"])]
889        slot: Option<u64>,
890        /// Fetch IDL before this date (YYYY-MM-DD)
891        #[clap(long)]
892        before: Option<String>,
893        /// Fetch IDL after this date (YYYY-MM-DD)
894        #[clap(long)]
895        after: Option<String>,
896        /// Output directory for fetched versions (defaults to the current directory)
897        #[clap(long)]
898        out_dir: Option<PathBuf>,
899        /// Max parallel RPC workers for transaction fetches.
900        #[clap(long)]
901        rpc_workers: Option<usize>,
902        /// Force sequential transaction fetches (equivalent to --rpc-workers 1).
903        #[clap(long, conflicts_with = "rpc_workers")]
904        no_parallel: bool,
905        /// Max retry attempts per transaction on 429/timeout errors.
906        #[clap(long, default_value_t = 5)]
907        rpc_max_retries: u32,
908        /// Base backoff in milliseconds between retries (doubled each attempt).
909        #[clap(long, default_value_t = 500)]
910        rpc_retry_backoff_ms: u64,
911        /// Hard cap on signatures fetched per history source.
912        #[clap(long, default_value_t = 1000)]
913        max_signatures: usize,
914        /// Print diagnostic progress messages.
915        #[clap(long)]
916        verbose: bool,
917    },
918    /// Convert legacy IDLs (pre Anchor 0.30) to the new IDL spec
919    Convert {
920        /// Path to the IDL file
921        path: PathBuf,
922        /// Output file for the IDL (stdout if not specified)
923        #[clap(short, long)]
924        out: Option<PathBuf>,
925        /// Program id to initialize IDL for.
926        /// If not provided, discovers program ID from IDL.
927        #[clap(short, long)]
928        program_id: Option<Pubkey>,
929        /// Convert a current-spec IDL back to the legacy (pre Anchor
930        /// v0.30) format. Without this flag the converter runs in the
931        /// default direction (legacy -> current).
932        #[clap(long)]
933        to_legacy: bool,
934    },
935    /// Generate TypeScript type for the IDL
936    Type {
937        /// Path to the IDL file
938        path: PathBuf,
939        /// Output file for the IDL (stdout if not specified)
940        #[clap(short, long)]
941        out: Option<PathBuf>,
942    },
943    /// Close a metadata account and recover rent
944    Close {
945        /// The program ID
946        program_id: Pubkey,
947        /// The seed used for the metadata account (default: "idl")
948        #[clap(long, default_value = "idl")]
949        seed: String,
950        /// Priority fees in micro-lamports per compute unit
951        #[clap(long)]
952        priority_fee: Option<u64>,
953    },
954    /// Create a buffer account for metadata
955    CreateBuffer {
956        /// Path to the metadata file
957        #[clap(short, long)]
958        filepath: PathBuf,
959        /// Priority fees in micro-lamports per compute unit
960        #[clap(long)]
961        priority_fee: Option<u64>,
962    },
963    /// Set a new authority on a buffer account
964    SetBufferAuthority {
965        /// The buffer account address
966        buffer: Pubkey,
967        /// The new authority
968        #[clap(short, long)]
969        new_authority: Pubkey,
970        /// Priority fees in micro-lamports per compute unit
971        #[clap(long)]
972        priority_fee: Option<u64>,
973    },
974    /// Write metadata using a buffer account
975    WriteBuffer {
976        /// The program ID
977        program_id: Pubkey,
978        /// The buffer account address
979        #[clap(short, long)]
980        buffer: Pubkey,
981        /// The seed to use for the metadata account (default: "idl")
982        #[clap(long, default_value = "idl")]
983        seed: String,
984        /// Close the buffer after writing
985        #[clap(long)]
986        close_buffer: bool,
987        /// Priority fees in micro-lamports per compute unit
988        #[clap(long)]
989        priority_fee: Option<u64>,
990    },
991}
992
993#[derive(Debug, Parser, AbsolutePath)]
994pub enum ClusterCommand {
995    /// Prints common cluster urls.
996    List,
997}
998
999#[derive(Debug, Parser, AbsolutePath)]
1000pub enum ConfigCommand {
1001    /// Get configuration settings from the local Anchor.toml
1002    Get,
1003    /// Set configuration settings in the local Anchor.toml
1004    Set {
1005        /// Cluster to connect to (custom URL). Use -um, -ud, -ut, -ul for standard clusters
1006        #[clap(short = 'u', long = "url")]
1007        url: Option<String>,
1008        /// Path to wallet keypair file to update the Anchor.toml file with
1009        #[clap(short = 'k', long = "keypair")]
1010        keypair: Option<PathBuf>,
1011    },
1012}
1013
1014fn get_keypair(path: &Path) -> Result<Keypair> {
1015    solana_keypair::read_keypair_file(path)
1016        .map_err(|_| anyhow!("Unable to read keypair file ({})", path.display()))
1017}
1018
1019/// Format lamports as SOL with trailing zeros removed
1020fn format_sol(lamports: u64) -> String {
1021    let sol = lamports as f64 / 1_000_000_000.0;
1022    let formatted = format!("{:.8}", sol);
1023
1024    // Remove trailing zeros and decimal point if not needed
1025    let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
1026    format!("{} SOL", trimmed)
1027}
1028
1029/// Get cluster URL and wallet path from Anchor config, CLI overrides, or Solana CLI config
1030fn get_cluster_and_wallet(cfg_override: &ConfigOverride) -> Result<(String, String)> {
1031    // Try to get from Anchor workspace config first
1032    if let Ok(Some(cfg)) = Config::discover(cfg_override) {
1033        return Ok((
1034            cfg.provider.cluster.url().to_string(),
1035            cfg.provider.wallet.to_string(),
1036        ));
1037    }
1038
1039    // Try to load Solana CLI config
1040    let (cluster_url, wallet_path) =
1041        if let Some(config_file) = solana_cli_config::CONFIG_FILE.as_ref() {
1042            match SolanaCliConfig::load(config_file) {
1043                Ok(cli_config) => (
1044                    cli_config.json_rpc_url.clone(),
1045                    cli_config.keypair_path.clone(),
1046                ),
1047                Err(_) => {
1048                    // Fallback to defaults if Solana CLI config doesn't exist
1049                    (
1050                        "https://api.mainnet-beta.solana.com".to_string(),
1051                        dirs::home_dir()
1052                            .map(|home| {
1053                                home.join(".config/solana/id.json")
1054                                    .to_string_lossy()
1055                                    .to_string()
1056                            })
1057                            .unwrap_or_else(|| "~/.config/solana/id.json".to_string()),
1058                    )
1059                }
1060            }
1061        } else {
1062            // If CONFIG_FILE is None, use defaults
1063            (
1064                "https://api.mainnet-beta.solana.com".to_string(),
1065                dirs::home_dir()
1066                    .map(|home| {
1067                        home.join(".config/solana/id.json")
1068                            .to_string_lossy()
1069                            .to_string()
1070                    })
1071                    .unwrap_or_else(|| "~/.config/solana/id.json".to_string()),
1072            )
1073        };
1074
1075    // Apply cluster override if provided
1076    let final_cluster = if let Some(cluster) = &cfg_override.cluster {
1077        cluster.url().to_string()
1078    } else {
1079        cluster_url
1080    };
1081
1082    Ok((final_cluster, wallet_path))
1083}
1084
1085/// Get the recommended priority fee from the RPC client, falling back to 0 if unavailable.
1086/// `write_locked_accounts` scopes the query to txs that write-locked all of these
1087/// accounts in recent blocks — passing the accounts the upcoming tx will lock
1088/// gives a contention-aware fee. Pass `&[]` for a global median (often too low
1089/// for hot mainnet windows).
1090pub fn get_recommended_micro_lamport_fee(
1091    client: &RpcClient,
1092    write_locked_accounts: &[Pubkey],
1093) -> u64 {
1094    let mut fees = match client.get_recent_prioritization_fees(write_locked_accounts) {
1095        // Fees may be empty or query may fail, e.g. on localnet
1096        Err(e) => {
1097            eprintln!("Warning: failed to fetch prioritization fees, defaulting to 0: {e}");
1098            return 0;
1099        }
1100        Ok(f) if f.is_empty() => {
1101            return 0;
1102        }
1103        Ok(f) => f,
1104    };
1105
1106    // Get the median fee from the most recent 150 slots' prioritization fee
1107    fees.sort_unstable_by_key(|fee| fee.prioritization_fee);
1108    let median_index = fees.len() / 2;
1109
1110    if fees.len() % 2 == 0 {
1111        (fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2
1112    } else {
1113        fees[median_index].prioritization_fee
1114    }
1115}
1116
1117/// Prepend a compute unit ix, if the priority fee is greater than 0.
1118pub fn prepend_compute_unit_ix(
1119    instructions: Vec<Instruction>,
1120    client: &RpcClient,
1121    priority_fee: Option<u64>,
1122    write_locked_accounts: &[Pubkey],
1123) -> Vec<Instruction> {
1124    let priority_fee = priority_fee
1125        .unwrap_or_else(|| get_recommended_micro_lamport_fee(client, write_locked_accounts));
1126
1127    if priority_fee > 0 {
1128        let mut instructions_appended = instructions.clone();
1129        instructions_appended.insert(
1130            0,
1131            ComputeBudgetInstruction::set_compute_unit_price(priority_fee),
1132        );
1133        instructions_appended
1134    } else {
1135        instructions
1136    }
1137}
1138
1139pub fn entry(opts: Opts) -> Result<()> {
1140    let opts = opts.absolute();
1141
1142    let restore_cbs = override_toolchain(&opts.cfg_override)?;
1143    let result = process_command(opts);
1144    restore_toolchain(restore_cbs)?;
1145
1146    result
1147}
1148
1149/// Functions to restore toolchain entries
1150type RestoreToolchainCallbacks = Vec<Box<dyn FnOnce() -> Result<()>>>;
1151
1152/// Override the toolchain from `Anchor.toml`.
1153///
1154/// Returns the previous versions to restore back to.
1155fn override_toolchain(cfg_override: &ConfigOverride) -> Result<RestoreToolchainCallbacks> {
1156    let mut restore_cbs: RestoreToolchainCallbacks = vec![];
1157
1158    let cfg = Config::discover(cfg_override)?;
1159    if let Some(cfg) = cfg {
1160        fn parse_version(text: &str) -> Option<String> {
1161            Some(
1162                Regex::new(r"(\d+\.\d+\.\S+)")
1163                    .unwrap()
1164                    .captures_iter(text)
1165                    .next()?
1166                    .get(0)?
1167                    .as_str()
1168                    .to_string(),
1169            )
1170        }
1171
1172        fn get_current_version(cmd_name: &str) -> Result<String> {
1173            let output = std::process::Command::new(cmd_name)
1174                .arg("--version")
1175                .output()?;
1176            if !output.status.success() {
1177                return Err(anyhow!("Failed to run `{cmd_name} --version`"));
1178            }
1179
1180            let output_version = std::str::from_utf8(&output.stdout)?;
1181            parse_version(output_version)
1182                .ok_or_else(|| anyhow!("Failed to parse the version of `{cmd_name}`"))
1183        }
1184
1185        if let Some(solana_version) = &cfg.toolchain.solana_version {
1186            let current_version = get_current_version("solana")?;
1187            if solana_version != &current_version {
1188                // We are overriding with `solana-install` command instead of using the binaries
1189                // from `~/.local/share/solana/install/releases` because we use multiple Solana
1190                // binaries in various commands.
1191                fn override_solana_version(version: String) -> Result<bool> {
1192                    // There is a deprecation warning message starting with `1.18.19` which causes
1193                    // parsing problems https://github.com/otter-sec/anchor/issues/3147
1194                    let (cmd_name, domain) =
1195                        if Version::parse(&version)? < Version::parse("1.18.19")? {
1196                            ("solana-install", "anza.xyz")
1197                        } else {
1198                            ("agave-install", "anza.xyz")
1199                        };
1200
1201                    // Install the command if it's not installed
1202                    if get_current_version(cmd_name).is_err() {
1203                        // `solana-install` and `agave-install` are not usable at the same time i.e.
1204                        // using one of them makes the other unusable with the default installation,
1205                        // causing the installation process to run each time users switch between
1206                        // `agave` supported versions. For example, if the user's active Solana
1207                        // version is `1.18.17`, and he specifies `solana_version = "2.0.6"`, this
1208                        // code path will run each time an Anchor command gets executed.
1209                        eprintln!(
1210                            "Command not installed: `{cmd_name}`. \
1211                            See https://github.com/anza-xyz/agave/wiki/Agave-Transition, \
1212                            installing..."
1213                        );
1214                        let install_script = std::process::Command::new("curl")
1215                            .args([
1216                                "-sSfL",
1217                                &format!("https://release.{domain}/v{version}/install"),
1218                            ])
1219                            .output()?;
1220                        let is_successful = std::process::Command::new("sh")
1221                            .args(["-c", std::str::from_utf8(&install_script.stdout)?])
1222                            .spawn()?
1223                            .wait_with_output()?
1224                            .status
1225                            .success();
1226                        if !is_successful {
1227                            return Err(anyhow!("Failed to install `{cmd_name}`"));
1228                        }
1229                    }
1230
1231                    // Hide the installation progress if the version is already installed.
1232                    // Some installer versions cannot list releases after switching between
1233                    // Solana and Agave. `init` remains able to install the requested version,
1234                    // so continue with visible output instead of failing before the command.
1235                    let is_installed =
1236                        match std::process::Command::new(cmd_name).arg("list").output() {
1237                            Ok(output) if output.status.success() => {
1238                                String::from_utf8_lossy(&output.stdout)
1239                                    .lines()
1240                                    .filter_map(parse_version)
1241                                    .any(|line_version| line_version == version)
1242                            }
1243                            Ok(output) => {
1244                                eprintln!(
1245                                    "Failed to list installed Solana versions with `{cmd_name}`; \
1246                                     continuing with installation:\n{}",
1247                                    String::from_utf8_lossy(&output.stderr).trim()
1248                                );
1249                                false
1250                            }
1251                            Err(err) => {
1252                                eprintln!(
1253                                    "Failed to list installed Solana versions with `{cmd_name}`; \
1254                                     continuing with installation: {err}"
1255                                );
1256                                false
1257                            }
1258                        };
1259                    let (stderr, stdout) = if is_installed {
1260                        (Stdio::null(), Stdio::null())
1261                    } else {
1262                        (Stdio::inherit(), Stdio::inherit())
1263                    };
1264
1265                    std::process::Command::new(cmd_name)
1266                        .arg("init")
1267                        .arg(&version)
1268                        .stderr(stderr)
1269                        .stdout(stdout)
1270                        .spawn()?
1271                        .wait()
1272                        .map(|status| status.success())
1273                        .map_err(|err| anyhow!("Failed to run `{cmd_name}` command: {err}"))
1274                }
1275
1276                match override_solana_version(solana_version.to_owned())? {
1277                    true => restore_cbs.push(Box::new(|| {
1278                        match override_solana_version(current_version)? {
1279                            true => Ok(()),
1280                            false => Err(anyhow!("Failed to restore `solana` version")),
1281                        }
1282                    })),
1283                    false => eprintln!(
1284                        "Failed to override `solana` version to {solana_version}, using \
1285                         {current_version} instead"
1286                    ),
1287                }
1288            }
1289        }
1290
1291        // Anchor version override should be handled last.
1292        //
1293        // When invoked via the AVM proxy (`AVM_ACTIVE=1`), AVM has already resolved
1294        // the requested toolchain version and spawned the matching binary. Re-execing
1295        // here would either be a no-op (binary already matches) or fight AVM's
1296        // resolution (e.g. when AVM resolved via `anchor-lang` in Cargo.toml). Skip.
1297        if let Some(anchor_version) = &cfg.toolchain.anchor_version {
1298            if std::env::var("AVM_ACTIVE").is_ok() {
1299                return Ok(restore_cbs);
1300            }
1301            // Anchor binary name prefix(applies to binaries that are installed via `avm`)
1302            const ANCHOR_BINARY_PREFIX: &str = "anchor-";
1303
1304            // Get the current version from the executing binary name if possible because commit
1305            // based toolchain overrides do not have version information.
1306            let current_version = std::env::args()
1307                .next()
1308                .expect("First arg should exist")
1309                .parse::<PathBuf>()?
1310                .file_name()
1311                .and_then(|name| name.to_str())
1312                .expect("File name should be valid Unicode")
1313                .split_once(ANCHOR_BINARY_PREFIX)
1314                .map(|(_, version)| version)
1315                .unwrap_or(VERSION)
1316                .to_owned();
1317            if anchor_version != &current_version {
1318                let binary_path = home_dir()
1319                    .unwrap()
1320                    .join(".avm")
1321                    .join("bin")
1322                    .join(format!("{ANCHOR_BINARY_PREFIX}{anchor_version}"));
1323
1324                if !binary_path.exists() {
1325                    eprintln!(
1326                        "`anchor` {anchor_version} is not installed with `avm`. Installing...\n"
1327                    );
1328
1329                    if let Err(e) = install_with_avm(anchor_version, false) {
1330                        eprintln!(
1331                            "Failed to install `anchor`: {e}, using {current_version} instead"
1332                        );
1333                        return Ok(restore_cbs);
1334                    }
1335                }
1336
1337                let exit_code = std::process::Command::new(binary_path)
1338                    .args(std::env::args_os().skip(1))
1339                    .spawn()?
1340                    .wait()?
1341                    .code()
1342                    .unwrap_or(1);
1343                restore_toolchain(restore_cbs)?;
1344                std::process::exit(exit_code);
1345            }
1346        }
1347    }
1348
1349    Ok(restore_cbs)
1350}
1351
1352/// Installs Anchor using AVM, passing `--force` (and optionally) installing
1353/// `solana-verify`.
1354fn install_with_avm(version: &str, verify: bool) -> Result<()> {
1355    let mut cmd = std::process::Command::new("avm");
1356    cmd.arg("install");
1357    cmd.arg(version);
1358    cmd.arg("--force");
1359    if verify {
1360        cmd.arg("--verify");
1361    }
1362    let status = cmd.status().context("running AVM")?;
1363    if !status.success() {
1364        bail!("failed to install `anchor` {version} with avm");
1365    }
1366    Ok(())
1367}
1368
1369/// Restore toolchain to how it was before the command was run.
1370fn restore_toolchain(restore_cbs: RestoreToolchainCallbacks) -> Result<()> {
1371    for restore_toolchain in restore_cbs {
1372        if let Err(e) = restore_toolchain() {
1373            eprintln!("Toolchain error: {e}");
1374        }
1375    }
1376
1377    Ok(())
1378}
1379
1380/// Get the system's default license - what 'npm init' would use.
1381fn get_npm_init_license() -> Result<String> {
1382    let npm_init_license_output = std::process::Command::new("npm")
1383        .arg("config")
1384        .arg("get")
1385        .arg("init-license")
1386        .output()?;
1387
1388    if !npm_init_license_output.status.success() {
1389        return Err(anyhow!("Failed to get npm init license"));
1390    }
1391
1392    let license = String::from_utf8(npm_init_license_output.stdout)?;
1393    Ok(license.trim().to_string())
1394}
1395
1396fn process_command(opts: Opts) -> Result<()> {
1397    match opts.command {
1398        Command::Init {
1399            name,
1400            javascript,
1401            no_install,
1402            package_manager,
1403            no_git,
1404            template,
1405            anchor_version,
1406            test_template,
1407            force,
1408            install_agent_skills,
1409        } => init(
1410            &opts.cfg_override,
1411            name,
1412            javascript,
1413            no_install,
1414            package_manager,
1415            no_git,
1416            template,
1417            anchor_version,
1418            test_template,
1419            force,
1420            install_agent_skills,
1421        ),
1422        Command::Fuzz(cli) => crucible_fuzz_cli::run(cli),
1423        Command::New {
1424            name,
1425            template,
1426            anchor_version,
1427            force,
1428        } => new(&opts.cfg_override, name, template, anchor_version, force),
1429        Command::Build {
1430            no_idl,
1431            idl,
1432            idl_ts,
1433            verifiable,
1434            program_name,
1435            solana_version,
1436            tools_version,
1437            arch,
1438            docker_image,
1439            bootstrap,
1440            cargo_args,
1441            env,
1442            skip_lint,
1443            ignore_keys,
1444            no_docs,
1445        } => build(
1446            &opts.cfg_override,
1447            no_idl,
1448            idl,
1449            idl_ts,
1450            verifiable,
1451            skip_lint,
1452            ignore_keys,
1453            program_name,
1454            solana_version,
1455            docker_image,
1456            bootstrap,
1457            BuildSbfOptions::from_build_command(tools_version, arch),
1458            None,
1459            None,
1460            env,
1461            cargo_args,
1462            no_docs,
1463        ),
1464        Command::Verify {
1465            program_id,
1466            repo_url,
1467            commit_hash,
1468            current_dir,
1469            program_name,
1470            args,
1471        } => verify(
1472            program_id,
1473            repo_url,
1474            commit_hash,
1475            current_dir,
1476            program_name,
1477            args,
1478        ),
1479        Command::Clean => clean(&opts.cfg_override),
1480        #[allow(deprecated)]
1481        Command::Deploy {
1482            program_name,
1483            program_keypair,
1484            verifiable,
1485            no_idl,
1486            solana_args,
1487        } => {
1488            eprintln!(
1489                "Warning: 'anchor deploy' is deprecated. Use 'anchor program deploy' instead."
1490            );
1491            deploy(
1492                &opts.cfg_override,
1493                program_name,
1494                program_keypair,
1495                verifiable,
1496                no_idl,
1497                solana_args,
1498            )
1499        }
1500        Command::Expand {
1501            program_name,
1502            stdout,
1503            cargo_args,
1504        } => expand(&opts.cfg_override, program_name, stdout, &cargo_args),
1505        #[allow(deprecated)]
1506        Command::Upgrade {
1507            program_id,
1508            program_filepath,
1509            max_retries,
1510            solana_args,
1511        } => {
1512            eprintln!(
1513                "Warning: 'anchor upgrade' is deprecated. Use 'anchor program upgrade' instead."
1514            );
1515            upgrade(
1516                &opts.cfg_override,
1517                program_id,
1518                program_filepath,
1519                max_retries,
1520                solana_args,
1521            )
1522        }
1523        Command::Idl { subcmd } => idl(&opts.cfg_override, subcmd),
1524        Command::LegacyIdl { subcmd } => {
1525            legacy_idl::handle_legacy_idl_command(&opts.cfg_override, subcmd)
1526        }
1527        Command::Migrate => migrate(&opts.cfg_override),
1528        Command::Test {
1529            program_name,
1530            skip_deploy,
1531            skip_local_validator,
1532            skip_build,
1533            no_idl,
1534            detach,
1535            run,
1536            script,
1537            validator,
1538            profile,
1539            args,
1540            env,
1541            cargo_args,
1542            skip_lint,
1543        } => test(
1544            &opts.cfg_override,
1545            program_name,
1546            skip_deploy,
1547            skip_local_validator,
1548            skip_build,
1549            skip_lint,
1550            no_idl,
1551            detach,
1552            run,
1553            script,
1554            validator,
1555            profile,
1556            false,
1557            args,
1558            env,
1559            cargo_args,
1560        ),
1561        #[cfg(not(windows))]
1562        Command::Debugger {
1563            test_name,
1564            skip_run,
1565            skip_build,
1566            skip_lint,
1567            gdb,
1568            cargo_args,
1569        } => debugger(
1570            &opts.cfg_override,
1571            test_name,
1572            skip_run,
1573            skip_build,
1574            skip_lint,
1575            gdb,
1576            cargo_args,
1577        ),
1578        #[cfg(not(windows))]
1579        Command::Coverage {
1580            skip_run,
1581            skip_build,
1582            output,
1583            trace_dir,
1584            cargo_args,
1585        } => run_coverage(
1586            &opts.cfg_override,
1587            skip_run,
1588            skip_build,
1589            &output,
1590            &trace_dir,
1591            cargo_args,
1592        ),
1593        Command::Airdrop { amount, pubkey } => airdrop(&opts.cfg_override, amount, pubkey),
1594        Command::Cluster { subcmd } => cluster(subcmd),
1595        Command::Config { subcmd } => config_cmd(&opts.cfg_override, subcmd),
1596        Command::Shell => shell(&opts.cfg_override),
1597        Command::Run {
1598            script,
1599            script_args,
1600        } => run(&opts.cfg_override, script, script_args),
1601        Command::Keys { subcmd } => keys(&opts.cfg_override, subcmd),
1602        Command::Localnet {
1603            skip_build,
1604            skip_deploy,
1605            skip_lint,
1606            ignore_keys,
1607            validator,
1608            env,
1609            cargo_args,
1610        } => localnet(
1611            &opts.cfg_override,
1612            skip_build,
1613            skip_deploy,
1614            skip_lint,
1615            ignore_keys,
1616            validator,
1617            env,
1618            cargo_args,
1619        ),
1620        Command::Account {
1621            account_type,
1622            address,
1623            idl,
1624        } => account(&opts.cfg_override, account_type, address, idl),
1625        Command::Completions { shell } => {
1626            clap_complete::generate(
1627                shell,
1628                &mut Opts::command(),
1629                "anchor",
1630                &mut std::io::stdout(),
1631            );
1632            Ok(())
1633        }
1634        Command::Address => address(&opts.cfg_override),
1635        Command::Balance { pubkey, lamports } => balance(&opts.cfg_override, pubkey, lamports),
1636        Command::Epoch => epoch(&opts.cfg_override),
1637        Command::EpochInfo => epoch_info(&opts.cfg_override),
1638        Command::Logs {
1639            include_votes,
1640            address,
1641        } => logs_subscribe(&opts.cfg_override, include_votes, address),
1642        Command::ShowAccount { cmd } => account::show_account(&opts.cfg_override, cmd),
1643        Command::Keygen { subcmd } => keygen::keygen(&opts.cfg_override, subcmd),
1644        Command::Program { subcmd } => program::program(&opts.cfg_override, subcmd),
1645        Command::Codama { subcmd } => codama::entry(subcmd),
1646    }
1647}
1648
1649/// Cargo does not support nested workspaces. If `start` lives inside a
1650/// directory tree containing any `Cargo.toml`, refuse to create a new
1651/// Anchor workspace here and point at `anchor new`, which is the
1652/// supported flow for adding a program to an existing project.
1653fn reject_if_inside_cargo_project(start: PathBuf) -> Result<()> {
1654    if let Some(parent) = Manifest::discover_from_path(start)? {
1655        return Err(anyhow!(
1656            "Cannot run `anchor init` inside an existing Cargo project at `{}`.\nTo add a new \
1657             program to the existing project, run `anchor new <name>` from the workspace root. To \
1658             create a fresh Anchor workspace, run `anchor init` outside any Cargo project tree.",
1659            parent.path().display()
1660        ));
1661    }
1662    Ok(())
1663}
1664
1665#[allow(clippy::too_many_arguments)]
1666fn init(
1667    cfg_override: &ConfigOverride,
1668    name: String,
1669    javascript: bool,
1670    no_install: bool,
1671    package_manager: Option<PackageManager>,
1672    no_git: bool,
1673    template: ProgramTemplate,
1674    anchor_version: AnchorVersion,
1675    test_template: TestTemplate,
1676    force: bool,
1677    install_agent_skills: bool,
1678) -> Result<()> {
1679    if !force {
1680        if Config::discover(cfg_override)?.is_some() {
1681            return Err(anyhow!("Workspace already initialized"));
1682        }
1683        reject_if_inside_cargo_project(std::env::current_dir()?)?;
1684    }
1685
1686    // We need to format different cases for the dir and the name
1687    let rust_name = name.to_snake_case();
1688    let project_name = if name == rust_name {
1689        rust_name.clone()
1690    } else {
1691        name.to_kebab_case()
1692    };
1693
1694    // Additional keywords that have not been added to the `syn` crate as reserved words
1695    // https://github.com/dtolnay/syn/pull/1098
1696    let extra_keywords = ["async", "await", "try"];
1697    // Anchor converts to snake case before writing the program name
1698    if syn::parse_str::<syn::Ident>(&rust_name).is_err()
1699        || extra_keywords.contains(&rust_name.as_str())
1700    {
1701        return Err(anyhow!(
1702            "Anchor workspace name must be a valid Rust identifier. It may not be a Rust reserved word, start with a digit, or include certain disallowed characters. See https://doc.rust-lang.org/reference/identifiers.html for more detail.",
1703        ));
1704    }
1705
1706    if force {
1707        fs::create_dir_all(&project_name)?;
1708    } else {
1709        fs::create_dir(&project_name)?;
1710    }
1711    std::env::set_current_dir(&project_name)?;
1712    fs::create_dir_all("app")?;
1713
1714    let mut cfg = Config::default();
1715
1716    let uses_node = test_template.uses_node();
1717    let package_manager = if uses_node {
1718        Some(resolve_package_manager(package_manager)?)
1719    } else {
1720        None
1721    };
1722    let test_script = test_template.get_test_script(javascript, package_manager.as_ref());
1723    cfg.scripts.insert("test".to_owned(), test_script);
1724
1725    // In-process test templates drive the Solana VM inside `cargo test`, so
1726    // auto-starting a validator at `anchor test` time is unnecessary.
1727    if matches!(test_template, TestTemplate::Litesvm | TestTemplate::Mollusk) {
1728        cfg.skip_local_validator = Some(true);
1729    }
1730
1731    let package_manager_cmd = package_manager.as_ref().map(ToString::to_string);
1732    if uses_node {
1733        cfg.toolchain.package_manager = package_manager.clone();
1734    }
1735
1736    // Initialize .gitignore file
1737    fs::write(".gitignore", template::git_ignore())?;
1738
1739    // Initialize .prettierignore file
1740    fs::write(".prettierignore", template::prettier_ignore())?;
1741
1742    // Remove the default program if `--force` is passed
1743    if force {
1744        let default_program_dir = std::env::current_dir()?
1745            .join("programs")
1746            .join(&project_name);
1747        if default_program_dir.exists() {
1748            fs::remove_dir_all(default_program_dir)?;
1749        }
1750    }
1751
1752    // Build the program.
1753    template::create_program(
1754        &project_name,
1755        template,
1756        Some(&test_template),
1757        anchor_version,
1758    )?;
1759
1760    let program_id = template::get_or_create_program_id(&rust_name, target_dir()?);
1761    let mut localnet = BTreeMap::new();
1762    localnet.insert(
1763        rust_name,
1764        ProgramDeployment {
1765            address: program_id,
1766            path: None,
1767            idl: None,
1768        },
1769    );
1770    cfg.programs.insert(Cluster::Localnet, localnet);
1771    let toml = cfg.to_string();
1772    fs::write("Anchor.toml", toml)?;
1773
1774    if uses_node {
1775        // Build the migrations directory.
1776        let migrations_path = Path::new("migrations");
1777        fs::create_dir_all(migrations_path)?;
1778
1779        let license = get_npm_init_license()?;
1780
1781        let jest = TestTemplate::Jest == test_template;
1782        if javascript {
1783            // Build javascript config
1784            let mut package_json = File::create("package.json")?;
1785            package_json
1786                .write_all(template::package_json(jest, license, anchor_version).as_bytes())?;
1787
1788            let mut deploy = File::create(migrations_path.join("deploy.js"))?;
1789            deploy.write_all(template::deploy_script().as_bytes())?;
1790        } else {
1791            // Build typescript config
1792            let mut ts_config = File::create("tsconfig.json")?;
1793            ts_config.write_all(template::ts_config(jest).as_bytes())?;
1794
1795            let mut ts_package_json = File::create("package.json")?;
1796            ts_package_json
1797                .write_all(template::ts_package_json(jest, license, anchor_version).as_bytes())?;
1798
1799            let mut deploy = File::create(migrations_path.join("deploy.ts"))?;
1800            deploy.write_all(template::ts_deploy_script().as_bytes())?;
1801        }
1802    }
1803
1804    test_template.create_test_files(
1805        &project_name,
1806        javascript,
1807        &program_id.to_string(),
1808        anchor_version,
1809    )?;
1810
1811    if !no_install && uses_node {
1812        let package_manager_cmd =
1813            package_manager_cmd.expect("Node templates resolve a package manager");
1814        let output = install_node_modules(&package_manager_cmd)?;
1815        if !output.status.success() {
1816            return Err(anyhow!(
1817                "`{package_manager_cmd} install` failed (exit code {:?}). Re-run with \
1818                 `--no-install` to keep the generated files without installing dependencies.",
1819                output.status.code()
1820            ));
1821        }
1822    }
1823
1824    if !no_git {
1825        let git_result = std::process::Command::new("git")
1826            .arg("init")
1827            .stdout(Stdio::inherit())
1828            .stderr(Stdio::inherit())
1829            .output()
1830            .map_err(|e| anyhow::format_err!("git init failed: {}", e))?;
1831        if !git_result.status.success() {
1832            eprintln!("Failed to automatically initialize a new git repository");
1833        }
1834    }
1835
1836    if install_agent_skills {
1837        install_solana_skill();
1838    }
1839
1840    println!("{project_name} initialized");
1841
1842    Ok(())
1843}
1844
1845fn install_solana_skill() {
1846    const SKILL_REPO: &str = "https://github.com/solana-foundation/solana-dev-skill";
1847    const SKILL_NAME: &str = "solana-dev";
1848
1849    // Skip if globally installed (active across all projects already)
1850    if home_dir().is_some_and(|home| {
1851        home.join(".agents")
1852            .join("skills")
1853            .join(SKILL_NAME)
1854            .exists()
1855    }) {
1856        return;
1857    }
1858
1859    // Skip if already project-scoped (could be anchor init --force on existing folder)
1860    let project_path = Path::new(".agents").join("skills").join(SKILL_NAME);
1861    if project_path.exists() {
1862        return;
1863    }
1864
1865    println!("Installing Solana dev skill for Agents from {SKILL_REPO}");
1866
1867    let status = std::process::Command::new("npx")
1868        .args([
1869            "--yes",
1870            "skills@1.4.4",
1871            "add",
1872            SKILL_REPO,
1873            "--skill",
1874            "*",
1875            "-y",
1876        ])
1877        .stdout(Stdio::inherit())
1878        .stderr(Stdio::inherit())
1879        .status();
1880
1881    match status {
1882        Ok(s) if s.success() => {
1883            println!("Solana dev skill installed successfully");
1884        }
1885        _ => {
1886            eprintln!(
1887                "Warning: Failed to install Solana dev skill. Install manually with:\n  npx \
1888                 skills add {SKILL_REPO}"
1889            );
1890        }
1891    }
1892}
1893
1894const PACKAGE_MANAGER_WATERFALL: &[PackageManager] = &[
1895    PackageManager::PNPM,
1896    PackageManager::Yarn,
1897    PackageManager::NPM,
1898];
1899
1900fn package_manager_available(pm: &PackageManager) -> bool {
1901    let cmd = pm.to_string();
1902    let mut command = if cfg!(target_os = "windows") {
1903        let mut command = std::process::Command::new("cmd");
1904        command.arg(format!("/C {cmd} --version"));
1905        command
1906    } else {
1907        let mut command = std::process::Command::new(&cmd);
1908        command.arg("--version");
1909        command
1910    };
1911    command
1912        .stdout(Stdio::null())
1913        .stderr(Stdio::null())
1914        .status()
1915        .map(|status| status.success())
1916        .unwrap_or(false)
1917}
1918
1919fn resolve_package_manager(explicit: Option<PackageManager>) -> Result<PackageManager> {
1920    if let Some(pm) = explicit {
1921        if !package_manager_available(&pm) {
1922            return Err(anyhow!(
1923                "`{pm}` was requested but is not on PATH. Install it or pick a different package \
1924                 manager with `--package-manager`."
1925            ));
1926        }
1927        return Ok(pm);
1928    }
1929
1930    let mut skipped = Vec::new();
1931    for candidate in PACKAGE_MANAGER_WATERFALL {
1932        if package_manager_available(candidate) {
1933            if !skipped.is_empty() {
1934                let missing = skipped
1935                    .iter()
1936                    .map(|pm: &PackageManager| pm.to_string())
1937                    .collect::<Vec<_>>()
1938                    .join(", ");
1939                eprintln!("warning: {missing} not found on PATH, using `{candidate}` instead");
1940            }
1941            return Ok(candidate.clone());
1942        }
1943        skipped.push(candidate.clone());
1944    }
1945
1946    Err(anyhow!(
1947        "No supported package manager found on PATH (tried pnpm, yarn, npm). Install one of them, \
1948         or re-run with `--no-install`."
1949    ))
1950}
1951
1952fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
1953    if cfg!(target_os = "windows") {
1954        std::process::Command::new("cmd")
1955            .arg(format!("/C {cmd} install"))
1956            .stdout(Stdio::inherit())
1957            .stderr(Stdio::inherit())
1958            .output()
1959            .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e))
1960    } else {
1961        std::process::Command::new(cmd)
1962            .arg("install")
1963            .stdout(Stdio::inherit())
1964            .stderr(Stdio::inherit())
1965            .output()
1966            .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e))
1967    }
1968}
1969
1970// Creates a new program crate in the `programs/<name>` directory.
1971fn new(
1972    cfg_override: &ConfigOverride,
1973    name: String,
1974    template: ProgramTemplate,
1975    anchor_version: AnchorVersion,
1976    force: bool,
1977) -> Result<()> {
1978    with_workspace(cfg_override, |cfg| -> Result<()> {
1979        match cfg.path().parent() {
1980            None => {
1981                println!("Unable to make new program");
1982            }
1983            Some(parent) => {
1984                std::env::set_current_dir(parent)?;
1985
1986                let cluster = cfg.provider.cluster.clone();
1987                let programs = cfg.programs.entry(cluster).or_default();
1988                if programs.contains_key(&name) {
1989                    if !force {
1990                        return Err(anyhow!("Program already exists"));
1991                    }
1992
1993                    // Delete all files within the program folder
1994                    fs::remove_dir_all(std::env::current_dir()?.join("programs").join(&name))?;
1995                }
1996
1997                template::create_program(&name, template, None, anchor_version)?;
1998
1999                programs.insert(
2000                    name.clone(),
2001                    ProgramDeployment {
2002                        address: template::get_or_create_program_id(&name, target_dir()?),
2003                        path: None,
2004                        idl: None,
2005                    },
2006                );
2007
2008                let toml = cfg.to_string();
2009                fs::write("Anchor.toml", toml)?;
2010
2011                println!("Created new program.");
2012            }
2013        };
2014        Ok(())
2015    })?
2016}
2017
2018/// Array of (path, content) tuple.
2019pub type Files = Vec<(PathBuf, String)>;
2020
2021/// Create files from the given (path, content) tuple array.
2022///
2023/// # Example
2024///
2025/// ```rust,no_run
2026/// # use anchor_cli::create_files;
2027/// # use std::path::PathBuf;
2028/// # fn main() -> anyhow::Result<()> {
2029/// let files = vec![(PathBuf::from("programs/my_program/src/lib.rs"), "// Content".to_string())];
2030/// create_files(&files)?;
2031/// # Ok(())
2032/// # }
2033/// ```
2034pub fn create_files(files: &Files) -> Result<()> {
2035    for (path, content) in files {
2036        let path = path
2037            .display()
2038            .to_string()
2039            .replace('/', std::path::MAIN_SEPARATOR_STR);
2040        let path = Path::new(&path);
2041        if path.exists() {
2042            continue;
2043        }
2044
2045        match path.extension() {
2046            Some(_) => {
2047                fs::create_dir_all(path.parent().unwrap())?;
2048                fs::write(path, content)?;
2049            }
2050            None => fs::create_dir_all(path)?,
2051        }
2052    }
2053
2054    Ok(())
2055}
2056
2057/// Override or create files from the given (path, content) tuple array.
2058///
2059/// # Example
2060///
2061/// ```rust,no_run
2062/// # use anchor_cli::override_or_create_files;
2063/// # use std::path::PathBuf;
2064/// # fn main() -> anyhow::Result<()> {
2065/// let files = vec![(PathBuf::from("test.rs"), "// Content".to_string())];
2066/// override_or_create_files(&files)?;
2067/// # Ok(())
2068/// # }
2069/// ```
2070pub fn override_or_create_files(files: &Files) -> Result<()> {
2071    for (path, content) in files {
2072        let path = Path::new(path);
2073        if path.exists() {
2074            let mut f = fs::OpenOptions::new()
2075                .write(true)
2076                .truncate(true)
2077                .open(path)?;
2078            f.write_all(content.as_bytes())?;
2079            f.flush()?;
2080        } else {
2081            fs::create_dir_all(path.parent().unwrap())?;
2082            fs::write(path, content)?;
2083        }
2084    }
2085
2086    Ok(())
2087}
2088
2089pub fn expand(
2090    cfg_override: &ConfigOverride,
2091    program_name: Option<String>,
2092    stdout: bool,
2093    cargo_args: &[String],
2094) -> Result<()> {
2095    // Change to the workspace member directory, if needed.
2096    if let Some(program_name) = program_name.as_ref() {
2097        cd_member(cfg_override, program_name)?;
2098    }
2099
2100    let workspace_cfg = Config::discover(cfg_override)?
2101        .ok_or_else(|| anyhow!("The 'anchor expand' command requires an Anchor workspace."))?;
2102    let cfg_parent = workspace_cfg.path().parent().expect("Invalid Anchor.toml");
2103    let cargo = Manifest::discover()?;
2104
2105    let expansions_path = cfg_parent.join(".anchor").join("expanded-macros");
2106    fs::create_dir_all(&expansions_path)?;
2107
2108    match cargo {
2109        // No Cargo.toml found, expand entire workspace
2110        None => expand_all(&workspace_cfg, expansions_path, stdout, cargo_args),
2111        // Cargo.toml is at root of workspace, expand entire workspace
2112        Some(cargo) if cargo.path().parent() == workspace_cfg.path().parent() => {
2113            expand_all(&workspace_cfg, expansions_path, stdout, cargo_args)
2114        }
2115        // Reaching this arm means Cargo.toml belongs to a single package. Expand it.
2116        Some(cargo) => expand_program(
2117            // If we found Cargo.toml, it must be in a directory so unwrap is safe
2118            cargo.path().parent().unwrap().to_path_buf(),
2119            expansions_path,
2120            stdout,
2121            cargo_args,
2122        ),
2123    }
2124}
2125
2126fn expand_all(
2127    workspace_cfg: &WithPath<Config>,
2128    expansions_path: PathBuf,
2129    stdout: bool,
2130    cargo_args: &[String],
2131) -> Result<()> {
2132    let cur_dir = std::env::current_dir()?;
2133    for p in workspace_cfg.get_program_list()? {
2134        expand_program(p, expansions_path.clone(), stdout, cargo_args)?;
2135    }
2136    std::env::set_current_dir(cur_dir)?;
2137    Ok(())
2138}
2139
2140fn expand_program(
2141    program_path: PathBuf,
2142    expansions_path: PathBuf,
2143    stdout: bool,
2144    cargo_args: &[String],
2145) -> Result<()> {
2146    let cargo = Manifest::from_path(program_path.join("Cargo.toml"))
2147        .map_err(|_| anyhow!("Could not find Cargo.toml for program"))?;
2148    let package_name = &cargo
2149        .package
2150        .as_ref()
2151        .ok_or_else(|| anyhow!("Cargo config is missing a package"))?
2152        .name;
2153
2154    let mut cmd = std::process::Command::new("cargo");
2155    cmd.arg("expand")
2156        .arg("--target-dir")
2157        .arg(expansions_path.join("expand-target"))
2158        .arg("--package")
2159        .arg(package_name)
2160        .args(cargo_args);
2161
2162    let handle_err = |err| anyhow!("Failed to run `cargo expand`: {err}");
2163    let exit_on_err = |exit_status: ExitStatus| {
2164        if !exit_status.success() {
2165            eprintln!("'anchor expand' failed. Perhaps you have not installed 'cargo-expand'? https://github.com/dtolnay/cargo-expand#installation");
2166            std::process::exit(exit_status.code().unwrap_or(1));
2167        }
2168    };
2169
2170    if stdout {
2171        let status = cmd.status().map_err(handle_err)?;
2172        exit_on_err(status);
2173    } else {
2174        let output = cmd.stderr(Stdio::inherit()).output().map_err(handle_err)?;
2175        exit_on_err(output.status);
2176
2177        let program_expansions_path = expansions_path.join(package_name);
2178        fs::create_dir_all(&program_expansions_path)?;
2179
2180        let version = cargo.version();
2181        let time = chrono::Utc::now().to_string().replace(' ', "_");
2182        let file_path = program_expansions_path.join(format!("{package_name}-{version}-{time}.rs"));
2183        fs::write(&file_path, &output.stdout)?;
2184
2185        println!(
2186            "Expanded {} into file {}\n",
2187            package_name,
2188            file_path.to_string_lossy()
2189        );
2190    }
2191
2192    Ok(())
2193}
2194
2195#[allow(clippy::too_many_arguments)]
2196pub fn build(
2197    cfg_override: &ConfigOverride,
2198    no_idl: bool,
2199    idl: Option<String>,
2200    idl_ts: Option<String>,
2201    verifiable: bool,
2202    skip_lint: bool,
2203    ignore_keys: bool,
2204    program_name: Option<String>,
2205    solana_version: Option<String>,
2206    docker_image: Option<String>,
2207    bootstrap: BootstrapMode,
2208    build_sbf_options: BuildSbfOptions,
2209    stdout: Option<File>, // Used for the package registry server.
2210    stderr: Option<File>, // Used for the package registry server.
2211    env_vars: Vec<String>,
2212    cargo_args: Vec<String>,
2213    no_docs: bool,
2214) -> Result<()> {
2215    // Change to the workspace member directory, if needed.
2216    if let Some(program_name) = program_name.as_ref() {
2217        cd_member(cfg_override, program_name)?;
2218    }
2219    let cfg = Config::discover(cfg_override)?
2220        .ok_or_else(|| anyhow!("The 'anchor build' command requires an Anchor workspace."))?;
2221    let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
2222
2223    // Require overflow checks
2224    let workspace_cargo_toml_path = cfg_parent.join("Cargo.toml");
2225    if workspace_cargo_toml_path.exists() {
2226        check_overflow(workspace_cargo_toml_path)?;
2227    }
2228
2229    // Check whether there is a mismatch between CLI and crate/package versions
2230    check_anchor_version(&cfg).ok();
2231    check_deps(&cfg).ok();
2232
2233    // Check for program ID mismatches before building (skip if --ignore-keys is used), Always skipped in anchor test
2234    if !ignore_keys {
2235        // FIXME: Consider making this a hard error in `deploy` instead
2236        if let ProgramIdComparison::Mismatch {
2237            lib_name,
2238            actual_id,
2239            declared_id,
2240        } = check_program_id_mismatch(&cfg, program_name.clone())?
2241        {
2242            eprintln!(
2243                "Program ID mismatch detected for program '{lib_name}':\n  Keypair file has: \
2244                 {actual_id}\n  Source code has:  {declared_id}\n\nPlease run 'anchor keys sync' \
2245                 to update the program ID in your source code or use the '--ignore-keys' flag to \
2246                 skip this check.",
2247            );
2248        }
2249    }
2250
2251    let idl_out = match idl {
2252        Some(idl) => Some(PathBuf::from(idl)),
2253        None => Some(target_dir()?.join("idl")),
2254    };
2255    fs::create_dir_all(idl_out.as_ref().unwrap())?;
2256
2257    let idl_ts_out = match idl_ts {
2258        Some(idl_ts) => Some(PathBuf::from(idl_ts)),
2259        None => Some(target_dir()?.join("types")),
2260    };
2261    fs::create_dir_all(idl_ts_out.as_ref().unwrap())?;
2262
2263    if !cfg.workspace.idls.is_empty() {
2264        fs::create_dir_all(cfg_parent.join(&cfg.workspace.idls))?;
2265    };
2266    if !cfg.workspace.types.is_empty() {
2267        fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
2268    };
2269
2270    cfg.run_hooks(HookType::PreBuild)?;
2271
2272    let cargo = Manifest::discover()?;
2273    let build_config = BuildConfig {
2274        verifiable,
2275        solana_version: solana_version.or_else(|| cfg.toolchain.solana_version.clone()),
2276        docker_image: docker_image.unwrap_or_else(|| cfg.docker()),
2277        bootstrap,
2278    };
2279    let built_idl_paths = match cargo {
2280        // No Cargo.toml so build the entire workspace.
2281        None => build_all(
2282            &cfg,
2283            cfg.path(),
2284            no_idl,
2285            idl_out.clone(),
2286            idl_ts_out.clone(),
2287            &build_config,
2288            &build_sbf_options,
2289            stdout,
2290            stderr,
2291            env_vars,
2292            cargo_args,
2293            skip_lint,
2294            no_docs,
2295        )?,
2296        // If the Cargo.toml is at the root, build the entire workspace.
2297        Some(cargo) if cargo.path().parent() == cfg.path().parent() => build_all(
2298            &cfg,
2299            cfg.path(),
2300            no_idl,
2301            idl_out.clone(),
2302            idl_ts_out.clone(),
2303            &build_config,
2304            &build_sbf_options,
2305            stdout,
2306            stderr,
2307            env_vars,
2308            cargo_args,
2309            skip_lint,
2310            no_docs,
2311        )?,
2312        // Cargo.toml represents a single package. Build it.
2313        Some(cargo) => build_cwd(
2314            &cfg,
2315            cargo.path().to_path_buf(),
2316            no_idl,
2317            idl_out.clone(),
2318            idl_ts_out.clone(),
2319            &build_config,
2320            &build_sbf_options,
2321            stdout,
2322            stderr,
2323            env_vars,
2324            cargo_args,
2325            skip_lint,
2326            no_docs,
2327        )?,
2328    };
2329    cfg.run_hooks(HookType::PostBuild)?;
2330
2331    if cfg.clients.auto && !no_idl {
2332        // Only pass IDLs produced by this build. The output directory can
2333        // contain stale JSON from earlier full builds, especially after
2334        // `anchor build --program-name ...` from inside a program crate.
2335        codama::auto_generate_for_workspace(&cfg.clients, cfg_parent, &built_idl_paths)?;
2336    }
2337
2338    set_workspace_dir_or_exit();
2339
2340    Ok(())
2341}
2342
2343#[allow(clippy::too_many_arguments)]
2344fn build_all(
2345    cfg: &WithPath<Config>,
2346    cfg_path: &Path,
2347    no_idl: bool,
2348    idl_out: Option<PathBuf>,
2349    idl_ts_out: Option<PathBuf>,
2350    build_config: &BuildConfig,
2351    build_sbf_options: &BuildSbfOptions,
2352    stdout: Option<File>, // Used for the package registry server.
2353    stderr: Option<File>, // Used for the package registry server.
2354    env_vars: Vec<String>,
2355    cargo_args: Vec<String>,
2356    skip_lint: bool,
2357    no_docs: bool,
2358) -> Result<Vec<PathBuf>> {
2359    let cur_dir = std::env::current_dir()?;
2360    let r = (|| -> Result<Vec<PathBuf>> {
2361        match cfg_path.parent() {
2362            None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())),
2363            Some(_parent) => {
2364                let mut idl_paths = Vec::new();
2365                for p in get_metadata_ordered_program_list(cfg)? {
2366                    idl_paths.extend(build_cwd(
2367                        cfg,
2368                        p.join("Cargo.toml"),
2369                        no_idl,
2370                        idl_out.clone(),
2371                        idl_ts_out.clone(),
2372                        build_config,
2373                        build_sbf_options,
2374                        stdout.as_ref().map(|f| f.try_clone()).transpose()?,
2375                        stderr.as_ref().map(|f| f.try_clone()).transpose()?,
2376                        env_vars.clone(),
2377                        cargo_args.clone(),
2378                        skip_lint,
2379                        no_docs,
2380                    )?);
2381                }
2382                Ok(idl_paths)
2383            }
2384        }
2385    })();
2386    std::env::set_current_dir(cur_dir)?;
2387    r
2388}
2389
2390fn get_metadata_ordered_program_list(cfg: &WithPath<Config>) -> Result<Vec<PathBuf>> {
2391    let programs = cfg.get_program_list()?;
2392    let ordered = order_programs_by_metadata(cfg, &programs);
2393    Ok(ordered.unwrap_or(programs))
2394}
2395
2396fn order_programs_by_metadata(
2397    cfg: &WithPath<Config>,
2398    programs: &[PathBuf],
2399) -> Result<Vec<PathBuf>> {
2400    let workspace_dir = cfg
2401        .path()
2402        .parent()
2403        .ok_or_else(|| anyhow!("Invalid Anchor.toml at {}", cfg.path().display()))?;
2404    let metadata = MetadataCommand::new()
2405        .current_dir(workspace_dir)
2406        .exec()
2407        .context("Failed to run `cargo metadata`")?;
2408
2409    let mut package_dirs = HashMap::new();
2410    for (idx, package) in metadata.packages.iter().enumerate() {
2411        if package.source.is_some() {
2412            continue;
2413        }
2414        let manifest_path = package.manifest_path.clone().into_std_path_buf();
2415        if let Some(package_dir) = manifest_path.parent() {
2416            if let Ok(package_dir) = package_dir.canonicalize() {
2417                package_dirs.insert(package_dir, idx);
2418            }
2419        }
2420    }
2421
2422    let program_indices = programs
2423        .iter()
2424        .filter_map(|program| package_dirs.get(program).copied())
2425        .collect::<HashSet<_>>();
2426    if program_indices.len() != programs.len() {
2427        bail!("Failed to match all Anchor programs in `cargo metadata`");
2428    }
2429
2430    let mut local_deps = vec![Vec::new(); metadata.packages.len()];
2431    for (idx, package) in metadata.packages.iter().enumerate() {
2432        for dep in &package.dependencies {
2433            if dep.kind == DependencyKind::Development {
2434                continue;
2435            }
2436            let Some(dep_path) = dep.path.as_ref() else {
2437                continue;
2438            };
2439            if let Ok(dep_path) = dep_path.clone().into_std_path_buf().canonicalize() {
2440                if let Some(dep_idx) = package_dirs.get(&dep_path) {
2441                    local_deps[idx].push(*dep_idx);
2442                }
2443            }
2444        }
2445    }
2446
2447    let mut program_closures = HashMap::new();
2448    for idx in &program_indices {
2449        program_closures.insert(*idx, local_dependency_closure(*idx, &local_deps));
2450    }
2451
2452    let original_order_by_package = programs
2453        .iter()
2454        .enumerate()
2455        .map(|(idx, program)| (package_dirs[program], idx))
2456        .collect::<HashMap<_, _>>();
2457    let program_by_index = programs
2458        .iter()
2459        .map(|program| (package_dirs[program], program.clone()))
2460        .collect::<HashMap<_, _>>();
2461    let ordered = order_program_indices_by_dependency_cache_heuristic(
2462        &program_indices,
2463        &program_closures,
2464        &original_order_by_package,
2465    )
2466    .into_iter()
2467    .map(|idx| program_by_index[&idx].clone())
2468    .collect();
2469
2470    Ok(ordered)
2471}
2472
2473fn order_program_indices_by_dependency_cache_heuristic(
2474    program_indices: &HashSet<usize>,
2475    program_closures: &HashMap<usize, HashSet<usize>>,
2476    original_order: &HashMap<usize, usize>,
2477) -> Vec<usize> {
2478    let mut reverse_dependents = HashMap::new();
2479    for idx in program_indices {
2480        reverse_dependents.insert(*idx, 0usize);
2481    }
2482    for (program_idx, deps) in program_closures {
2483        for dep_idx in deps {
2484            if program_indices.contains(dep_idx) && dep_idx != program_idx {
2485                *reverse_dependents.entry(*dep_idx).or_default() += 1;
2486            }
2487        }
2488    }
2489
2490    let mut ordered = program_indices.iter().copied().collect::<Vec<_>>();
2491    ordered.sort_by(|a, b| {
2492        let a_deps = &program_closures[a];
2493        let b_deps = &program_closures[b];
2494        let a_program_deps = a_deps
2495            .iter()
2496            .filter(|idx| program_indices.contains(idx))
2497            .count();
2498        let b_program_deps = b_deps
2499            .iter()
2500            .filter(|idx| program_indices.contains(idx))
2501            .count();
2502        let a_reverse = reverse_dependents[a];
2503        let b_reverse = reverse_dependents[b];
2504        let a_isolated = a_program_deps == 0 && a_reverse == 0;
2505        let b_isolated = b_program_deps == 0 && b_reverse == 0;
2506
2507        b_isolated
2508            .cmp(&a_isolated)
2509            .then_with(|| b_program_deps.cmp(&a_program_deps))
2510            .then_with(|| b_deps.len().cmp(&a_deps.len()))
2511            .then_with(|| a_reverse.cmp(&b_reverse))
2512            .then_with(|| original_order[a].cmp(&original_order[b]))
2513    });
2514
2515    ordered
2516}
2517
2518fn local_dependency_closure(start: usize, deps: &[Vec<usize>]) -> HashSet<usize> {
2519    let mut seen = HashSet::new();
2520    let mut stack = deps[start].clone();
2521
2522    while let Some(idx) = stack.pop() {
2523        if seen.insert(idx) {
2524            stack.extend(deps[idx].iter().copied());
2525        }
2526    }
2527
2528    seen
2529}
2530
2531// Runs the build command outside of a workspace.
2532#[allow(clippy::too_many_arguments)]
2533fn build_cwd(
2534    cfg: &WithPath<Config>,
2535    cargo_toml: PathBuf,
2536    no_idl: bool,
2537    idl_out: Option<PathBuf>,
2538    idl_ts_out: Option<PathBuf>,
2539    build_config: &BuildConfig,
2540    build_sbf_options: &BuildSbfOptions,
2541    stdout: Option<File>,
2542    stderr: Option<File>,
2543    env_vars: Vec<String>,
2544    cargo_args: Vec<String>,
2545    skip_lint: bool,
2546    no_docs: bool,
2547) -> Result<Vec<PathBuf>> {
2548    match cargo_toml.parent() {
2549        None => return Err(anyhow!("Unable to find parent")),
2550        Some(p) => std::env::set_current_dir(p)?,
2551    };
2552    match build_config.verifiable {
2553        false => _build_cwd(
2554            cfg,
2555            no_idl,
2556            idl_out,
2557            idl_ts_out,
2558            skip_lint,
2559            no_docs,
2560            build_sbf_options,
2561            cargo_args,
2562        ),
2563        true => build_cwd_verifiable(
2564            cfg,
2565            cargo_toml,
2566            build_config,
2567            build_sbf_options,
2568            stdout,
2569            stderr,
2570            skip_lint,
2571            env_vars,
2572            cargo_args,
2573            no_docs,
2574        ),
2575    }
2576}
2577
2578// Builds an anchor program in a docker image and copies the build artifacts
2579// into the `target/` directory.
2580#[allow(clippy::too_many_arguments)]
2581fn build_cwd_verifiable(
2582    cfg: &WithPath<Config>,
2583    cargo_toml: PathBuf,
2584    build_config: &BuildConfig,
2585    build_sbf_options: &BuildSbfOptions,
2586    stdout: Option<File>,
2587    stderr: Option<File>,
2588    skip_lint: bool,
2589    env_vars: Vec<String>,
2590    cargo_args: Vec<String>,
2591    no_docs: bool,
2592) -> Result<Vec<PathBuf>> {
2593    // Create output dirs.
2594    let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
2595    let target_dir = target_dir()?;
2596    fs::create_dir_all(target_dir.join("verifiable"))?;
2597    fs::create_dir_all(target_dir.join("idl"))?;
2598    fs::create_dir_all(target_dir.join("types"))?;
2599    if !&cfg.workspace.idls.is_empty() {
2600        fs::create_dir_all(workspace_dir.join(&cfg.workspace.idls))?;
2601    }
2602    if !&cfg.workspace.types.is_empty() {
2603        fs::create_dir_all(workspace_dir.join(&cfg.workspace.types))?;
2604    }
2605
2606    let container_name = "anchor-program";
2607
2608    // Build the binary in docker.
2609    let result = docker_build(
2610        cfg,
2611        container_name,
2612        cargo_toml,
2613        build_config,
2614        build_sbf_options,
2615        stdout,
2616        stderr,
2617        env_vars,
2618        cargo_args.clone(),
2619    );
2620
2621    match result {
2622        Err(e) => {
2623            eprintln!("Error during Docker build: {e:?}");
2624            Err(e)
2625        }
2626        Ok(_) => {
2627            // Build the idl.
2628            println!("Extracting the IDL");
2629            let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
2630            // Write out the JSON file.
2631            println!("Writing the IDL file");
2632            let out_file = target_dir
2633                .join("idl")
2634                .join(&idl.metadata.name)
2635                .with_extension("json");
2636            write_idl(&idl, OutFile::File(out_file.clone()))?;
2637
2638            if !&cfg.workspace.idls.is_empty() {
2639                write_idl(
2640                    &idl,
2641                    OutFile::File(
2642                        workspace_dir
2643                            .join(&cfg.workspace.idls)
2644                            .join(&idl.metadata.name)
2645                            .with_extension("json"),
2646                    ),
2647                )?;
2648            }
2649
2650            // Write out the TypeScript type.
2651            println!("Writing the .ts file");
2652            let ts_file = target_dir
2653                .join("types")
2654                .join(&idl.metadata.name)
2655                .with_extension("ts");
2656            fs::write(&ts_file, idl_ts(&idl)?)?;
2657
2658            // Generate error constants file if errors exist
2659            let types_dir = if cfg.workspace.types.is_empty() {
2660                None
2661            } else {
2662                Some(workspace_dir.join(&cfg.workspace.types))
2663            };
2664            write_error_constants_file(&idl, &target_dir.join("types"), types_dir)?;
2665
2666            // Copy out the TypeScript type.
2667            if !&cfg.workspace.types.is_empty() {
2668                fs::copy(
2669                    ts_file,
2670                    workspace_dir
2671                        .join(&cfg.workspace.types)
2672                        .join(idl.metadata.name)
2673                        .with_extension("ts"),
2674                )?;
2675            }
2676
2677            println!("Build success");
2678            Ok(vec![out_file])
2679        }
2680    }
2681}
2682
2683#[allow(clippy::too_many_arguments)]
2684fn docker_build(
2685    cfg: &WithPath<Config>,
2686    container_name: &str,
2687    cargo_toml: PathBuf,
2688    build_config: &BuildConfig,
2689    build_sbf_options: &BuildSbfOptions,
2690    stdout: Option<File>,
2691    stderr: Option<File>,
2692    env_vars: Vec<String>,
2693    cargo_args: Vec<String>,
2694) -> Result<()> {
2695    let binary_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
2696
2697    // Docker vars.
2698    let workdir = Path::new("/workdir");
2699    let volume_mount = format!(
2700        "{}:{}",
2701        cfg.path().parent().unwrap().canonicalize()?.display(),
2702        workdir.to_str().unwrap(),
2703    );
2704    println!("Using image {:?}", build_config.docker_image);
2705
2706    // Start the docker image running detached in the background.
2707    let target_dir = workdir.join("docker-target");
2708    println!("Run docker image");
2709    let exit = std::process::Command::new("docker")
2710        .args([
2711            "run",
2712            "-it",
2713            "-d",
2714            "--name",
2715            container_name,
2716            "--env",
2717            &format!(
2718                "CARGO_TARGET_DIR={}",
2719                target_dir.as_path().to_str().unwrap()
2720            ),
2721            "-v",
2722            &volume_mount,
2723            "-w",
2724            workdir.to_str().unwrap(),
2725            &build_config.docker_image,
2726            "bash",
2727        ])
2728        .stdout(Stdio::inherit())
2729        .stderr(Stdio::inherit())
2730        .output()
2731        .map_err(|e| anyhow::format_err!("Docker build failed: {}", e))?;
2732    if !exit.status.success() {
2733        return Err(anyhow!("Failed to build program"));
2734    }
2735
2736    let result = docker_prep(container_name, build_config).and_then(|_| {
2737        let cfg_parent = cfg.path().parent().unwrap();
2738        docker_build_bpf(
2739            container_name,
2740            cargo_toml.as_path(),
2741            cfg_parent,
2742            target_dir.as_path(),
2743            binary_name,
2744            build_sbf_options,
2745            stdout,
2746            stderr,
2747            env_vars,
2748            cargo_args,
2749        )
2750    });
2751
2752    // Cleanup regardless of errors
2753    docker_cleanup(container_name, target_dir.as_path())?;
2754
2755    // Done.
2756    result
2757}
2758
2759fn docker_prep(container_name: &str, build_config: &BuildConfig) -> Result<()> {
2760    // Set the solana version in the container, if given. Otherwise use the
2761    // default.
2762    match build_config.bootstrap {
2763        BootstrapMode::Debian => {
2764            // Install build requirements
2765            docker_exec(container_name, &["apt", "update"])?;
2766            docker_exec(
2767                container_name,
2768                &["apt", "install", "-y", "curl", "build-essential"],
2769            )?;
2770
2771            // Install Rust
2772            docker_exec(
2773                container_name,
2774                &["curl", "https://sh.rustup.rs", "-sfo", "rustup.sh"],
2775            )?;
2776            docker_exec(container_name, &["sh", "rustup.sh", "-y"])?;
2777            docker_exec(container_name, &["rm", "-f", "rustup.sh"])?;
2778        }
2779        BootstrapMode::None => {}
2780    }
2781
2782    if let Some(solana_version) = &build_config.solana_version {
2783        println!("Using solana version: {solana_version}");
2784
2785        // Install Solana CLI
2786        docker_exec(
2787            container_name,
2788            &[
2789                "curl",
2790                "-sSfL",
2791                &format!("https://release.anza.xyz/v{solana_version}/install",),
2792                "-o",
2793                "solana_installer.sh",
2794            ],
2795        )?;
2796        docker_exec(container_name, &["sh", "solana_installer.sh"])?;
2797        docker_exec(container_name, &["rm", "-f", "solana_installer.sh"])?;
2798    }
2799    Ok(())
2800}
2801
2802#[allow(clippy::too_many_arguments)]
2803fn docker_build_bpf(
2804    container_name: &str,
2805    cargo_toml: &Path,
2806    cfg_parent: &Path,
2807    target_dir: &Path,
2808    binary_name: String,
2809    build_sbf_options: &BuildSbfOptions,
2810    stdout: Option<File>,
2811    stderr: Option<File>,
2812    env_vars: Vec<String>,
2813    cargo_args: Vec<String>,
2814) -> Result<()> {
2815    let manifest_path =
2816        pathdiff::diff_paths(cargo_toml.canonicalize()?, cfg_parent.canonicalize()?)
2817            .ok_or_else(|| anyhow!("Unable to diff paths"))?;
2818    println!(
2819        "Building {} manifest: {:?}",
2820        binary_name,
2821        manifest_path.display()
2822    );
2823
2824    // Execute the build.
2825    let exit = std::process::Command::new("docker")
2826        .args([
2827            "exec",
2828            "--env",
2829            "PATH=/root/.local/share/solana/install/active_release/bin:/root/.cargo/bin:/usr/\
2830             local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
2831        ])
2832        .args(
2833            env_vars
2834                .iter()
2835                .map(|x| ["--env", x.as_str()])
2836                .collect::<Vec<[&str; 2]>>()
2837                .concat(),
2838        )
2839        .args([container_name, "cargo"])
2840        .args(build_sbf_base_args(build_sbf_options))
2841        .args(["--manifest-path", &manifest_path.display().to_string()])
2842        .args(cargo_args)
2843        .stdout(match stdout {
2844            None => Stdio::inherit(),
2845            Some(f) => f.into(),
2846        })
2847        .stderr(match stderr {
2848            None => Stdio::inherit(),
2849            Some(f) => f.into(),
2850        })
2851        .output()
2852        .map_err(|e| anyhow::format_err!("Docker build failed: {}", e))?;
2853    if !exit.status.success() {
2854        return Err(anyhow!("Failed to build program"));
2855    }
2856
2857    // Copy the binary out of the docker image.
2858    println!("Copying out the build artifacts");
2859    let out_file = crate::target_dir()?
2860        .join("verifiable")
2861        .join(&binary_name)
2862        .with_extension("so")
2863        .display()
2864        .to_string();
2865
2866    // This requires the target directory of any built program to be located at
2867    // the root of the workspace.
2868    let mut bin_path = target_dir.join("deploy");
2869    bin_path.push(format!("{binary_name}.so"));
2870    let bin_artifact = format!(
2871        "{}:{}",
2872        container_name,
2873        bin_path.as_path().to_str().unwrap()
2874    );
2875    let exit = std::process::Command::new("docker")
2876        .args(["cp", &bin_artifact, &out_file])
2877        .stdout(Stdio::inherit())
2878        .stderr(Stdio::inherit())
2879        .output()
2880        .map_err(|e| anyhow::format_err!("{}", e))?;
2881    if !exit.status.success() {
2882        Err(anyhow!(
2883            "Failed to copy binary out of docker. Is the target directory set correctly?"
2884        ))
2885    } else {
2886        Ok(())
2887    }
2888}
2889
2890fn docker_cleanup(container_name: &str, target_dir: &Path) -> Result<()> {
2891    // Wipe the generated docker-target dir.
2892    println!("Cleaning up the docker target directory");
2893    docker_exec(container_name, &["rm", "-rf", target_dir.to_str().unwrap()])?;
2894
2895    // Remove the docker image.
2896    println!("Removing the docker container");
2897    let exit = std::process::Command::new("docker")
2898        .args(["rm", "-f", container_name])
2899        .stdout(Stdio::inherit())
2900        .stderr(Stdio::inherit())
2901        .output()
2902        .map_err(|e| anyhow::format_err!("{}", e))?;
2903    if !exit.status.success() {
2904        println!("Unable to remove the docker container");
2905        std::process::exit(exit.status.code().unwrap_or(1));
2906    }
2907    Ok(())
2908}
2909
2910fn docker_exec(container_name: &str, args: &[&str]) -> Result<()> {
2911    let exit = std::process::Command::new("docker")
2912        .args([&["exec", container_name], args].concat())
2913        .stdout(Stdio::inherit())
2914        .stderr(Stdio::inherit())
2915        .output()
2916        .map_err(|e| anyhow!("Failed to run command \"{:?}\": {:?}", args, e))?;
2917    if !exit.status.success() {
2918        Err(anyhow!("Failed to run command: {:?}", args))
2919    } else {
2920        Ok(())
2921    }
2922}
2923
2924#[allow(clippy::too_many_arguments)]
2925fn _build_cwd(
2926    cfg: &WithPath<Config>,
2927    no_idl: bool,
2928    idl_out: Option<PathBuf>,
2929    idl_ts_out: Option<PathBuf>,
2930    skip_lint: bool,
2931    no_docs: bool,
2932    build_sbf_options: &BuildSbfOptions,
2933    cargo_args: Vec<String>,
2934) -> Result<Vec<PathBuf>> {
2935    let build_args = build_sbf_args(build_sbf_options, &cargo_args);
2936    let exit = std::process::Command::new("cargo")
2937        .args(&build_args)
2938        .stdout(Stdio::inherit())
2939        .stderr(Stdio::inherit())
2940        .output()
2941        .map_err(|e| anyhow::format_err!("{}", e))?;
2942    if !exit.status.success() {
2943        std::process::exit(exit.status.code().unwrap_or(1));
2944    }
2945
2946    // Generate IDL
2947    if !no_idl {
2948        let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
2949        let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
2950
2951        // JSON out path.
2952        let out = match idl_out {
2953            None => PathBuf::from(".")
2954                .join(&idl.metadata.name)
2955                .with_extension("json"),
2956            Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("json")),
2957        };
2958        // TS out path.
2959        let ts_out = match idl_ts_out {
2960            None => PathBuf::from(".")
2961                .join(&idl.metadata.name)
2962                .with_extension("ts"),
2963            Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("ts")),
2964        };
2965
2966        // Write out the JSON file.
2967        write_idl(&idl, OutFile::File(out.clone()))?;
2968        if !&cfg.workspace.idls.is_empty() {
2969            write_idl(
2970                &idl,
2971                OutFile::File(
2972                    cfg_parent
2973                        .join(&cfg.workspace.idls)
2974                        .join(&idl.metadata.name)
2975                        .with_extension("json"),
2976                ),
2977            )?;
2978        }
2979        // Write out the TypeScript type.
2980        fs::write(&ts_out, idl_ts(&idl)?)?;
2981
2982        // Generate error constants file if errors exist
2983        let types_dir = if cfg.workspace.types.is_empty() {
2984            None
2985        } else {
2986            Some(cfg_parent.join(&cfg.workspace.types))
2987        };
2988        write_error_constants_file(&idl, ts_out.parent().unwrap(), types_dir)?;
2989
2990        // Copy out the TypeScript type.
2991        if !&cfg.workspace.types.is_empty() {
2992            fs::copy(
2993                &ts_out,
2994                cfg_parent
2995                    .join(&cfg.workspace.types)
2996                    .join(&idl.metadata.name)
2997                    .with_extension("ts"),
2998            )?;
2999        }
3000        Ok(vec![out])
3001    } else {
3002        Ok(Vec::new())
3003    }
3004}
3005
3006/// Subcommand to be passed to cargo.
3007const BUILD_SUBCOMMAND: &str = "build-sbf";
3008
3009#[derive(Clone, Debug, PartialEq, Eq)]
3010pub struct BuildSbfOptions {
3011    tools_version: String,
3012    arch: String,
3013}
3014
3015impl BuildSbfOptions {
3016    fn new(tools_version: String, arch: String) -> Self {
3017        Self {
3018            tools_version,
3019            arch,
3020        }
3021    }
3022
3023    fn from_build_command(tools_version: String, arch: String) -> Self {
3024        Self::new(tools_version, arch)
3025    }
3026}
3027
3028impl Default for BuildSbfOptions {
3029    fn default() -> Self {
3030        Self::new(DEFAULT_TOOLS_VERSION.to_owned(), default_build_arch())
3031    }
3032}
3033
3034fn default_build_arch() -> String {
3035    std::env::var(BUILD_ARCH_ENV).unwrap_or_else(|_| DEFAULT_BUILD_ARCH.to_owned())
3036}
3037
3038fn validator_type_from_env() -> Result<Option<ValidatorType>> {
3039    let Ok(value) = std::env::var("ANCHOR_TEST_VALIDATOR") else {
3040        return Ok(None);
3041    };
3042    match value.to_ascii_lowercase().as_str() {
3043        "surfpool" => Ok(Some(ValidatorType::Surfpool)),
3044        "legacy" => Ok(Some(ValidatorType::Legacy)),
3045        _ => Err(anyhow!(
3046            "invalid ANCHOR_TEST_VALIDATOR value `{value}`; expected `surfpool` or `legacy`"
3047        )),
3048    }
3049}
3050
3051fn build_sbf_base_args(build_sbf_options: &BuildSbfOptions) -> Vec<String> {
3052    let mut args = vec![BUILD_SUBCOMMAND.to_owned()];
3053    args.push("--tools-version".to_owned());
3054    args.push(build_sbf_options.tools_version.clone());
3055    args.push("--arch".to_owned());
3056    args.push(build_sbf_options.arch.clone());
3057    args
3058}
3059
3060fn build_sbf_args(build_sbf_options: &BuildSbfOptions, extra_args: &[String]) -> Vec<String> {
3061    let mut args = build_sbf_base_args(build_sbf_options);
3062    args.extend(extra_args.iter().cloned());
3063    args
3064}
3065
3066/// Run the configured SBF build command.
3067pub fn cargo_build_sbf(
3068    cwd: Option<&Path>,
3069    build_sbf_options: &BuildSbfOptions,
3070    extra_args: &[String],
3071) -> Result<()> {
3072    let mut cmd = std::process::Command::new("cargo");
3073    if let Some(d) = cwd {
3074        cmd.current_dir(d);
3075    }
3076    let args = build_sbf_args(build_sbf_options, extra_args);
3077    let status = cmd
3078        .args(&args)
3079        .stdout(Stdio::inherit())
3080        .stderr(Stdio::inherit())
3081        .status()
3082        .context("running cargo build-sbf")?;
3083    if !status.success() {
3084        return Err(anyhow!(
3085            "`cargo {}` failed with status {status}",
3086            args.join(" ")
3087        ));
3088    }
3089    Ok(())
3090}
3091
3092pub fn verify(
3093    program_id: Pubkey,
3094    repo_url: Option<String>,
3095    commit_hash: Option<String>,
3096    current_dir: bool,
3097    program_name: Option<String>,
3098    args: Vec<String>,
3099) -> Result<()> {
3100    let mut command_args = Vec::new();
3101
3102    match (current_dir, repo_url) {
3103        (true, _) => {
3104            let current_path = std::env::current_dir()?
3105                .to_str()
3106                .ok_or_else(|| anyhow!("Invalid current directory path"))?
3107                .to_owned();
3108            command_args.push(current_path);
3109            command_args.push("--current-dir".into());
3110        }
3111        (false, Some(url)) => {
3112            command_args.push(url);
3113        }
3114        (false, None) => {
3115            return Err(anyhow!(
3116                "You must provide either --repo-url or --current-dir"
3117            ));
3118        }
3119    }
3120
3121    if let Some(commit) = commit_hash {
3122        command_args.push("--commit-hash".into());
3123        command_args.push(commit);
3124    }
3125
3126    if let Some(name) = program_name {
3127        command_args.push("--library-name".into());
3128        command_args.push(name);
3129    }
3130
3131    command_args.push("--program-id".into());
3132    command_args.push(program_id.to_string());
3133
3134    command_args.extend(args);
3135
3136    println!("Verifying program {program_id}");
3137    let verify_path = AVM_HOME.join("bin").join("solana-verify");
3138    if !verify_path.exists() {
3139        install_with_avm(env!("CARGO_PKG_VERSION"), true)
3140            .context("installing Anchor with solana-verify")?;
3141    }
3142
3143    let status = std::process::Command::new(verify_path)
3144        .arg("verify-from-repo")
3145        .args(&command_args)
3146        .stdout(std::process::Stdio::inherit())
3147        .stderr(std::process::Stdio::inherit())
3148        .status()
3149        .with_context(|| "Failed to run `solana-verify`")?;
3150
3151    if !status.success() {
3152        return Err(anyhow!("Failed to verify program"));
3153    }
3154
3155    Ok(())
3156}
3157
3158fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> {
3159    // Change directories to the given `program_name`, using either Anchor or Cargo workspace
3160    let programs = program::get_programs_from_workspace(cfg_override, None)?;
3161
3162    for program in programs {
3163        let cargo_toml = program.path.join("Cargo.toml");
3164        if !cargo_toml.exists() {
3165            return Err(anyhow!(
3166                "Did not find Cargo.toml at the path: {}",
3167                program.path.display()
3168            ));
3169        }
3170
3171        let manifest = Manifest::from_path(&cargo_toml)?;
3172        let pkg_name = manifest.package().name();
3173        if program_name == pkg_name || program_name == program.lib_name {
3174            std::env::set_current_dir(&program.path)?;
3175            return Ok(());
3176        }
3177    }
3178
3179    Err(anyhow!("{} is not part of the workspace", program_name,))
3180}
3181
3182fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
3183    match subcmd {
3184        IdlCommand::Init {
3185            program_id,
3186            filepath,
3187            priority_fee,
3188            non_canonical,
3189            #[cfg(feature = "idl-localnet-testing")]
3190            allow_localnet,
3191        } => {
3192            #[cfg(feature = "idl-localnet-testing")]
3193            let allow_localnet = allow_localnet;
3194            #[cfg(not(feature = "idl-localnet-testing"))]
3195            let allow_localnet = false;
3196            idl_init(
3197                program_id,
3198                cfg_override,
3199                filepath,
3200                priority_fee,
3201                non_canonical,
3202                allow_localnet,
3203            )
3204        }
3205        IdlCommand::Upgrade {
3206            program_id,
3207            filepath,
3208            priority_fee,
3209            #[cfg(feature = "idl-localnet-testing")]
3210            allow_localnet,
3211        } => {
3212            #[cfg(feature = "idl-localnet-testing")]
3213            let allow_localnet = allow_localnet;
3214            #[cfg(not(feature = "idl-localnet-testing"))]
3215            let allow_localnet = false;
3216            idl_upgrade(
3217                program_id,
3218                cfg_override,
3219                filepath,
3220                priority_fee,
3221                allow_localnet,
3222            )
3223        }
3224        IdlCommand::Build {
3225            program_name,
3226            out,
3227            out_ts,
3228            no_docs,
3229            skip_lint,
3230            cargo_args,
3231        } => idl_build(
3232            cfg_override,
3233            program_name,
3234            out,
3235            out_ts,
3236            no_docs,
3237            skip_lint,
3238            cargo_args,
3239        ),
3240        IdlCommand::Fetch {
3241            program_id: address,
3242            out,
3243            non_canonical,
3244        } => idl_fetch(cfg_override, address, out, non_canonical),
3245        IdlCommand::FetchHistorical {
3246            program_id: address,
3247            authority,
3248            slot,
3249            before,
3250            after,
3251            out_dir,
3252            rpc_workers,
3253            no_parallel,
3254            rpc_max_retries,
3255            rpc_retry_backoff_ms,
3256            max_signatures,
3257            verbose,
3258        } => fetch::idl_fetch_historical(
3259            cfg_override,
3260            address,
3261            authority,
3262            slot,
3263            before,
3264            after,
3265            out_dir,
3266            fetch::FetchTuning {
3267                workers: rpc_workers,
3268                no_parallel,
3269                max_retries: rpc_max_retries,
3270                retry_backoff_ms: rpc_retry_backoff_ms,
3271                max_signatures,
3272                verbose,
3273            },
3274        ),
3275        IdlCommand::Convert {
3276            path,
3277            out,
3278            program_id,
3279            to_legacy,
3280        } => idl_convert(path, out, program_id, to_legacy),
3281        IdlCommand::Type { path, out } => idl_type(path, out),
3282        IdlCommand::Close {
3283            program_id,
3284            seed,
3285            priority_fee,
3286        } => idl_close_metadata(cfg_override, program_id, seed, priority_fee),
3287        IdlCommand::CreateBuffer {
3288            filepath,
3289            priority_fee,
3290        } => idl_create_buffer(cfg_override, filepath, priority_fee),
3291        IdlCommand::SetBufferAuthority {
3292            buffer,
3293            new_authority,
3294            priority_fee,
3295        } => idl_set_buffer_authority(cfg_override, buffer, new_authority, priority_fee),
3296        IdlCommand::WriteBuffer {
3297            program_id,
3298            buffer,
3299            seed,
3300            close_buffer,
3301            priority_fee,
3302        } => idl_write_buffer_metadata(
3303            cfg_override,
3304            program_id,
3305            buffer,
3306            seed,
3307            close_buffer,
3308            priority_fee,
3309        ),
3310    }
3311}
3312
3313fn idl_init(
3314    program_id: Option<Pubkey>,
3315    cfg_override: &ConfigOverride,
3316    idl_filepath: PathBuf,
3317    priority_fee: Option<u64>,
3318    non_canonical: bool,
3319    allow_localnet: bool,
3320) -> Result<()> {
3321    // Get cluster URL and wallet path from Anchor config
3322    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3323
3324    let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
3325    if is_localnet && !allow_localnet {
3326        #[cfg(feature = "idl-localnet-testing")]
3327        println!(
3328            "Skipping IDL initialization on localnet. To deploy on localnet, use --allow-localnet"
3329        );
3330        #[cfg(not(feature = "idl-localnet-testing"))]
3331        println!("Skipping IDL initialization on localnet");
3332        return Ok(());
3333    }
3334
3335    let program_id = match program_id {
3336        Some(id) => id.to_string(),
3337        _ => {
3338            let idl = fs::read(&idl_filepath)?;
3339            let idl = convert_idl(&idl)?;
3340            idl.address
3341        }
3342    };
3343
3344    let command = metadata::IdlCommand::funded(
3345        cluster_url,
3346        wallet_path,
3347        priority_fee,
3348        metadata::FundedIdlSubcommand::Write {
3349            program_id,
3350            idl_filepath: idl_filepath
3351                .to_str()
3352                .ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
3353                .to_string(),
3354            non_canonical,
3355        },
3356    );
3357
3358    if !command.status()?.success() {
3359        return Err(anyhow!("Failed to initialize IDL"));
3360    }
3361
3362    println!("IDL initialized.");
3363    Ok(())
3364}
3365
3366// Currently identical to `idl_init`, other than not accepting `non_canonical`
3367fn idl_upgrade(
3368    program_id: Option<Pubkey>,
3369    cfg_override: &ConfigOverride,
3370    idl_filepath: PathBuf,
3371    priority_fee: Option<u64>,
3372    allow_localnet: bool,
3373) -> Result<()> {
3374    // Get cluster URL and wallet path from Anchor config
3375    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3376
3377    let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
3378    if is_localnet && !allow_localnet {
3379        #[cfg(feature = "idl-localnet-testing")]
3380        println!("Skipping IDL upgrade on localnet. To deploy on localnet, use --allow-localnet");
3381        #[cfg(not(feature = "idl-localnet-testing"))]
3382        println!("Skipping IDL upgrade on localnet");
3383        return Ok(());
3384    }
3385
3386    let program_id = match program_id {
3387        Some(id) => id.to_string(),
3388        _ => {
3389            let idl = fs::read(&idl_filepath)?;
3390            let idl = convert_idl(&idl)?;
3391            idl.address
3392        }
3393    };
3394
3395    let command = metadata::IdlCommand::funded(
3396        cluster_url,
3397        wallet_path,
3398        priority_fee,
3399        metadata::FundedIdlSubcommand::Write {
3400            program_id,
3401            idl_filepath: idl_filepath
3402                .to_str()
3403                .ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
3404                .to_string(),
3405            non_canonical: false,
3406        },
3407    );
3408    if !command.status()?.success() {
3409        return Err(anyhow!("Failed to upgrade IDL"));
3410    }
3411
3412    println!("IDL upgraded.");
3413    Ok(())
3414}
3415
3416fn idl_build(
3417    cfg_override: &ConfigOverride,
3418    program_name: Option<String>,
3419    out: Option<String>,
3420    out_ts: Option<String>,
3421    no_docs: bool,
3422    skip_lint: bool,
3423    cargo_args: Vec<String>,
3424) -> Result<()> {
3425    let cfg = Config::discover(cfg_override)?
3426        .ok_or_else(|| anyhow!("The 'anchor idl build' command requires an Anchor workspace."))?;
3427    let current_dir = std::env::current_dir()?;
3428    let program_path = match program_name {
3429        Some(name) => cfg.get_program(&name)?.path,
3430        None => {
3431            let programs = cfg.read_all_programs()?;
3432            if programs.len() == 1 {
3433                programs.into_iter().next().unwrap().path
3434            } else {
3435                programs
3436                    .into_iter()
3437                    .find(|program| program.path == current_dir)
3438                    .ok_or_else(|| anyhow!("Not in a program directory"))?
3439                    .path
3440            }
3441        }
3442    };
3443    std::env::set_current_dir(program_path)?;
3444    let idl = generate_idl(&cfg, skip_lint, no_docs, &cargo_args)?;
3445    std::env::set_current_dir(current_dir)?;
3446
3447    let out = match out {
3448        Some(path) => OutFile::File(PathBuf::from(path)),
3449        None => OutFile::Stdout,
3450    };
3451    write_idl(&idl, out)?;
3452
3453    if let Some(path) = out_ts {
3454        let ts_out_dir = PathBuf::from(&path);
3455        fs::write(path, idl_ts(&idl)?)?;
3456        // Generate error constants file if errors exist
3457        let ts_out_parent = ts_out_dir.parent().unwrap_or_else(|| Path::new("."));
3458        write_error_constants_file(&idl, ts_out_parent, None)?;
3459    }
3460
3461    Ok(())
3462}
3463
3464/// Generate IDL with method decided by whether manifest file has `idl-build` feature or not.
3465fn generate_idl(
3466    cfg: &WithPath<Config>,
3467    skip_lint: bool,
3468    no_docs: bool,
3469    cargo_args: &[String],
3470) -> Result<Idl> {
3471    check_idl_build_feature()?;
3472
3473    let idl = anchor_lang_idl::build::IdlBuilder::new()
3474        .resolution(cfg.features.resolution)
3475        .skip_lint(cfg.features.skip_lint || skip_lint)
3476        .no_docs(no_docs)
3477        .cargo_args(cargo_args.into())
3478        .build()?;
3479
3480    // Warn users if there is a potential for a conflict between user-defined discriminators and
3481    // hardcoded `event-cpi` discriminator.
3482    //
3483    // Note: Warn independent of whether the user has the `event-cpi` feature enabled to make sure
3484    // there are no potential conflicts in the future if/when the user decides to enable it.
3485    idl.instructions
3486        .iter()
3487        .filter(|ix| anchor_lang::event::EVENT_IX_TAG_LE.starts_with(&ix.discriminator))
3488        .for_each(|ix| {
3489            eprintln!(
3490                "Warning: Instruction conflicts with `event-cpi` instruction discriminator: `{}`",
3491                ix.name
3492            );
3493        });
3494
3495    Ok(idl)
3496}
3497
3498fn idl_fetch(
3499    cfg_override: &ConfigOverride,
3500    address: Pubkey,
3501    out: Option<String>,
3502    non_canonical: bool,
3503) -> Result<()> {
3504    let (cluster_url, _) = get_cluster_and_wallet(cfg_override)?;
3505    let command = metadata::IdlCommand::unfunded(
3506        cluster_url,
3507        metadata::UnfundedIdlSubcommand::Fetch {
3508            program_id: address.to_string(),
3509            out,
3510            non_canonical,
3511        },
3512    );
3513
3514    if !command.status()?.success() {
3515        return Err(anyhow!("Failed to fetch IDL"));
3516    }
3517    Ok(())
3518}
3519
3520/// Apply a `--program-id` override to a raw IDL JSON document. The current
3521/// and legacy specs store the program address in different places, so we
3522/// detect the spec by the presence of `metadata.spec` and patch the right
3523/// field. For the legacy spec we merge into the existing `metadata` object
3524/// instead of replacing it; replacing it would drop sibling fields, and for
3525/// a current-spec IDL it would also wipe `metadata.{spec,name,version}` and
3526/// cause `convert_idl` to mis-detect the file as legacy.
3527fn apply_program_id_override(idl: &[u8], program_id: Pubkey) -> Result<Vec<u8>> {
3528    let mut idl = serde_json::from_slice::<serde_json::Value>(idl)?;
3529    let obj = idl
3530        .as_object_mut()
3531        .ok_or_else(|| anyhow!("IDL must be an object"))?;
3532    let pid = program_id.to_string();
3533    let is_current_spec = obj.get("metadata").and_then(|m| m.get("spec")).is_some();
3534    if is_current_spec {
3535        // Current spec stores the address at the top level.
3536        obj.insert("address".into(), serde_json::Value::String(pid));
3537    } else {
3538        // Legacy spec stores it under `metadata.address`. Merge so we
3539        // don't drop any sibling metadata fields the file may already
3540        // carry.
3541        match obj.get_mut("metadata") {
3542            Some(serde_json::Value::Object(m)) => {
3543                m.insert("address".into(), serde_json::Value::String(pid));
3544            }
3545            _ => {
3546                obj.insert("metadata".into(), serde_json::json!({ "address": pid }));
3547            }
3548        }
3549    }
3550    serde_json::to_vec(&idl).map_err(Into::into)
3551}
3552
3553fn idl_convert(
3554    path: PathBuf,
3555    out: Option<PathBuf>,
3556    program_id: Option<Pubkey>,
3557    to_legacy: bool,
3558) -> Result<()> {
3559    let idl = fs::read(path)?;
3560    let idl = match program_id {
3561        Some(program_id) => apply_program_id_override(&idl, program_id)?,
3562        None => idl,
3563    };
3564
3565    // Normalize either input spec to a current-spec `Idl`; both output
3566    // branches need the parsed value.
3567    let parsed = convert_idl(&idl)?;
3568    let out = match out {
3569        None => OutFile::Stdout,
3570        Some(out) => OutFile::File(out),
3571    };
3572    if to_legacy {
3573        let bytes = convert_idl_to_legacy(&parsed)?;
3574        match out {
3575            OutFile::Stdout => {
3576                let s =
3577                    std::str::from_utf8(&bytes).context("legacy IDL JSON was not valid UTF-8")?;
3578                println!("{s}");
3579                Ok(())
3580            }
3581            OutFile::File(path) => fs::write(path, bytes).map_err(Into::into),
3582        }
3583    } else {
3584        write_idl(&parsed, out)
3585    }
3586}
3587
3588fn idl_type(path: PathBuf, out: Option<PathBuf>) -> Result<()> {
3589    let idl = fs::read(path)?;
3590    let idl = convert_idl(&idl)?;
3591    let types = idl_ts(&idl)?;
3592    match out {
3593        Some(out) => fs::write(out, types)?,
3594        _ => println!("{types}"),
3595    };
3596    Ok(())
3597}
3598
3599fn idl_close_metadata(
3600    cfg_override: &ConfigOverride,
3601    program_id: Pubkey,
3602    seed: String,
3603    priority_fee: Option<u64>,
3604) -> Result<()> {
3605    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3606    let command = metadata::IdlCommand::funded(
3607        cluster_url,
3608        wallet_path,
3609        priority_fee,
3610        metadata::FundedIdlSubcommand::Close {
3611            program_id: program_id.to_string(),
3612            seed,
3613        },
3614    );
3615
3616    if !command.status()?.success() {
3617        return Err(anyhow!("Failed to close metadata account"));
3618    }
3619
3620    println!("Metadata account closed successfully.");
3621    Ok(())
3622}
3623
3624fn idl_create_buffer(
3625    cfg_override: &ConfigOverride,
3626    filepath: PathBuf,
3627    priority_fee: Option<u64>,
3628) -> Result<()> {
3629    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3630    let command = metadata::IdlCommand::funded(
3631        cluster_url,
3632        wallet_path,
3633        priority_fee,
3634        metadata::FundedIdlSubcommand::CreateBuffer {
3635            filepath: filepath
3636                .to_str()
3637                .ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
3638                .to_string(),
3639        },
3640    );
3641
3642    if !command.status()?.success() {
3643        return Err(anyhow!("Failed to create buffer"));
3644    }
3645
3646    println!("Buffer created successfully.");
3647    Ok(())
3648}
3649
3650fn idl_set_buffer_authority(
3651    cfg_override: &ConfigOverride,
3652    buffer: Pubkey,
3653    new_authority: Pubkey,
3654    priority_fee: Option<u64>,
3655) -> Result<()> {
3656    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3657    let command = metadata::IdlCommand::funded(
3658        cluster_url,
3659        wallet_path,
3660        priority_fee,
3661        metadata::FundedIdlSubcommand::SetBufferAuthority {
3662            buffer: buffer.to_string(),
3663            new_authority: new_authority.to_string(),
3664        },
3665    );
3666
3667    if !command.status()?.success() {
3668        return Err(anyhow!("Failed to set buffer authority"));
3669    }
3670
3671    println!("Buffer authority set successfully.");
3672    Ok(())
3673}
3674
3675fn idl_write_buffer_metadata(
3676    cfg_override: &ConfigOverride,
3677    program_id: Pubkey,
3678    buffer: Pubkey,
3679    seed: String,
3680    close_buffer: bool,
3681    priority_fee: Option<u64>,
3682) -> Result<()> {
3683    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3684    let command = metadata::IdlCommand::funded(
3685        cluster_url,
3686        wallet_path,
3687        priority_fee,
3688        metadata::FundedIdlSubcommand::WriteBuffer {
3689            program_id: program_id.to_string(),
3690            buffer: buffer.to_string(),
3691            seed,
3692            close_buffer,
3693        },
3694    );
3695
3696    if !command.status()?.success() {
3697        return Err(anyhow!("Failed to write metadata using buffer"));
3698    }
3699
3700    println!("Metadata written successfully using buffer.");
3701    Ok(())
3702}
3703
3704fn idl_ts(idl: &Idl) -> Result<String> {
3705    let idl_name = &idl.metadata.name;
3706    let type_name = idl_name.to_pascal_case();
3707    let mut camel_idl = serde_json::to_value(idl)?;
3708    camel_case_idl_identifiers(&mut camel_idl);
3709    let camel_idl = serde_json::to_string_pretty(&serde_json::from_value::<Idl>(camel_idl)?)?;
3710
3711    Ok(format!(
3712        r#"/**
3713 * Program IDL in camelCase format in order to be used in JS/TS.
3714 *
3715 * Note that this is only a type helper and is not the actual IDL. The original
3716 * IDL can be found at `target/idl/{idl_name}.json`.
3717 */
3718export type {type_name} = {camel_idl};
3719"#
3720    ))
3721}
3722
3723fn camel_case_idl_identifiers(value: &mut JsonValue) {
3724    match value {
3725        JsonValue::Array(values) => {
3726            for value in values {
3727                camel_case_idl_identifiers(value);
3728            }
3729        }
3730        JsonValue::Object(map) => {
3731            for (key, value) in map {
3732                if is_idl_identifier_key(key) {
3733                    camel_case_idl_identifier(value);
3734                } else {
3735                    camel_case_idl_identifiers(value);
3736                }
3737            }
3738        }
3739        _ => {}
3740    }
3741}
3742
3743fn camel_case_idl_identifier(value: &mut JsonValue) {
3744    match value {
3745        JsonValue::String(s) => {
3746            if Pubkey::try_from(s.as_str()).is_err() {
3747                *s = s
3748                    .split('.')
3749                    .map(ToLowerCamelCase::to_lower_camel_case)
3750                    .collect::<Vec<_>>()
3751                    .join(".");
3752            }
3753        }
3754        JsonValue::Array(values) => {
3755            for value in values {
3756                camel_case_idl_identifier(value);
3757            }
3758        }
3759        _ => camel_case_idl_identifiers(value),
3760    }
3761}
3762
3763fn is_idl_identifier_key(key: &str) -> bool {
3764    matches!(key, "name" | "path" | "account" | "relations" | "generic")
3765}
3766
3767fn idl_ts_errors(idl: &Idl) -> Option<String> {
3768    if idl.errors.is_empty() {
3769        return None;
3770    }
3771
3772    let idl_name = &idl.metadata.name;
3773    let type_name = idl_name.to_pascal_case();
3774    let error_code_name = format!("{type_name}ErrorCode");
3775
3776    let error_entries: Vec<String> = idl
3777        .errors
3778        .iter()
3779        .map(|error| format!("  {}: {}", error.name, error.code))
3780        .collect();
3781
3782    Some(format!(
3783        r#"
3784export const {error_code_name} = {{
3785{errors}
3786}};
3787
3788export type {type_name}ErrorName = keyof typeof {error_code_name};
3789"#,
3790        errors = error_entries.join(",\n")
3791    ))
3792}
3793
3794fn write_error_constants_file(
3795    idl: &Idl,
3796    ts_out_dir: &Path,
3797    cfg_types_dir: Option<PathBuf>,
3798) -> Result<()> {
3799    let error_file_name = format!("{}_errors.ts", idl.metadata.name);
3800    let error_out = ts_out_dir.join(&error_file_name);
3801    let cfg_error_out = cfg_types_dir.map(|types_dir| types_dir.join(&error_file_name));
3802
3803    let Some(error_constants) = idl_ts_errors(idl) else {
3804        // No errors in the IDL: remove any stale error constants file.
3805        remove_file_if_exists(&error_out)?;
3806        if let Some(cfg_error_out) = cfg_error_out {
3807            remove_file_if_exists(&cfg_error_out)?;
3808        }
3809        return Ok(());
3810    };
3811
3812    fs::write(&error_out, error_constants)?;
3813
3814    // Copy out the error constants file to workspace types directory if configured
3815    if let Some(cfg_error_out) = cfg_error_out {
3816        fs::copy(&error_out, cfg_error_out)?;
3817    }
3818    Ok(())
3819}
3820
3821fn remove_file_if_exists(path: &Path) -> Result<()> {
3822    match fs::remove_file(path) {
3823        Ok(()) => Ok(()),
3824        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
3825        Err(err) => Err(err.into()),
3826    }
3827}
3828
3829fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
3830    let idl_json = serde_json::to_string_pretty(idl)?;
3831    match out {
3832        OutFile::Stdout => println!("{idl_json}"),
3833        OutFile::File(out) => fs::write(out, idl_json)?,
3834    };
3835
3836    Ok(())
3837}
3838fn account(
3839    cfg_override: &ConfigOverride,
3840    account_type: String,
3841    address: Pubkey,
3842    idl_filepath: Option<PathBuf>,
3843) -> Result<()> {
3844    let (program_name, account_type_name) = account_type
3845        .split_once('.') // Split at first occurrence of dot
3846        .and_then(|(x, y)| y.find('.').map_or_else(|| Some((x, y)), |_| None)) // ensures no dots in second substring
3847        .ok_or_else(|| {
3848            anyhow!(
3849                "Please enter the account struct in the following format: <program_name>.<Account>",
3850            )
3851        })?;
3852
3853    let idl = idl_filepath.map_or_else(
3854        || {
3855            let config = Config::discover(cfg_override)?.ok_or_else(|| {
3856                anyhow!(
3857                    "The 'anchor account' command requires an Anchor workspace with Anchor.toml \
3858                     for IDL type generation."
3859                )
3860            })?;
3861            let programs = config
3862                .read_all_programs()
3863                .expect("Workspace must contain atleast one program.");
3864
3865            let program = programs
3866                .iter()
3867                .find(|p| p.lib_name == *program_name)
3868                .ok_or_else(|| {
3869                    let mut available_programs: Vec<String> =
3870                        programs.iter().map(|p| p.lib_name.clone()).collect();
3871                    available_programs.sort();
3872
3873                    if available_programs.is_empty() {
3874                        anyhow!(
3875                            "Program '{program_name}' not found in workspace. No programs \
3876                             available."
3877                        )
3878                    } else {
3879                        anyhow!(
3880                            "Program '{program_name}' not found in workspace.\n\nAvailable \
3881                             programs:\n  {}",
3882                            available_programs.join("\n  ")
3883                        )
3884                    }
3885                })?;
3886
3887            program.idl.clone().ok_or_else(|| {
3888                anyhow!("IDL not found. Please build the program atleast once to generate the IDL.")
3889            })
3890        },
3891        |idl_path| {
3892            let idl = fs::read(idl_path)?;
3893            let idl = convert_idl(&idl)?;
3894            if idl.metadata.name != *program_name {
3895                return Err(anyhow!("IDL does not match program {program_name}."));
3896            }
3897
3898            Ok(idl)
3899        },
3900    )?;
3901
3902    let cluster = match &cfg_override.cluster {
3903        Some(cluster) => cluster.clone(),
3904        None => Config::discover(cfg_override)?
3905            .map(|cfg| cfg.provider.cluster.clone())
3906            .unwrap_or(Cluster::Localnet),
3907    };
3908
3909    let data = create_client(cluster.url()).get_account_data(&address)?;
3910    let disc_len = idl
3911        .accounts
3912        .iter()
3913        .find(|acc| acc.name == *account_type_name)
3914        .map(|acc| acc.discriminator.len())
3915        .ok_or_else(|| {
3916            let mut available_accounts: Vec<String> =
3917                idl.accounts.iter().map(|acc| acc.name.clone()).collect();
3918            available_accounts.sort();
3919
3920            if available_accounts.is_empty() {
3921                anyhow!(
3922                    "Account '{account_type_name}' not found in IDL. No accounts available in \
3923                     program '{program_name}'."
3924                )
3925            } else {
3926                anyhow!(
3927                    "Account '{account_type_name}' not found in IDL.\n\nAvailable accounts in \
3928                     program '{program_name}':\n  {}",
3929                    available_accounts.join("\n  ")
3930                )
3931            }
3932        })?;
3933    let mut data_view = &data[disc_len..];
3934
3935    let deserialized_json =
3936        deserialize_idl_defined_type_to_json(&idl, account_type_name, &mut data_view)?;
3937
3938    println!(
3939        "{}",
3940        serde_json::to_string_pretty(&deserialized_json).unwrap()
3941    );
3942
3943    Ok(())
3944}
3945
3946// Deserializes user defined IDL types by munching the account data(recursively).
3947fn deserialize_idl_defined_type_to_json(
3948    idl: &Idl,
3949    defined_type_name: &str,
3950    data: &mut &[u8],
3951) -> Result<JsonValue, anyhow::Error> {
3952    let defined_type = &idl
3953        .accounts
3954        .iter()
3955        .find(|acc| acc.name == defined_type_name)
3956        .and_then(|acc| idl.types.iter().find(|ty| ty.name == acc.name))
3957        .or_else(|| idl.types.iter().find(|ty| ty.name == defined_type_name))
3958        .ok_or_else(|| anyhow!("Type `{}` not found in IDL.", defined_type_name))?
3959        .ty;
3960
3961    let mut deserialized_fields = Map::new();
3962
3963    match defined_type {
3964        IdlTypeDefTy::Struct { fields } => {
3965            if let Some(fields) = fields {
3966                match fields {
3967                    IdlDefinedFields::Named(fields) => {
3968                        for field in fields {
3969                            deserialized_fields.insert(
3970                                field.name.clone(),
3971                                deserialize_idl_type_to_json(&field.ty, data, idl)?,
3972                            );
3973                        }
3974                    }
3975                    IdlDefinedFields::Tuple(fields) => {
3976                        let mut values = Vec::new();
3977                        for field in fields {
3978                            values.push(deserialize_idl_type_to_json(field, data, idl)?);
3979                        }
3980                        deserialized_fields
3981                            .insert(defined_type_name.to_owned(), JsonValue::Array(values));
3982                    }
3983                }
3984            }
3985        }
3986        IdlTypeDefTy::Enum { variants } => {
3987            let repr = <u8 as AnchorDeserialize>::deserialize(data)?;
3988
3989            let variant = variants
3990                .get(repr as usize)
3991                .ok_or_else(|| anyhow!("Error while deserializing enum variant {repr}"))?;
3992
3993            let mut value = json!({});
3994
3995            if let Some(enum_field) = &variant.fields {
3996                match enum_field {
3997                    IdlDefinedFields::Named(fields) => {
3998                        let mut values = Map::new();
3999                        for field in fields {
4000                            values.insert(
4001                                field.name.clone(),
4002                                deserialize_idl_type_to_json(&field.ty, data, idl)?,
4003                            );
4004                        }
4005                        value = JsonValue::Object(values);
4006                    }
4007                    IdlDefinedFields::Tuple(fields) => {
4008                        let mut values = Vec::new();
4009                        for field in fields {
4010                            values.push(deserialize_idl_type_to_json(field, data, idl)?);
4011                        }
4012                        value = JsonValue::Array(values);
4013                    }
4014                }
4015            }
4016
4017            deserialized_fields.insert(variant.name.clone(), value);
4018        }
4019        IdlTypeDefTy::Type { alias } => {
4020            return deserialize_idl_type_to_json(alias, data, idl);
4021        }
4022    }
4023
4024    Ok(JsonValue::Object(deserialized_fields))
4025}
4026
4027// Deserializes a primitive type using AnchorDeserialize
4028fn deserialize_idl_type_to_json(
4029    idl_type: &IdlType,
4030    data: &mut &[u8],
4031    parent_idl: &Idl,
4032) -> Result<JsonValue, anyhow::Error> {
4033    if data.is_empty() {
4034        return Err(anyhow::anyhow!("Unable to parse from empty bytes"));
4035    }
4036
4037    Ok(match idl_type {
4038        IdlType::Bool => json!(<bool as AnchorDeserialize>::deserialize(data)?),
4039        IdlType::U8 => {
4040            json!(<u8 as AnchorDeserialize>::deserialize(data)?)
4041        }
4042        IdlType::I8 => {
4043            json!(<i8 as AnchorDeserialize>::deserialize(data)?)
4044        }
4045        IdlType::U16 => {
4046            json!(<u16 as AnchorDeserialize>::deserialize(data)?)
4047        }
4048        IdlType::I16 => {
4049            json!(<i16 as AnchorDeserialize>::deserialize(data)?)
4050        }
4051        IdlType::U32 => {
4052            json!(<u32 as AnchorDeserialize>::deserialize(data)?)
4053        }
4054        IdlType::I32 => {
4055            json!(<i32 as AnchorDeserialize>::deserialize(data)?)
4056        }
4057        IdlType::F32 => json!(<f32 as AnchorDeserialize>::deserialize(data)?),
4058        IdlType::U64 => {
4059            json!(<u64 as AnchorDeserialize>::deserialize(data)?)
4060        }
4061        IdlType::I64 => {
4062            json!(<i64 as AnchorDeserialize>::deserialize(data)?)
4063        }
4064        IdlType::F64 => json!(<f64 as AnchorDeserialize>::deserialize(data)?),
4065        IdlType::U128 => {
4066            json!(<u128 as AnchorDeserialize>::deserialize(data)?)
4067        }
4068        IdlType::I128 => {
4069            json!(<i128 as AnchorDeserialize>::deserialize(data)?)
4070        }
4071        IdlType::U256 => todo!("Upon completion of u256 IDL standard"),
4072        IdlType::I256 => todo!("Upon completion of i256 IDL standard"),
4073        IdlType::Bytes => JsonValue::Array(
4074            <Vec<u8> as AnchorDeserialize>::deserialize(data)?
4075                .iter()
4076                .map(|i| json!(*i))
4077                .collect(),
4078        ),
4079        IdlType::String => json!(<String as AnchorDeserialize>::deserialize(data)?),
4080        IdlType::Pubkey => {
4081            json!(<Pubkey as AnchorDeserialize>::deserialize(data)?.to_string())
4082        }
4083        IdlType::Array(ty, size) => match size {
4084            IdlArrayLen::Value(size) => {
4085                let mut array_data: Vec<JsonValue> = Vec::with_capacity(*size);
4086
4087                for _ in 0..*size {
4088                    array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
4089                }
4090
4091                JsonValue::Array(array_data)
4092            }
4093            // TODO:
4094            IdlArrayLen::Generic(_) => unimplemented!("Generic array length is not yet supported"),
4095        },
4096        IdlType::Option(ty) => {
4097            let is_present = <u8 as AnchorDeserialize>::deserialize(data)?;
4098
4099            if is_present == 0 {
4100                JsonValue::String("None".to_string())
4101            } else {
4102                deserialize_idl_type_to_json(ty, data, parent_idl)?
4103            }
4104        }
4105        IdlType::Vec(ty) => {
4106            let size: usize = <u32 as AnchorDeserialize>::deserialize(data)?
4107                .try_into()
4108                .unwrap();
4109
4110            let mut vec_data: Vec<JsonValue> = Vec::with_capacity(size);
4111
4112            for _ in 0..size {
4113                vec_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
4114            }
4115
4116            JsonValue::Array(vec_data)
4117        }
4118        IdlType::Defined {
4119            name,
4120            generics: _generics,
4121        } => {
4122            // TODO: Generics
4123            deserialize_idl_defined_type_to_json(parent_idl, name, data)?
4124        }
4125        IdlType::Generic(generic) => json!(generic),
4126        _ => unimplemented!("{idl_type:?}"),
4127    })
4128}
4129
4130enum OutFile {
4131    Stdout,
4132    File(PathBuf),
4133}
4134
4135// Builds, deploys, and tests all workspace programs in a single command.
4136#[allow(clippy::too_many_arguments)]
4137fn test(
4138    cfg_override: &ConfigOverride,
4139    program_name: Option<String>,
4140    skip_deploy: bool,
4141    skip_local_validator: bool,
4142    skip_build: bool,
4143    skip_lint: bool,
4144    no_idl: bool,
4145    detach: bool,
4146    tests_to_run: Vec<String>,
4147    script_name: Option<String>,
4148    validator_type: ValidatorType,
4149    profile: bool,
4150    gdb: bool,
4151    extra_args: Vec<String>,
4152    env_vars: Vec<String>,
4153    cargo_args: Vec<String>,
4154) -> Result<()> {
4155    let test_paths = tests_to_run
4156        .iter()
4157        .map(|path| {
4158            PathBuf::from(path)
4159                .canonicalize()
4160                .map_err(|_| anyhow!("Wrong path {}", path))
4161        })
4162        .collect::<Result<Vec<_>, _>>()?;
4163
4164    with_workspace(cfg_override, |cfg| -> Result<()> {
4165        // Set validator type based on CLI choice, with an escape hatch for CI
4166        // matrices that need a runtime compatible with the build arch.
4167        let validator_type = validator_type_from_env()?.unwrap_or(validator_type);
4168        cfg.validator = Some(validator_type);
4169
4170        let cli_skip_local_validator = skip_local_validator;
4171        let config_skip_local_validator = cfg.skip_local_validator.unwrap_or(false);
4172        let workspace_root = cfg.path().parent().unwrap().to_owned();
4173
4174        #[cfg(windows)]
4175        if profile {
4176            return Err(anyhow!(
4177                "`anchor test --profile` is not supported on Windows"
4178            ));
4179        }
4180        #[cfg(windows)]
4181        let _ = gdb;
4182
4183        #[cfg(not(windows))]
4184        let profile_dir = workspace_root.join(crate::profile::DEFAULT_PROFILE_DIR);
4185        #[cfg(not(windows))]
4186        let _gdb_guard: Option<crate::debugger::gdb::GdbDriver> = if profile {
4187            let _ = fs::remove_dir_all(&profile_dir);
4188            std::env::set_var("ANCHOR_PROFILE_DIR", &profile_dir);
4189            std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
4190
4191            if let Some(test_script) = cfg.scripts.get_mut("test") {
4192                if test_script.contains("cargo test") {
4193                    *test_script =
4194                        test_script.replacen("cargo test", "cargo test --features profile", 1);
4195                    if gdb {
4196                        let sep = if test_script.contains(" -- ") {
4197                            " "
4198                        } else {
4199                            " -- "
4200                        };
4201                        *test_script = format!("{test_script}{sep}--test-threads=1");
4202                    }
4203                } else {
4204                    eprintln!(
4205                        "warning: --profile requires the `test` script in Anchor.toml to invoke \
4206                         `cargo test`; got: {test_script:?}. Profiling will not activate."
4207                    );
4208                }
4209            } else {
4210                eprintln!(
4211                    "warning: --profile requires a [scripts] test entry in Anchor.toml; none \
4212                     found. Profiling will not activate."
4213                );
4214            }
4215
4216            if gdb {
4217                let driver = crate::debugger::gdb::start_gdb_driver(&profile_dir)?;
4218                std::env::set_var(crate::debugger::gdb::SOCKET_ENV, driver.sock_path());
4219                std::env::set_var("RUST_TEST_THREADS", "1");
4220                Some(driver)
4221            } else {
4222                None
4223            }
4224        } else {
4225            None
4226        };
4227
4228        // Build if needed.
4229        if !skip_build {
4230            build(
4231                cfg_override,
4232                no_idl,
4233                None,
4234                None,
4235                false,
4236                skip_lint,
4237                true,
4238                program_name.clone(),
4239                None,
4240                None,
4241                BootstrapMode::None,
4242                BuildSbfOptions::default(),
4243                None,
4244                None,
4245                env_vars,
4246                cargo_args,
4247                false,
4248            )?;
4249        }
4250
4251        cfg.add_test_config(workspace_root, test_paths)?;
4252
4253        // Deploy to the cluster unless told to skip. For localnet, preserve
4254        // explicit `--skip-local-validator` deploys because the validator is
4255        // already running, but don't let generated in-process templates force
4256        // an RPC deploy through their persisted config.
4257        let is_localnet = cfg.provider.cluster == Cluster::Localnet;
4258        let validator_plan = test_validator_plan(
4259            skip_deploy,
4260            is_localnet,
4261            cli_skip_local_validator,
4262            config_skip_local_validator,
4263        );
4264        if validator_plan.predeploy {
4265            deploy(cfg_override, None, None, false, true, vec![])?;
4266        }
4267
4268        cfg.run_hooks(HookType::PreTest)?;
4269
4270        let mut is_first_suite = true;
4271        let script_name_to_use = script_name.as_deref().unwrap_or("test");
4272        if let Some(test_script) = cfg.scripts.get_mut(script_name_to_use) {
4273            is_first_suite = false;
4274
4275            match program_name {
4276                Some(program_name) => {
4277                    if let Some((from, to)) = Regex::new("\\s(tests/\\S+\\.(js|ts))")
4278                        .unwrap()
4279                        .captures_iter(&test_script.clone())
4280                        .last()
4281                        .and_then(|c| c.get(1).zip(c.get(2)))
4282                        .map(|(mtch, ext)| {
4283                            (
4284                                mtch.as_str(),
4285                                format!("tests/{program_name}.{}", ext.as_str()),
4286                            )
4287                        })
4288                    {
4289                        println!("\nRunning tests of program `{program_name}`!");
4290                        // Replace the last path to the program name's path
4291                        *test_script = test_script.replace(from, &to);
4292                    }
4293                }
4294                _ => println!(
4295                    "\nFound a '{}' script in the Anchor.toml. Running it as a test suite!",
4296                    script_name_to_use
4297                ),
4298            }
4299
4300            run_test_suite(
4301                cfg,
4302                cfg.path(),
4303                is_localnet,
4304                validator_plan.skip_local_validator,
4305                skip_deploy,
4306                detach,
4307                validator_type,
4308                &cfg.test_validator,
4309                &cfg.scripts,
4310                script_name_to_use,
4311                validator_plan.stream_program_logs,
4312                &extra_args,
4313                &cfg.surfpool_config,
4314            )?;
4315        }
4316        if let Some(test_config) = &cfg.test_config {
4317            for test_suite in test_config.iter() {
4318                if !is_first_suite {
4319                    std::thread::sleep(std::time::Duration::from_millis(
4320                        test_suite
4321                            .1
4322                            .test
4323                            .as_ref()
4324                            .map(|val| val.shutdown_wait)
4325                            .unwrap_or(SHUTDOWN_WAIT) as u64,
4326                    ));
4327                } else {
4328                    is_first_suite = false;
4329                }
4330
4331                run_test_suite(
4332                    cfg,
4333                    test_suite.0,
4334                    is_localnet,
4335                    validator_plan.skip_local_validator,
4336                    skip_deploy,
4337                    detach,
4338                    validator_type,
4339                    &test_suite.1.test,
4340                    &test_suite.1.scripts,
4341                    script_name_to_use,
4342                    validator_plan.stream_program_logs,
4343                    &extra_args,
4344                    &cfg.surfpool_config,
4345                )?;
4346            }
4347        }
4348        cfg.run_hooks(HookType::PostTest)?;
4349
4350        #[cfg(not(windows))]
4351        if profile {
4352            render_profile(cfg, &profile_dir)?;
4353        }
4354
4355        Ok(())
4356    })?
4357}
4358
4359fn should_predeploy_before_test(
4360    skip_deploy: bool,
4361    is_localnet: bool,
4362    cli_skip_local_validator: bool,
4363) -> bool {
4364    !skip_deploy && (!is_localnet || cli_skip_local_validator)
4365}
4366
4367#[derive(Debug, PartialEq, Eq)]
4368struct TestValidatorPlan {
4369    skip_local_validator: bool,
4370    predeploy: bool,
4371    stream_program_logs: bool,
4372}
4373
4374fn test_validator_plan(
4375    skip_deploy: bool,
4376    is_localnet: bool,
4377    cli_skip_local_validator: bool,
4378    config_skip_local_validator: bool,
4379) -> TestValidatorPlan {
4380    TestValidatorPlan {
4381        skip_local_validator: cli_skip_local_validator || config_skip_local_validator,
4382        predeploy: should_predeploy_before_test(skip_deploy, is_localnet, cli_skip_local_validator),
4383        stream_program_logs: true,
4384    }
4385}
4386
4387/// Run the test suite with profile tracing enabled and then launch the SBF instruction stepper.
4388#[cfg(not(windows))]
4389#[allow(clippy::too_many_arguments)]
4390fn debugger(
4391    cfg_override: &ConfigOverride,
4392    test_name: Option<String>,
4393    skip_run: bool,
4394    skip_build: bool,
4395    skip_lint: bool,
4396    gdb: bool,
4397    cargo_args: Vec<String>,
4398) -> Result<()> {
4399    let has_anchor_toml = match Config::discover(cfg_override) {
4400        Ok(Some(_)) => true,
4401        Ok(None) => false,
4402        Err(e) => return Err(anyhow!("failed to probe for Anchor.toml: {e}")),
4403    };
4404
4405    if has_anchor_toml {
4406        debugger_anchor_workspace(
4407            cfg_override,
4408            test_name,
4409            skip_run,
4410            skip_build,
4411            skip_lint,
4412            gdb,
4413            cargo_args,
4414        )
4415    } else {
4416        debugger_loose(
4417            cfg_override,
4418            test_name,
4419            skip_run,
4420            skip_build,
4421            gdb,
4422            cargo_args,
4423        )
4424    }
4425}
4426
4427#[cfg(not(windows))]
4428#[allow(clippy::too_many_arguments)]
4429fn debugger_anchor_workspace(
4430    cfg_override: &ConfigOverride,
4431    test_name: Option<String>,
4432    skip_run: bool,
4433    skip_build: bool,
4434    skip_lint: bool,
4435    gdb: bool,
4436    cargo_args: Vec<String>,
4437) -> Result<()> {
4438    if !skip_run {
4439        test(
4440            cfg_override,
4441            None,
4442            true,
4443            true,
4444            skip_build,
4445            skip_lint,
4446            true,
4447            false,
4448            Vec::new(),
4449            None, // script_name — debugger drives test execution itself
4450            ValidatorType::Surfpool,
4451            true,
4452            gdb,
4453            Vec::new(),
4454            Vec::new(),
4455            cargo_args,
4456        )?;
4457    }
4458
4459    with_workspace(cfg_override, |cfg| -> Result<()> {
4460        let workspace_root = cfg.path().parent().unwrap().to_owned();
4461        let profile_dir = workspace_root.join(crate::profile::DEFAULT_PROFILE_DIR);
4462        let (pubkey_to_so, sources) = resolve_anchor_workspace_programs(cfg);
4463
4464        if pubkey_to_so.is_empty() {
4465            return Err(anyhow!(
4466                "no programs resolved for the debugger.\n\nEither declare them in Anchor.toml:\n  \
4467                 [programs.localnet]\n  <name> = \"<pubkey>\"\n\nor run `anchor build` so \
4468                 `target/deploy/<name>-keypair.json` exists."
4469            ));
4470        }
4471
4472        println!("\nResolved programs:");
4473        for (pk, so) in &pubkey_to_so {
4474            let src = sources.get(pk).copied().unwrap_or("unknown");
4475            println!("  {pk}  ->  {}  [{src}]", display_path_relative_to_cwd(so));
4476        }
4477
4478        debugger::run(
4479            &profile_dir,
4480            &pubkey_to_so,
4481            Some(&workspace_root),
4482            None,
4483            test_name.as_deref(),
4484        )
4485    })?
4486}
4487
4488#[cfg(not(windows))]
4489#[allow(clippy::too_many_arguments)]
4490fn debugger_loose(
4491    _cfg_override: &ConfigOverride,
4492    test_name: Option<String>,
4493    skip_run: bool,
4494    skip_build: bool,
4495    gdb: bool,
4496    cargo_args: Vec<String>,
4497) -> Result<()> {
4498    let cwd = std::env::current_dir().context("read current directory")?;
4499    let ws = debugger::loose::LooseWorkspace::discover(cwd)?;
4500
4501    if !skip_run {
4502        ws.check_dev_dep()?;
4503    }
4504    let profile_feature = ws.detect_profile_feature()?;
4505    let profile_dir = ws.root.join(debugger::loose_profile_dir_name());
4506
4507    if !skip_run {
4508        debugger::loose::clear_profile_dir(&profile_dir)?;
4509        std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
4510
4511        let anchor_exe =
4512            std::env::current_exe().context("resolve anchor binary path for RUSTC_WRAPPER")?;
4513        std::env::set_var("RUSTC_WRAPPER", &anchor_exe);
4514        std::env::set_var(debugger::rustc_wrapper::WRAPPER_SENTINEL, "1");
4515
4516        if !skip_build {
4517            let build_cwd = ws.cargo_invocation_dir();
4518            eprintln!("running `cargo build-sbf` from {}", build_cwd.display());
4519            cargo_build_sbf(Some(build_cwd), &BuildSbfOptions::default(), &cargo_args)?;
4520        }
4521
4522        std::env::remove_var("RUSTC_WRAPPER");
4523        std::env::remove_var(debugger::rustc_wrapper::WRAPPER_SENTINEL);
4524
4525        eprintln!(
4526            "running `cargo test{gdb} --features {profile_feature}{pkg}{filter}` from {dir}",
4527            gdb = if gdb { " [gdb mode]" } else { "" },
4528            pkg = ws
4529                .current_package
4530                .as_deref()
4531                .map(|p| format!(" -p {p}"))
4532                .unwrap_or_default(),
4533            filter = test_name
4534                .as_deref()
4535                .map(|f| format!(" -- {f}"))
4536                .unwrap_or_default(),
4537            dir = ws.cargo_invocation_dir().display(),
4538        );
4539        if gdb {
4540            debugger::gdb::run_gdb_mode(
4541                ws.cargo_invocation_dir(),
4542                ws.current_package.as_deref(),
4543                &profile_feature,
4544                &profile_dir,
4545                test_name.as_deref(),
4546            )?;
4547        } else {
4548            debugger::loose::run_cargo_test(
4549                ws.cargo_invocation_dir(),
4550                ws.current_package.as_deref(),
4551                &profile_feature,
4552                &profile_dir,
4553                test_name.as_deref(),
4554            )?;
4555        }
4556    }
4557
4558    let pubkey_to_so = debugger::loose::discover_programs(&ws.root, ws.current_package.as_deref())?;
4559    if pubkey_to_so.is_empty() {
4560        eprintln!(
4561            "warning: no programs found under {}/target/deploy/.\nELFs are required for \
4562             source/disasm symbolication. The debugger will still open but the static disasm pane \
4563             will be empty.",
4564            ws.root.display()
4565        );
4566    }
4567
4568    if !profile_dir.exists() {
4569        return Err(anyhow!(
4570            "no traces produced at {}.\n\nDid the test actually run? Check that:\n- the test \
4571             calls `anchor_v2_testing::svm()` (NOT `LiteSVM::new()`)\n- the `{profile_feature}` \
4572             feature is enabled in the test build\n- the test sent at least one transaction that \
4573             hit a BPF program",
4574            profile_dir.display()
4575        ));
4576    }
4577
4578    debugger::run(
4579        &profile_dir,
4580        &pubkey_to_so,
4581        Some(&ws.root),
4582        Some(&ws.cwd),
4583        test_name.as_deref(),
4584    )
4585}
4586
4587#[cfg(not(windows))]
4588fn run_coverage(
4589    _cfg_override: &ConfigOverride,
4590    skip_run: bool,
4591    skip_build: bool,
4592    output: &str,
4593    trace_dir: &str,
4594    cargo_args: Vec<String>,
4595) -> Result<()> {
4596    let cwd = std::env::current_dir().context("read current directory")?;
4597    let ws = debugger::loose::LooseWorkspace::discover(cwd)?;
4598
4599    let trace_path = ws.root.join(trace_dir);
4600    let output_path = ws.root.join(output);
4601
4602    if !skip_run {
4603        std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
4604
4605        let anchor_exe =
4606            std::env::current_exe().context("resolve anchor binary path for RUSTC_WRAPPER")?;
4607        std::env::set_var("RUSTC_WRAPPER", &anchor_exe);
4608        std::env::set_var(debugger::rustc_wrapper::WRAPPER_SENTINEL, "1");
4609
4610        if !skip_build {
4611            let build_cwd = ws.cargo_invocation_dir();
4612            eprintln!("building programs with DWARF...");
4613            cargo_build_sbf(Some(build_cwd), &BuildSbfOptions::default(), &cargo_args)?;
4614        }
4615
4616        if trace_path.exists() {
4617            fs::remove_dir_all(&trace_path)?;
4618        }
4619        fs::create_dir_all(&trace_path)?;
4620
4621        let profile_feature = ws.detect_profile_feature().ok();
4622        eprintln!("running tests with register tracing...");
4623        let mut cmd = std::process::Command::new("cargo");
4624        cmd.current_dir(ws.cargo_invocation_dir()).arg("test");
4625        if let Some(feature) = &profile_feature {
4626            cmd.env("ANCHOR_PROFILE_DIR", &trace_path)
4627                .arg("--features")
4628                .arg(feature);
4629        } else {
4630            cmd.env("SBF_TRACE_DIR", &trace_path);
4631        }
4632        if let Some(pkg) = &ws.current_package {
4633            cmd.arg("-p").arg(pkg);
4634        }
4635        let status = cmd.status().context("spawn cargo test")?;
4636        if !status.success() {
4637            return Err(anyhow!("cargo test failed"));
4638        }
4639    }
4640
4641    if !trace_path.exists() {
4642        return Err(anyhow!(
4643            "no traces at {}. Run without --skip-run first.",
4644            trace_path.display()
4645        ));
4646    }
4647
4648    let programs = debugger::loose::discover_programs(&ws.root, ws.current_package.as_deref())?;
4649    if programs.is_empty() {
4650        return Err(anyhow!(
4651            "no programs found. Ensure declare_id!() is present in source.",
4652        ));
4653    }
4654
4655    if let Some(parent) = output_path.parent() {
4656        fs::create_dir_all(parent)?;
4657    }
4658    coverage::generate_lcov(&trace_path, &programs, Some(&ws.root), &output_path)
4659}
4660
4661#[cfg(not(windows))]
4662fn display_path_relative_to_cwd(p: &Path) -> String {
4663    std::env::current_dir()
4664        .ok()
4665        .as_deref()
4666        .and_then(|c| p.strip_prefix(c).ok())
4667        .map(|rel| rel.display().to_string())
4668        .unwrap_or_else(|| p.display().to_string())
4669}
4670
4671#[cfg(not(windows))]
4672fn resolve_anchor_workspace_programs(
4673    cfg: &WithPath<Config>,
4674) -> (BTreeMap<String, PathBuf>, BTreeMap<String, &'static str>) {
4675    let workspace_root = cfg.path().parent().unwrap();
4676    let deploy_dir = workspace_root.join("target").join("deploy");
4677    let mut pubkey_to_so: BTreeMap<String, PathBuf> = BTreeMap::new();
4678    let mut sources: BTreeMap<String, &'static str> = BTreeMap::new();
4679    for programs in cfg.programs.values() {
4680        for (name, deployment) in programs {
4681            let pk = deployment.address.to_string();
4682            pubkey_to_so.insert(pk.clone(), deploy_dir.join(format!("{name}.so")));
4683            sources.insert(pk, "Anchor.toml");
4684        }
4685    }
4686    if let Ok(discovered) = debugger::loose::discover_programs(workspace_root, None) {
4687        for (pk, so) in discovered {
4688            if !pubkey_to_so.contains_key(&pk) {
4689                pubkey_to_so.insert(pk.clone(), so);
4690                sources.insert(pk, "target/deploy");
4691            }
4692        }
4693    }
4694    (pubkey_to_so, sources)
4695}
4696
4697#[cfg(not(windows))]
4698fn render_profile(cfg: &WithPath<Config>, profile_dir: &Path) -> Result<()> {
4699    let workspace_root = cfg.path().parent().unwrap().to_owned();
4700    let (pubkey_to_so, _sources) = resolve_anchor_workspace_programs(cfg);
4701
4702    let rendered = profile::render_all_tests(profile_dir, Some(&workspace_root), &pubkey_to_so)
4703        .context("failed to render flamegraphs from trace directory")?;
4704
4705    if rendered.is_empty() {
4706        eprintln!(
4707            "warning: no per-test trace directories found under {}. Did your tests call \
4708             `anchor_v2_testing::svm()` with the `profile` feature?",
4709            profile_dir.display()
4710        );
4711        return Ok(());
4712    }
4713
4714    let mut sorted: Vec<&profile::RenderedTest> = rendered.iter().collect();
4715    sorted.sort_by(|a, b| a.test_name.cmp(&b.test_name));
4716
4717    let max_name = sorted
4718        .iter()
4719        .filter(|t| t.svg_paths.len() == 1)
4720        .map(|t| t.test_name.len())
4721        .max()
4722        .unwrap_or(0);
4723
4724    println!("\nFlamegraphs:");
4725    for test in &sorted {
4726        if test.svg_paths.len() == 1 {
4727            println!(
4728                "  {:<width$}  ->  {}",
4729                test.test_name,
4730                display_path_relative_to_cwd(&test.svg_paths[0]),
4731                width = max_name,
4732            );
4733        } else {
4734            println!("  {}", test.test_name);
4735            for (i, svg) in test.svg_paths.iter().enumerate() {
4736                println!("    tx{}  ->  {}", i + 1, display_path_relative_to_cwd(svg));
4737            }
4738        }
4739    }
4740
4741    Ok(())
4742}
4743
4744#[allow(clippy::too_many_arguments)]
4745fn run_test_suite(
4746    cfg: &WithPath<Config>,
4747    test_suite_path: impl AsRef<Path>,
4748    is_localnet: bool,
4749    skip_local_validator: bool,
4750    skip_deploy: bool,
4751    detach: bool,
4752    validator_type: ValidatorType,
4753    test_validator: &Option<TestValidator>,
4754    scripts: &ScriptsConfig,
4755    script_name: &str,
4756    stream_program_logs: bool,
4757    extra_args: &[String],
4758    surfpool_config: &Option<SurfpoolConfig>,
4759) -> Result<()> {
4760    println!("\nRunning test suite: {:#?}\n", test_suite_path.as_ref());
4761    let mut validator_handle = None;
4762    if is_localnet && !skip_local_validator {
4763        let generated_accounts = generated_validator_accounts(cfg, test_validator)?;
4764        match validator_type {
4765            ValidatorType::Surfpool => {
4766                let full_simnet_mode = false;
4767                let flags = Some(surfpool_flags(
4768                    cfg,
4769                    surfpool_config,
4770                    full_simnet_mode,
4771                    skip_deploy,
4772                    Some(test_suite_path.as_ref()),
4773                    &generated_accounts,
4774                )?);
4775                validator_handle = Some(start_surfpool_validator(
4776                    flags,
4777                    surfpool_config,
4778                    full_simnet_mode,
4779                )?);
4780            }
4781            ValidatorType::Legacy => {
4782                let flags = Some(validator_flags(
4783                    cfg,
4784                    test_validator,
4785                    skip_deploy,
4786                    &generated_accounts,
4787                )?);
4788                validator_handle = Some(start_solana_test_validator(
4789                    cfg,
4790                    test_validator,
4791                    flags,
4792                    true,
4793                )?);
4794            }
4795        }
4796    }
4797    let url = cluster_url(cfg, test_validator, surfpool_config);
4798
4799    let node_options = format!(
4800        "{} {}",
4801        match std::env::var_os("NODE_OPTIONS") {
4802            Some(value) => value
4803                .into_string()
4804                .map_err(std::env::VarError::NotUnicode)?,
4805            None => "".to_owned(),
4806        },
4807        get_node_dns_option(),
4808    );
4809
4810    // Setup log reader - kept alive until end of scope
4811    let log_streams = if stream_program_logs {
4812        match stream_logs(cfg, &url) {
4813            Ok(streams) => Some(streams),
4814            Err(e) => {
4815                eprintln!("Warning: Failed to setup program log streaming: {:#}", e);
4816                eprintln!("Program logs will still be visible in the test output.");
4817                None
4818            }
4819        }
4820    } else {
4821        None
4822    };
4823
4824    // Run the tests.
4825    let test_result = {
4826        let Some(cmd) = scripts.get(script_name) else {
4827            bail!("Not able to find script for `{}`", script_name);
4828        };
4829        let cmd = cmd.clone();
4830        let script_args = format!("{cmd} {}", extra_args.join(" "));
4831
4832        std::process::Command::new("bash")
4833            .arg("-c")
4834            .arg(script_args)
4835            .env("ANCHOR_PROVIDER_URL", url)
4836            .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
4837            .env("NODE_OPTIONS", node_options)
4838            .stdout(Stdio::inherit())
4839            .stderr(Stdio::inherit())
4840            .output()
4841            .map_err(anyhow::Error::from)
4842            .context(cmd)
4843    };
4844
4845    // Keep validator running if needed.
4846    if test_result.is_ok() && detach {
4847        println!("Local validator still running. Press Ctrl + C quit.");
4848        std::io::stdin().lock().lines().next().unwrap().unwrap();
4849    }
4850
4851    // Check all errors and shut down.
4852    if let Some(mut child) = validator_handle {
4853        if let Err(err) = child.kill() {
4854            println!("Failed to kill subprocess {}: {}", child.id(), err);
4855        }
4856    }
4857
4858    // Explicitly shutdown log streams - closes WebSocket subscriptions
4859    if let Some(log_streams) = log_streams {
4860        for handle in log_streams {
4861            handle.shutdown();
4862        }
4863    }
4864
4865    // Must exist *after* shutting down the validator and log streams.
4866    match test_result {
4867        Ok(exit) => {
4868            if !exit.status.success() {
4869                std::process::exit(exit.status.code().unwrap());
4870            }
4871        }
4872        Err(err) => {
4873            println!("Failed to run test: {err:#}");
4874            return Err(err);
4875        }
4876    }
4877
4878    Ok(())
4879}
4880
4881// Returns the solana-test-validator flags. When `skip_deploy` is false, this
4882// embeds the workspace programs in the genesis block so we don't have to deploy
4883// every time. It also allows control of other solana-test-validator features.
4884fn validator_flags(
4885    cfg: &WithPath<Config>,
4886    test_validator: &Option<TestValidator>,
4887    skip_deploy: bool,
4888    generated_accounts: &[GeneratedAccount],
4889) -> Result<Vec<String>> {
4890    let mut flags = match skip_deploy {
4891        true => Vec::new(),
4892        false => validator_deploy_flags(cfg, test_validator)?,
4893    };
4894    for acct in generated_accounts {
4895        flags.push("--account".to_string());
4896        flags.push(acct.pubkey.to_string());
4897        flags.push(acct.file_path.display().to_string());
4898    }
4899    flags.extend(validator_config_flags(test_validator)?);
4900    Ok(flags)
4901}
4902
4903/// Returns the rent-exempt minimum for the given account size.
4904fn rent_exempt_minimum(data_len: u64) -> u64 {
4905    (128 + data_len) * 3480 * 2
4906}
4907
4908const RENT_EPOCH_NEVER: u64 = u64::MAX;
4909
4910/// Writes a keypair file with restrictive permissions.
4911fn write_keypair_secure(keypair: &Keypair, path: &Path) -> Result<()> {
4912    use std::io::Write;
4913    let bytes = keypair.to_bytes().to_vec();
4914    let json = serde_json::to_string(&bytes)
4915        .with_context(|| format!("Failed to serialize keypair for {}", path.display()))?;
4916
4917    let mut opts = std::fs::OpenOptions::new();
4918    opts.write(true).create_new(true);
4919    #[cfg(unix)]
4920    {
4921        use std::os::unix::fs::OpenOptionsExt;
4922        opts.mode(0o600);
4923    }
4924    let mut file = opts
4925        .open(path)
4926        .with_context(|| format!("Failed to create keypair file: {}", path.display()))?;
4927    file.write_all(json.as_bytes())
4928        .with_context(|| format!("Failed to write keypair file: {}", path.display()))?;
4929    Ok(())
4930}
4931
4932/// Packs a `COption<Pubkey>` into the canonical on-chain byte layout.
4933fn pack_coption_pubkey(buf: &mut Vec<u8>, value: Option<Pubkey>) {
4934    match value {
4935        Some(pk) => {
4936            buf.extend_from_slice(&1u32.to_le_bytes());
4937            buf.extend_from_slice(pk.as_ref());
4938        }
4939        None => {
4940            buf.extend_from_slice(&0u32.to_le_bytes());
4941            buf.extend_from_slice(&[0u8; 32]);
4942        }
4943    }
4944}
4945
4946/// Writes a validator account JSON fixture to disk.
4947fn write_account_json(path: &Path, value: &JsonValue) -> Result<()> {
4948    let mut file = File::create(path)
4949        .with_context(|| format!("Failed to create account file: {}", path.display()))?;
4950    serde_json::to_writer_pretty(&mut file, value)
4951        .with_context(|| format!("Failed to write account JSON to: {}", path.display()))?;
4952    Ok(())
4953}
4954
4955/// Returns whether a config field requests a freshly generated address.
4956fn is_new_address(address: &str) -> bool {
4957    address.eq_ignore_ascii_case("new")
4958}
4959
4960#[derive(Debug, Clone)]
4961struct GeneratedAccount {
4962    pubkey: Pubkey,
4963    file_path: PathBuf,
4964    surfpool_snapshot_value: JsonValue,
4965}
4966
4967/// Resolves a config-relative path against the workspace root.
4968fn resolve_workspace_path(cfg: &WithPath<Config>, path: &str) -> Result<PathBuf> {
4969    let workspace_root = cfg
4970        .path()
4971        .parent()
4972        .ok_or_else(|| anyhow!("Anchor.toml path has no parent directory"))?;
4973    let candidate = Path::new(path);
4974    Ok(if candidate.is_relative() {
4975        workspace_root.join(candidate)
4976    } else {
4977        candidate.to_path_buf()
4978    })
4979}
4980
4981/// Collects pubkeys declared by `account_dir` JSON fixtures.
4982fn account_dir_pubkeys(cfg: &WithPath<Config>, validator: &Validator) -> Result<HashSet<Pubkey>> {
4983    let mut pubkeys = HashSet::new();
4984    for account_dir in validator.account_dir.iter().flatten() {
4985        let directory = resolve_workspace_path(cfg, &account_dir.directory)?;
4986        for entry in fs::read_dir(&directory)
4987            .with_context(|| format!("Failed to read account directory: {}", directory.display()))?
4988        {
4989            let path = entry?.path();
4990            if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
4991                continue;
4992            }
4993
4994            let fixture: JsonValue =
4995                serde_json::from_reader(File::open(&path).with_context(|| {
4996                    format!("Failed to open account fixture: {}", path.display())
4997                })?)
4998                .with_context(|| format!("Failed to parse account fixture: {}", path.display()))?;
4999
5000            let Some(pubkey) = fixture.get("pubkey").and_then(JsonValue::as_str) else {
5001                continue;
5002            };
5003            pubkeys.insert(
5004                Pubkey::try_from(pubkey)
5005                    .map_err(|_| anyhow!("Invalid pubkey {} in {}", pubkey, path.display()))?,
5006            );
5007        }
5008    }
5009    Ok(pubkeys)
5010}
5011
5012/// Collects the pubkeys the validator will preload before token-account materialization.
5013fn validator_supplied_account_pubkeys(
5014    cfg: &WithPath<Config>,
5015    validator: &Validator,
5016    created_mints: &[Pubkey],
5017) -> Result<HashSet<Pubkey>> {
5018    let mut pubkeys = created_mints.iter().copied().collect::<HashSet<_>>();
5019
5020    if let Some(accounts) = &validator.account {
5021        for account in accounts {
5022            pubkeys.insert(
5023                Pubkey::try_from(account.address.as_str())
5024                    .map_err(|_| anyhow!("Invalid account pubkey: {}", account.address))?,
5025            );
5026        }
5027    }
5028
5029    if let Some(clones) = &validator.clone {
5030        for clone in clones {
5031            pubkeys.insert(
5032                Pubkey::try_from(clone.address.as_str())
5033                    .map_err(|_| anyhow!("Invalid clone pubkey: {}", clone.address))?,
5034            );
5035        }
5036    }
5037
5038    pubkeys.extend(account_dir_pubkeys(cfg, validator)?);
5039    Ok(pubkeys)
5040}
5041
5042/// Materializes generated validator accounts for use by both legacy validator flags and Surfpool.
5043fn materialize_validator_accounts(
5044    cfg: &WithPath<Config>,
5045    validator: &Validator,
5046) -> Result<Vec<GeneratedAccount>> {
5047    let mut out = Vec::new();
5048    let needs_dir = validator.mints.is_some()
5049        || validator.token_accounts.is_some()
5050        || validator.fund_accounts.is_some();
5051    if !needs_dir {
5052        return Ok(out);
5053    }
5054
5055    let workspace_root = cfg
5056        .path()
5057        .parent()
5058        .ok_or_else(|| anyhow!("Anchor.toml path has no parent directory"))?;
5059    let accounts_dir = workspace_root.join(".anchor").join("generated_accounts");
5060    fs::create_dir_all(&accounts_dir).with_context(|| {
5061        format!(
5062            "Failed to create accounts directory: {}",
5063            accounts_dir.display()
5064        )
5065    })?;
5066
5067    let mut seen_pubkeys: HashSet<Pubkey> = HashSet::new();
5068    let mut record_pubkey = |pk: Pubkey, section: &str| -> Result<()> {
5069        if !seen_pubkeys.insert(pk) {
5070            bail!(
5071                "Duplicate pubkey {} across [test.validator] sections (collision detected in \
5072                 `{}`). Each generated account must have a unique address.",
5073                pk,
5074                section
5075            );
5076        }
5077        Ok(())
5078    };
5079
5080    let mut created_mints: Vec<Pubkey> = Vec::new();
5081
5082    if let Some(mints) = &validator.mints {
5083        for token_mint in mints {
5084            let pubkey = if is_new_address(&token_mint.address) {
5085                let keypair = Keypair::new();
5086                let pubkey = keypair.pubkey();
5087                let keypair_path = accounts_dir.join(format!("{}.mint.json", pubkey));
5088                write_keypair_secure(&keypair, &keypair_path)?;
5089                pubkey
5090            } else {
5091                Pubkey::try_from(token_mint.address.as_str())
5092                    .map_err(|_| anyhow!("Invalid mint pubkey address: {}", token_mint.address))?
5093            };
5094            record_pubkey(pubkey, "mints")?;
5095            created_mints.push(pubkey);
5096
5097            let parse_authority = |opt: &Option<String>, field: &str| -> Result<Option<Pubkey>> {
5098                opt.as_ref()
5099                    .map(|s| {
5100                        Pubkey::try_from(s.as_str()).map_err(|_| {
5101                            anyhow!("Invalid {} pubkey for mint {}: {}", field, pubkey, s)
5102                        })
5103                    })
5104                    .transpose()
5105            };
5106            let mint_authority = parse_authority(&token_mint.mint_authority, "mint_authority")?;
5107            let freeze_authority =
5108                parse_authority(&token_mint.freeze_authority, "freeze_authority")?;
5109
5110            let mut data = Vec::with_capacity(82);
5111            pack_coption_pubkey(&mut data, mint_authority);
5112            data.extend_from_slice(&token_mint.supply.unwrap_or(0).to_le_bytes());
5113            data.push(token_mint.decimals);
5114            data.push(1u8); // is_initialized
5115            pack_coption_pubkey(&mut data, freeze_authority);
5116
5117            let account_json = json!({
5118                "pubkey": pubkey.to_string(),
5119                "account": {
5120                    "lamports": rent_exempt_minimum(82),
5121                    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
5122                    "executable": false,
5123                    "rentEpoch": RENT_EPOCH_NEVER,
5124                    "data": [STANDARD.encode(&data), "base64"]
5125                }
5126            });
5127            let file_path = accounts_dir.join(format!("{}.json", pubkey));
5128            write_account_json(&file_path, &account_json)?;
5129            out.push(GeneratedAccount {
5130                pubkey,
5131                file_path,
5132                surfpool_snapshot_value: json!({
5133                    "lamports": rent_exempt_minimum(82),
5134                    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
5135                    "executable": false,
5136                    "rentEpoch": RENT_EPOCH_NEVER,
5137                    "data": STANDARD.encode(&data),
5138                    "parsedData": JsonValue::Null,
5139                }),
5140            });
5141        }
5142    }
5143
5144    if let Some(token_accounts) = &validator.token_accounts {
5145        let validator_supplied_pubkeys =
5146            validator_supplied_account_pubkeys(cfg, validator, &created_mints)?;
5147        for token_account in token_accounts {
5148            let mint_pubkey = if is_new_address(&token_account.mint) {
5149                *created_mints.last().ok_or_else(|| {
5150                    anyhow!(
5151                        "token_account specifies `mint = \"new\"` but no [[test.validator.mints]] \
5152                         entries are configured"
5153                    )
5154                })?
5155            } else {
5156                let mint_pubkey = Pubkey::try_from(token_account.mint.as_str()).map_err(|_| {
5157                    anyhow!(
5158                        "Invalid mint pubkey in token_account: {}",
5159                        token_account.mint
5160                    )
5161                })?;
5162                if !validator_supplied_pubkeys.contains(&mint_pubkey) {
5163                    bail!(
5164                        "token_account mint {} is not loaded by the validator. Add it via \
5165                         [[test.validator.mints]], [[test.validator.clone]], \
5166                         [[test.validator.account]], or [[test.validator.account_dir]].",
5167                        mint_pubkey
5168                    );
5169                }
5170                mint_pubkey
5171            };
5172
5173            let owner_pubkey = if is_new_address(&token_account.owner) {
5174                let kp = Keypair::new();
5175                let pk = kp.pubkey();
5176                let owner_path = accounts_dir.join(format!("{}.owner.json", pk));
5177                write_keypair_secure(&kp, &owner_path)?;
5178                pk
5179            } else {
5180                Pubkey::try_from(token_account.owner.as_str()).map_err(|_| {
5181                    anyhow!(
5182                        "Invalid owner pubkey in token_account: {}",
5183                        token_account.owner
5184                    )
5185                })?
5186            };
5187
5188            let token_account_pubkey = match &token_account.address {
5189                Some(addr) if !is_new_address(addr) => Pubkey::try_from(addr.as_str())
5190                    .map_err(|_| anyhow!("Invalid token_account address pubkey: {}", addr))?,
5191                _ => {
5192                    let kp = Keypair::new();
5193                    let pk = kp.pubkey();
5194                    let ta_path = accounts_dir.join(format!("{}.token_account.json", pk));
5195                    write_keypair_secure(&kp, &ta_path)?;
5196                    pk
5197                }
5198            };
5199            record_pubkey(token_account_pubkey, "token_accounts")?;
5200
5201            let mut data = Vec::with_capacity(165);
5202            data.extend_from_slice(mint_pubkey.as_ref());
5203            data.extend_from_slice(owner_pubkey.as_ref());
5204            data.extend_from_slice(&token_account.amount.to_le_bytes());
5205            pack_coption_pubkey(&mut data, None); // delegate
5206            data.push(1u8); // state = initialized
5207            data.extend_from_slice(&0u32.to_le_bytes()); // is_native None tag
5208            data.extend_from_slice(&[0u8; 8]); // is_native body
5209            data.extend_from_slice(&0u64.to_le_bytes()); // delegated_amount
5210            pack_coption_pubkey(&mut data, None); // close_authority
5211
5212            let account_json = json!({
5213                "pubkey": token_account_pubkey.to_string(),
5214                "account": {
5215                    "lamports": rent_exempt_minimum(165),
5216                    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
5217                    "executable": false,
5218                    "rentEpoch": RENT_EPOCH_NEVER,
5219                    "data": [STANDARD.encode(&data), "base64"]
5220                }
5221            });
5222            let file_path = accounts_dir.join(format!("{}.json", token_account_pubkey));
5223            write_account_json(&file_path, &account_json)?;
5224            out.push(GeneratedAccount {
5225                pubkey: token_account_pubkey,
5226                file_path,
5227                surfpool_snapshot_value: json!({
5228                    "lamports": rent_exempt_minimum(165),
5229                    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
5230                    "executable": false,
5231                    "rentEpoch": RENT_EPOCH_NEVER,
5232                    "data": STANDARD.encode(&data),
5233                    "parsedData": JsonValue::Null,
5234                }),
5235            });
5236        }
5237    }
5238
5239    if let Some(fund_accounts) = &validator.fund_accounts {
5240        for funded_account in fund_accounts {
5241            let pubkey = if is_new_address(&funded_account.address) {
5242                let keypair = Keypair::new();
5243                let pubkey = keypair.pubkey();
5244                let keypair_path = accounts_dir.join(format!("{}.keypair.json", pubkey));
5245                write_keypair_secure(&keypair, &keypair_path)?;
5246                pubkey
5247            } else {
5248                Pubkey::try_from(funded_account.address.as_str())
5249                    .map_err(|_| anyhow!("Invalid pubkey address: {}", funded_account.address))?
5250            };
5251            record_pubkey(pubkey, "fund_accounts")?;
5252
5253            let lamports = funded_account.lamports.unwrap_or(1_000_000_000);
5254
5255            let account_json = json!({
5256                "pubkey": pubkey.to_string(),
5257                "account": {
5258                    "lamports": lamports,
5259                    "owner": "11111111111111111111111111111111",
5260                    "executable": false,
5261                    "rentEpoch": RENT_EPOCH_NEVER,
5262                    "data": ["", "base64"]
5263                }
5264            });
5265            let file_path = accounts_dir.join(format!("{}.json", pubkey));
5266            write_account_json(&file_path, &account_json)?;
5267            out.push(GeneratedAccount {
5268                pubkey,
5269                file_path,
5270                surfpool_snapshot_value: json!({
5271                    "lamports": lamports,
5272                    "owner": "11111111111111111111111111111111",
5273                    "executable": false,
5274                    "rentEpoch": RENT_EPOCH_NEVER,
5275                    "data": "",
5276                    "parsedData": JsonValue::Null,
5277                }),
5278            });
5279        }
5280    }
5281
5282    Ok(out)
5283}
5284
5285/// Computes the generated validator accounts once so all validator backends share them.
5286fn generated_validator_accounts(
5287    cfg: &WithPath<Config>,
5288    test_validator: &Option<TestValidator>,
5289) -> Result<Vec<GeneratedAccount>> {
5290    test_validator
5291        .as_ref()
5292        .and_then(|test| test.validator.as_ref())
5293        .map(|validator| materialize_validator_accounts(cfg, validator))
5294        .transpose()
5295        .map(|accounts| accounts.unwrap_or_default())
5296}
5297
5298/// Writes a Surfpool snapshot file for generated validator accounts and returns its path.
5299fn write_surfpool_snapshot(
5300    cfg: &WithPath<Config>,
5301    generated_accounts: &[GeneratedAccount],
5302) -> Result<Option<PathBuf>> {
5303    if generated_accounts.is_empty() {
5304        return Ok(None);
5305    }
5306
5307    let accounts_dir = resolve_workspace_path(cfg, ".anchor/generated_accounts")?;
5308    fs::create_dir_all(&accounts_dir).with_context(|| {
5309        format!(
5310            "Failed to create accounts directory for Surfpool snapshot: {}",
5311            accounts_dir.display()
5312        )
5313    })?;
5314
5315    let snapshot_path = accounts_dir.join("surfpool.snapshot.json");
5316    let mut snapshot = Map::new();
5317    for account in generated_accounts {
5318        snapshot.insert(
5319            account.pubkey.to_string(),
5320            account.surfpool_snapshot_value.clone(),
5321        );
5322    }
5323
5324    let mut file = File::create(&snapshot_path).with_context(|| {
5325        format!(
5326            "Failed to create Surfpool snapshot file: {}",
5327            snapshot_path.display()
5328        )
5329    })?;
5330    serde_json::to_writer_pretty(&mut file, &JsonValue::Object(snapshot)).with_context(|| {
5331        format!(
5332            "Failed to write Surfpool snapshot file: {}",
5333            snapshot_path.display()
5334        )
5335    })?;
5336
5337    Ok(Some(snapshot_path))
5338}
5339
5340fn validator_deploy_flags(
5341    cfg: &WithPath<Config>,
5342    test_validator: &Option<TestValidator>,
5343) -> Result<Vec<String>> {
5344    let programs = cfg.programs.get(&Cluster::Localnet);
5345
5346    let test_upgradeable_program = test_validator
5347        .as_ref()
5348        .map(|test_validator| test_validator.upgradeable)
5349        .unwrap_or(false);
5350
5351    let mut flags = Vec::new();
5352    for mut program in cfg.read_all_programs()? {
5353        let verifiable = false;
5354        let binary_path = program.binary_path(verifiable)?.display().to_string();
5355        // Use the [programs.cluster] override and fallback to the keypair
5356        // files if no override is given.
5357        let address = programs
5358            .and_then(|m| m.get(&program.lib_name))
5359            .map(|deployment| Ok(deployment.address.to_string()))
5360            .unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
5361
5362        if test_upgradeable_program {
5363            flags.push("--upgradeable-program".to_string());
5364            flags.push(address.clone());
5365            flags.push(binary_path);
5366            flags.push(cfg.wallet_kp()?.pubkey().to_string());
5367        } else {
5368            flags.push("--bpf-program".to_string());
5369            flags.push(address.clone());
5370            flags.push(binary_path);
5371        }
5372
5373        if let Some(idl) = program.idl.as_mut() {
5374            // Add program address to the IDL.
5375            idl.address = address;
5376
5377            // Persist it.
5378            let idl_out = target_dir()?
5379                .join("idl")
5380                .join(&idl.metadata.name)
5381                .with_extension("json");
5382            write_idl(idl, OutFile::File(idl_out))?;
5383        }
5384    }
5385
5386    if let Some(test) = test_validator.as_ref() {
5387        if let Some(genesis) = &test.genesis {
5388            for entry in genesis {
5389                let program_path = Path::new(&entry.program);
5390                if !program_path.exists() {
5391                    return Err(anyhow!(
5392                        "Program in genesis configuration does not exist at path: {}",
5393                        program_path.display()
5394                    ));
5395                }
5396                if entry.upgradeable.unwrap_or(false) {
5397                    flags.push("--upgradeable-program".to_string());
5398                    flags.push(entry.address.clone());
5399                    flags.push(entry.program.clone());
5400                    flags.push(cfg.wallet_kp()?.pubkey().to_string());
5401                } else {
5402                    flags.push("--bpf-program".to_string());
5403                    flags.push(entry.address.clone());
5404                    flags.push(entry.program.clone());
5405                }
5406            }
5407        }
5408    }
5409
5410    Ok(flags)
5411}
5412
5413fn validator_config_flags(test_validator: &Option<TestValidator>) -> Result<Vec<String>> {
5414    let mut flags = Vec::new();
5415
5416    if let Some(validator) = test_validator
5417        .as_ref()
5418        .and_then(|test| test.validator.as_ref())
5419    {
5420        let entries = serde_json::to_value(validator)?;
5421        for (key, value) in entries.as_object().unwrap() {
5422            if key == "ledger" {
5423                // Ledger flag is a special case as it is passed separately to the rest of
5424                // these validator flags.
5425                continue;
5426            };
5427            if key == "fund_accounts" || key == "mints" || key == "token_accounts" {
5428                continue;
5429            }
5430            if key == "extra_args" {
5431                for arg in value.as_array().unwrap() {
5432                    flags.push(arg.as_str().unwrap().to_string());
5433                }
5434                continue;
5435            }
5436            if key == "account" {
5437                for entry in value.as_array().unwrap() {
5438                    // Push the account flag for each array entry
5439                    flags.push("--account".to_string());
5440                    flags.push(entry["address"].as_str().unwrap().to_string());
5441                    flags.push(entry["filename"].as_str().unwrap().to_string());
5442                }
5443            } else if key == "account_dir" {
5444                for entry in value.as_array().unwrap() {
5445                    flags.push("--account-dir".to_string());
5446                    flags.push(entry["directory"].as_str().unwrap().to_string());
5447                }
5448            } else if key == "clone" {
5449                // Client for fetching accounts data
5450                let client = if let Some(url) = entries["url"].as_str() {
5451                    create_client(url)
5452                } else {
5453                    return Err(anyhow!(
5454                        "Validator url for Solana's JSON RPC should be provided in order to clone \
5455                         accounts from it"
5456                    ));
5457                };
5458
5459                let pubkeys = value
5460                    .as_array()
5461                    .unwrap()
5462                    .iter()
5463                    .map(|entry| {
5464                        let address = entry["address"].as_str().unwrap();
5465                        Pubkey::try_from(address).map_err(|_| anyhow!("Invalid pubkey {}", address))
5466                    })
5467                    .collect::<Result<HashSet<Pubkey>>>()?
5468                    .into_iter()
5469                    .collect::<Vec<_>>();
5470                let accounts = client.get_multiple_accounts(&pubkeys)?;
5471
5472                for (pubkey, account) in pubkeys.into_iter().zip(accounts) {
5473                    match account {
5474                        Some(account) => {
5475                            // Use a different flag for program accounts to fix the problem
5476                            // described in https://github.com/anza-xyz/agave/issues/522
5477                            if account.owner == bpf_loader_upgradeable::id()
5478                                // Only programs are supported with `--clone-upgradeable-program`
5479                                && matches!(
5480                                    account.deserialize_data::<UpgradeableLoaderState>()?,
5481                                    UpgradeableLoaderState::Program { .. }
5482                                )
5483                            {
5484                                flags.push("--clone-upgradeable-program".to_string());
5485                                flags.push(pubkey.to_string());
5486                            } else {
5487                                flags.push("--clone".to_string());
5488                                flags.push(pubkey.to_string());
5489                            }
5490                        }
5491                        _ => return Err(anyhow!("Account {} not found", pubkey)),
5492                    }
5493                }
5494            } else if key == "deactivate_feature" {
5495                // Verify that the feature flags are valid pubkeys
5496                let pubkeys_result: Result<Vec<Pubkey>, _> = value
5497                    .as_array()
5498                    .unwrap()
5499                    .iter()
5500                    .map(|entry| {
5501                        let feature_flag = entry.as_str().unwrap();
5502                        Pubkey::try_from(feature_flag)
5503                            .map_err(|_| anyhow!("Invalid pubkey (feature flag) {}", feature_flag))
5504                    })
5505                    .collect();
5506                let features = pubkeys_result?;
5507                for feature in features {
5508                    flags.push("--deactivate-feature".to_string());
5509                    flags.push(feature.to_string());
5510                }
5511            } else {
5512                // Remaining validator flags are non-array types
5513                flags.push(format!("--{}", key.replace('_', "-")));
5514                if let serde_json::Value::String(v) = value {
5515                    flags.push(v.to_string());
5516                } else {
5517                    flags.push(value.to_string());
5518                }
5519            }
5520        }
5521    }
5522
5523    Ok(flags)
5524}
5525
5526// Returns Surfpool flags.
5527// This flags will be passed to the Surfpool, it allows to configure the validator.
5528fn surfpool_flags(
5529    cfg: &WithPath<Config>,
5530    surfpool_config: &Option<SurfpoolConfig>,
5531    full_simnet_mode: bool,
5532    skip_deploy: bool,
5533    test_suite_path: Option<&Path>,
5534    generated_accounts: &[GeneratedAccount],
5535) -> Result<Vec<String>> {
5536    let programs = cfg.programs.get(&Cluster::Localnet);
5537    let mut flags = Vec::new();
5538
5539    for mut program in cfg.read_all_programs()? {
5540        let address = programs
5541            .and_then(|m| m.get(&program.lib_name))
5542            .map(|deployment| Ok(deployment.address.to_string()))
5543            .unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
5544        if let Some(idl) = program.idl.as_mut() {
5545            // Creating the idl files
5546            idl.address = address;
5547            let idl_out = target_dir()?
5548                .join("idl")
5549                .join(&idl.metadata.name)
5550                .with_extension("json");
5551            write_idl(idl, OutFile::File(idl_out))?;
5552        }
5553    }
5554
5555    if let Some(snapshot_path) = write_surfpool_snapshot(cfg, generated_accounts)? {
5556        flags.push("--snapshot".to_string());
5557        flags.push(snapshot_path.display().to_string());
5558    }
5559
5560    if let Some(config) = &surfpool_config {
5561        if let Some(airdrop_addresses) = &config.airdrop_addresses {
5562            for address in airdrop_addresses {
5563                flags.push("--airdrop".to_string());
5564                flags.push(address.to_string());
5565            }
5566        }
5567        if let Some(datasource_rpc_url) = &config.datasource_rpc_url {
5568            flags.push("--rpc-url".to_string());
5569            flags.push(datasource_rpc_url.to_string());
5570        }
5571
5572        let host = &config.host;
5573        flags.push("--host".to_string());
5574        flags.push(host.to_string());
5575
5576        let rpc_port = &config.rpc_port;
5577        flags.push("--port".to_string());
5578        flags.push(rpc_port.to_string());
5579
5580        if let Some(ws_port) = &config.ws_port {
5581            flags.push("--ws-port".to_string());
5582            flags.push(ws_port.to_string());
5583        }
5584
5585        if let Some(manifest_file_path) = &config.manifest_file_path {
5586            flags.push("--manifest-file-path".to_string());
5587            flags.push(manifest_file_path.to_string());
5588        }
5589
5590        if let Some(runbooks) = &config.runbooks {
5591            for runbook in runbooks {
5592                flags.push("--runbook".to_string());
5593                flags.push(runbook.to_string());
5594            }
5595        }
5596
5597        if let Some(slot_time) = &config.slot_time {
5598            flags.push("--slot-time".to_string());
5599            flags.push(slot_time.to_string());
5600        }
5601    }
5602
5603    let online = surfpool_config
5604        .as_ref()
5605        .and_then(|c| c.online)
5606        .unwrap_or(false);
5607    if !online {
5608        flags.push("--offline".to_string());
5609    }
5610
5611    let block_production_mode = surfpool_config
5612        .as_ref()
5613        .and_then(|c| c.block_production_mode.clone())
5614        .unwrap_or("transaction".into());
5615    flags.push("--block-production-mode".to_string());
5616    flags.push(block_production_mode);
5617
5618    flags.push("--log-level".to_string());
5619    flags.push(
5620        surfpool_config
5621            .as_ref()
5622            .and_then(|c| c.log_level.clone())
5623            .unwrap_or("none".into()),
5624    );
5625
5626    if !full_simnet_mode {
5627        flags.push("--no-tui".to_string());
5628        flags.push("--disable-instruction-profiling".to_string());
5629        flags.push("--max-profiles".to_string());
5630        flags.push("1".to_string());
5631        flags.push("--no-studio".to_string());
5632    }
5633
5634    match skip_deploy {
5635        true => flags.push("--no-deploy".to_string()),
5636        false => {
5637            // automatically generate in-memory runbooks
5638            flags.push("--legacy-anchor-compatibility".to_string());
5639            if let Some(test_suite_path) = test_suite_path {
5640                flags.push("--anchor-test-config-path".to_string());
5641                flags.push(test_suite_path.display().to_string());
5642            }
5643        }
5644    }
5645
5646    Ok(flags)
5647}
5648
5649/// Handle for a log streaming thread.
5650///
5651/// Manages a WebSocket subscription and its associated receiver thread.
5652/// Call `shutdown()` to cleanly stop the thread.
5653struct LogStreamHandle {
5654    subscription: PubsubClientSubscription<RpcResponse<RpcLogsResponse>>,
5655}
5656
5657impl LogStreamHandle {
5658    /// Explicitly shutdown the log stream
5659    fn shutdown(self) {
5660        // Send unsubscribe in a background thread to avoid blocking
5661        // PubsubClientSubscription::send_unsubscribe() can block indefinitely if WebSocket is stuck
5662        // The receiver threads will exit when the subscription closes
5663        std::thread::spawn(move || {
5664            let _ = self.subscription.send_unsubscribe();
5665        });
5666    }
5667}
5668
5669/// Spawns a thread to receive logs from a subscription and write them to a file
5670fn spawn_log_receiver_thread<R>(receiver: R, log_file_path: PathBuf)
5671where
5672    R: IntoIterator<Item = RpcResponse<RpcLogsResponse>> + Send + 'static,
5673{
5674    std::thread::spawn(move || {
5675        if let Ok(mut file) = File::create(&log_file_path) {
5676            for response in receiver {
5677                let _ = writeln!(
5678                    file,
5679                    "Transaction executed in slot {}:",
5680                    response.context.slot
5681                );
5682                let _ = writeln!(file, "  Signature: {}", response.value.signature);
5683                let _ = writeln!(
5684                    file,
5685                    "  Status: {}",
5686                    response
5687                        .value
5688                        .err
5689                        .map(|err| err.to_string())
5690                        .unwrap_or_else(|| "Ok".to_string())
5691                );
5692                let _ = writeln!(file, "  Log Messages:");
5693                for log in response.value.logs {
5694                    let _ = writeln!(file, "    {}", log);
5695                }
5696                let _ = writeln!(file); // Empty line between transactions
5697                let _ = file.flush();
5698            }
5699        } else {
5700            eprintln!("Failed to create log file: {:?}", log_file_path);
5701        }
5702    });
5703}
5704
5705fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStreamHandle>> {
5706    // Determine validator type to use appropriate logging
5707    match &config.validator {
5708        Some(ValidatorType::Surfpool) => {
5709            // For Surfpool, we don't need to stream logs via external commands
5710            // Surfpool handles its own logging to .surfpool/logs/ directory
5711            if config
5712                .surfpool_config
5713                .as_ref()
5714                .and_then(|s| {
5715                    s.log_level
5716                        .as_ref()
5717                        .map(|l| l.to_ascii_lowercase().ne("none"))
5718                })
5719                .unwrap_or(false)
5720            {
5721                println!("Surfpool validator logs: .surfpool/logs/ directory");
5722            }
5723            Ok(vec![])
5724        }
5725        Some(ValidatorType::Legacy) | None => stream_solana_logs(config, rpc_url),
5726    }
5727}
5728
5729fn stream_solana_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStreamHandle>> {
5730    let program_logs_dir = Path::new(".anchor").join("program-logs");
5731    if program_logs_dir.exists() {
5732        fs::remove_dir_all(&program_logs_dir)?;
5733    }
5734    fs::create_dir_all(&program_logs_dir)?;
5735
5736    // For solana-test-validator, the WebSocket port is RPC port + WEBSOCKET_PORT_OFFSET
5737    // Extract port from rpc_url and construct WebSocket URL
5738    let ws_url = if rpc_url.contains("127.0.0.1") || rpc_url.contains("localhost") {
5739        // Local validator: increment port by 1 for WebSocket
5740        let rpc_port = rpc_url
5741            .rsplit_once(':')
5742            .and_then(|(_, port)| port.parse::<u16>().ok())
5743            .unwrap_or(DEFAULT_RPC_PORT);
5744
5745        let ws_port = rpc_port + WEBSOCKET_PORT_OFFSET;
5746        let url = format!("ws://127.0.0.1:{}", ws_port);
5747        url
5748    } else {
5749        // Remote cluster: use same URL but replace http(s) with ws(s)
5750        rpc_url
5751            .replace("https://", "wss://")
5752            .replace("http://", "ws://")
5753    };
5754
5755    // Give the WebSocket endpoint a moment to be ready (especially for local validators)
5756    std::thread::sleep(std::time::Duration::from_millis(1500));
5757
5758    let mut handles = vec![];
5759
5760    // Subscribe to logs for all workspace programs
5761    for program in config.read_all_programs()? {
5762        let idl_path = target_dir()?
5763            .join("idl")
5764            .join(&program.lib_name)
5765            .with_extension("json");
5766        let idl = fs::read(&idl_path)?;
5767        let idl = convert_idl(&idl)?;
5768
5769        let log_file_path =
5770            program_logs_dir.join(format!("{}.{}.log", idl.address, program.lib_name));
5771        let program_address = idl.address.clone();
5772
5773        // Subscribe to logs using PubsubClient
5774        let (client, receiver) = match PubsubClient::logs_subscribe(
5775            &ws_url,
5776            RpcTransactionLogsFilter::Mentions(vec![program_address.clone()]),
5777            RpcTransactionLogsConfig {
5778                commitment: Some(CommitmentConfig::confirmed()),
5779            },
5780        ) {
5781            Ok(result) => result,
5782            Err(e) => {
5783                eprintln!(
5784                    "Warning: Failed to subscribe to logs for program {}: {}",
5785                    program.lib_name, e
5786                );
5787                continue;
5788            }
5789        };
5790
5791        // Spawn thread to write logs to file
5792        spawn_log_receiver_thread(receiver, log_file_path);
5793
5794        handles.push(LogStreamHandle {
5795            subscription: client,
5796        });
5797    }
5798
5799    // Also subscribe to logs for genesis programs
5800    if let Some(test) = config.test_validator.as_ref() {
5801        if let Some(genesis) = &test.genesis {
5802            for entry in genesis {
5803                let log_file_path = program_logs_dir.join(&entry.address).with_extension("log");
5804                let address = entry.address.clone();
5805
5806                // Subscribe to logs using PubsubClient
5807                let (client, receiver) = match PubsubClient::logs_subscribe(
5808                    &ws_url,
5809                    RpcTransactionLogsFilter::Mentions(vec![address.clone()]),
5810                    RpcTransactionLogsConfig {
5811                        commitment: Some(CommitmentConfig::confirmed()),
5812                    },
5813                ) {
5814                    Ok(result) => result,
5815                    Err(e) => {
5816                        eprintln!(
5817                            "Warning: Failed to subscribe to logs for genesis program {}: {}",
5818                            entry.address, e
5819                        );
5820                        continue;
5821                    }
5822                };
5823
5824                // Spawn thread to write logs to file
5825                spawn_log_receiver_thread(receiver, log_file_path);
5826
5827                handles.push(LogStreamHandle {
5828                    subscription: client,
5829                });
5830            }
5831        }
5832    }
5833
5834    Ok(handles)
5835}
5836
5837fn start_surfpool_validator(
5838    flags: Option<Vec<String>>,
5839    surfpool_config: &Option<SurfpoolConfig>,
5840    full_simnet_mode: bool,
5841) -> Result<Child> {
5842    let (host, port) = match surfpool_config {
5843        Some(SurfpoolConfig { host, rpc_port, .. }) => (host.clone(), *rpc_port),
5844        _ => (SURFPOOL_HOST.to_string(), DEFAULT_RPC_PORT),
5845    };
5846    let rpc_url = surfpool_rpc_url(surfpool_config);
5847
5848    if std::net::TcpStream::connect_timeout(
5849        &format!("{host}:{port}")
5850            .parse()
5851            .map_err(|err| anyhow!("invalid surfpool host:port `{host}:{port}`: {err}"))?,
5852        std::time::Duration::from_millis(200),
5853    )
5854    .is_ok()
5855    {
5856        return Err(anyhow!(
5857            "port {port} on {host} is already in use - another validator is running there. Kill \
5858             it or set `[surfpool] rpc_port = N` in Anchor.toml to pick a free port."
5859        ));
5860    }
5861
5862    let test_validator_stdout = match full_simnet_mode {
5863        true => Stdio::inherit(),
5864        false => Stdio::null(),
5865    };
5866
5867    let mut validator_handle = std::process::Command::new("surfpool")
5868        .arg("start")
5869        .args(flags.unwrap_or_default())
5870        .stdout(test_validator_stdout)
5871        .stderr(Stdio::inherit())
5872        .spawn()
5873        .map_err(|e| anyhow!("Failed to spawn `surfpool`: {e}"))?;
5874
5875    let client = create_client(rpc_url.clone());
5876
5877    let mut count = 0;
5878
5879    let ms_wait = surfpool_config
5880        .as_ref()
5881        .map(|surfpool| surfpool.startup_wait)
5882        .unwrap_or(STARTUP_WAIT);
5883
5884    while count < ms_wait {
5885        if let Ok(Some(status)) = validator_handle.try_wait() {
5886            return Err(anyhow!(
5887                "`surfpool` exited during startup with {status} - see the stderr output above. \
5888                 Common causes: port {port} in use, missing deploy artifacts in `target/deploy/`, \
5889                 invalid Anchor.toml config."
5890            ));
5891        }
5892        let r = client.get_latest_blockhash();
5893        if r.is_ok() {
5894            break;
5895        }
5896        std::thread::sleep(std::time::Duration::from_millis(100));
5897        count += 100;
5898    }
5899
5900    if count >= ms_wait {
5901        eprintln!(
5902            "Unable to get latest blockhash. Surfpool validator does not look started. Check \
5903             .surfpool/logs/ directory for errors. Consider increasing [surfpool.startup_wait] in \
5904             Anchor.toml."
5905        );
5906        validator_handle.kill()?;
5907        std::process::exit(1);
5908    }
5909
5910    loop {
5911        let resp = client
5912            .send::<RpcResponse<SurfnetInfoResponse>>(
5913                RpcRequest::Custom {
5914                    method: "surfnet_getSurfnetInfo",
5915                },
5916                serde_json::Value::Null,
5917            )?
5918            .value;
5919
5920        // break out if all runbooks are completed
5921        if resp
5922            .runbook_executions
5923            .iter()
5924            .all(|ex| ex.completed_at.is_some())
5925        {
5926            break;
5927        }
5928        std::thread::sleep(std::time::Duration::from_millis(500));
5929    }
5930    Ok(validator_handle)
5931}
5932
5933fn start_solana_test_validator(
5934    cfg: &WithPath<Config>,
5935    test_validator: &Option<TestValidator>,
5936    flags: Option<Vec<String>>,
5937    test_log_stdout: bool,
5938) -> Result<Child> {
5939    let (test_ledger_directory, test_ledger_log_filename) =
5940        test_validator_file_paths(test_validator)?;
5941
5942    // Start a validator for testing.
5943    let (test_validator_stdout, test_validator_stderr) = match test_log_stdout {
5944        true => {
5945            let test_validator_stdout_file =
5946                File::create(&test_ledger_log_filename).with_context(|| {
5947                    format!(
5948                        "Failed to create validator log file {}",
5949                        test_ledger_log_filename.display()
5950                    )
5951                })?;
5952            let test_validator_sterr_file = test_validator_stdout_file.try_clone()?;
5953            (
5954                Stdio::from(test_validator_stdout_file),
5955                Stdio::from(test_validator_sterr_file),
5956            )
5957        }
5958        false => (Stdio::inherit(), Stdio::inherit()),
5959    };
5960
5961    let rpc_url = test_validator_rpc_url(test_validator);
5962
5963    let rpc_port = cfg
5964        .test_validator
5965        .as_ref()
5966        .and_then(|test| test.validator.as_ref().map(|v| v.rpc_port))
5967        .unwrap_or(DEFAULT_RPC_PORT);
5968    if !portpicker::is_free(rpc_port) {
5969        return Err(anyhow!(
5970            "Your configured rpc port: {rpc_port} is already in use"
5971        ));
5972    }
5973    let faucet_port = cfg
5974        .test_validator
5975        .as_ref()
5976        .and_then(|test| test.validator.as_ref().and_then(|v| v.faucet_port))
5977        .unwrap_or(DEFAULT_FAUCET_PORT);
5978    if !portpicker::is_free(faucet_port) {
5979        return Err(anyhow!(
5980            "Your configured faucet port: {faucet_port} is already in use"
5981        ));
5982    }
5983    let program_ids: Vec<Pubkey> = flags
5984        .as_deref()
5985        .unwrap_or(&[])
5986        .windows(2)
5987        .filter_map(|w| {
5988            if w[0] == "--bpf-program" || w[0] == "--upgradeable-program" {
5989                w[1].parse::<Pubkey>().ok()
5990            } else {
5991                None
5992            }
5993        })
5994        .collect();
5995
5996    let mut validator_handle = std::process::Command::new("solana-test-validator")
5997        .arg("--ledger")
5998        .arg(test_ledger_directory)
5999        .arg("--mint")
6000        .arg(cfg.wallet_kp()?.pubkey().to_string())
6001        .args(flags.unwrap_or_default())
6002        .stdout(test_validator_stdout)
6003        .stderr(test_validator_stderr)
6004        .spawn()
6005        .map_err(|e| anyhow!("Failed to spawn `solana-test-validator`: {e}"))?;
6006
6007    // Wait for the validator to be ready.
6008    let client = create_client(rpc_url);
6009    let ms_wait = test_validator
6010        .as_ref()
6011        .map(|test| test.startup_wait)
6012        .unwrap_or(STARTUP_WAIT);
6013
6014    if let Err(e) = wait_for_validator_ready(&client, ms_wait, &program_ids) {
6015        eprintln!(
6016            "Test validator setup failed: {e}. Check {test_ledger_log_filename:?} for errors. \
6017             Consider increasing [test.startup_wait] in Anchor.toml."
6018        );
6019        validator_handle.kill()?;
6020        std::process::exit(1);
6021    }
6022    Ok(validator_handle)
6023}
6024
6025fn wait_for_validator_ready(
6026    client: &RpcClient,
6027    ms_wait: u64,
6028    program_ids: &[Pubkey],
6029) -> Result<()> {
6030    let start = std::time::Instant::now();
6031    let max_wait = std::time::Duration::from_millis(ms_wait);
6032
6033    // Wait for RPC to be up
6034    while client.get_latest_blockhash().is_err() {
6035        if start.elapsed() >= max_wait {
6036            return Err(anyhow!(
6037                "Timeout waiting for validator to start (RPC not ready)"
6038            ));
6039        }
6040        std::thread::sleep(std::time::Duration::from_millis(100));
6041    }
6042
6043    // Wait for programs to be deployed and executable
6044    let mut pending: Vec<Pubkey> = program_ids.to_vec();
6045    while !pending.is_empty() {
6046        pending = pending
6047            .chunks(100)
6048            .flat_map(|chunk| match client.get_multiple_accounts(chunk) {
6049                Ok(accounts) => chunk
6050                    .iter()
6051                    .zip(accounts.iter())
6052                    .filter(|(_, acc)| !acc.as_ref().is_some_and(|a| a.executable))
6053                    .map(|(pk, _)| *pk)
6054                    .collect::<Vec<_>>(),
6055                Err(_) => chunk.to_vec(),
6056            })
6057            .collect();
6058        if start.elapsed() >= max_wait {
6059            return Err(anyhow!("Timeout waiting for programs to deploy"));
6060        }
6061        std::thread::sleep(std::time::Duration::from_millis(100));
6062    }
6063
6064    Ok(())
6065}
6066
6067// Return the URL that solana-test-validator should be running on given the
6068// configuration
6069fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
6070    match test_validator {
6071        Some(TestValidator {
6072            validator: Some(validator),
6073            ..
6074        }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
6075        _ => "http://127.0.0.1:8899".to_string(),
6076    }
6077}
6078
6079// Returns the URL that surfpool should be running for the given configuration
6080fn surfpool_rpc_url(surfpool_config: &Option<SurfpoolConfig>) -> String {
6081    match surfpool_config {
6082        Some(SurfpoolConfig { host, rpc_port, .. }) => format!("http://{}:{}", host, rpc_port),
6083        _ => format!("http://{}:{}", SURFPOOL_HOST, DEFAULT_RPC_PORT),
6084    }
6085}
6086
6087// Setup and return paths to the solana-test-validator ledger directory and log
6088// files given the configuration
6089fn test_validator_file_paths(test_validator: &Option<TestValidator>) -> Result<(PathBuf, PathBuf)> {
6090    let ledger_path = match test_validator {
6091        Some(TestValidator {
6092            validator: Some(validator),
6093            ..
6094        }) => PathBuf::from(&validator.ledger),
6095        _ => get_default_ledger_path(),
6096    };
6097
6098    if !ledger_path.is_relative() {
6099        // Prevent absolute paths to avoid someone using / or similar, as the
6100        // directory gets removed
6101        eprintln!("Ledger directory {ledger_path:?} must be relative");
6102        std::process::exit(1);
6103    }
6104    if ledger_path.exists() {
6105        fs::remove_dir_all(&ledger_path).with_context(|| {
6106            format!(
6107                "Failed to remove ledger directory {}",
6108                ledger_path.display()
6109            )
6110        })?;
6111    }
6112
6113    fs::create_dir_all(&ledger_path).with_context(|| {
6114        format!(
6115            "Failed to create ledger directory {}",
6116            ledger_path.display()
6117        )
6118    })?;
6119
6120    let log_path = ledger_path.join("test-ledger-log.txt");
6121    Ok((ledger_path, log_path))
6122}
6123
6124pub(crate) fn cluster_url(
6125    cfg: &Config,
6126    test_validator: &Option<TestValidator>,
6127    surfpool_config: &Option<SurfpoolConfig>,
6128) -> String {
6129    let is_localnet = cfg.provider.cluster == Cluster::Localnet;
6130    match is_localnet {
6131        // Cluster is Localnet, determine which validator to use
6132        true => match &cfg.validator {
6133            Some(ValidatorType::Surfpool) => surfpool_rpc_url(surfpool_config),
6134            Some(ValidatorType::Legacy) | None => test_validator_rpc_url(test_validator),
6135        },
6136        false => cfg.provider.cluster.url().to_string(),
6137    }
6138}
6139
6140fn clean(cfg_override: &ConfigOverride) -> Result<()> {
6141    // Get workspace root - either from Anchor.toml or use current directory
6142    let workspace_root = if let Ok(Some(cfg)) = Config::discover(cfg_override) {
6143        cfg.path()
6144            .parent()
6145            .expect("Invalid Anchor.toml")
6146            .to_path_buf()
6147    } else {
6148        // No Anchor.toml - use current directory for Cargo workspace
6149        std::env::current_dir()?
6150    };
6151
6152    let dot_anchor_dir = workspace_root.join(".anchor");
6153    let target_dir = crate::target_dir()?;
6154    let deploy_dir = target_dir.join("deploy");
6155
6156    if dot_anchor_dir.exists() {
6157        fs::remove_dir_all(&dot_anchor_dir)
6158            .map_err(|e| anyhow!("Could not remove directory {:?}: {}", dot_anchor_dir, e))?;
6159    }
6160
6161    if target_dir.exists() {
6162        for entry in fs::read_dir(target_dir)? {
6163            let path = entry?.path();
6164            if path.is_dir() && path != deploy_dir {
6165                fs::remove_dir_all(&path)
6166                    .map_err(|e| anyhow!("Could not remove directory {}: {}", path.display(), e))?;
6167            } else if path.is_file() {
6168                fs::remove_file(&path)
6169                    .map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
6170            }
6171        }
6172    } else {
6173        println!("skipping target directory: not found")
6174    }
6175
6176    if deploy_dir.exists() {
6177        for file in fs::read_dir(deploy_dir)? {
6178            let path = file?.path();
6179            if path.extension() != Some(&OsString::from("json")) {
6180                fs::remove_file(&path)
6181                    .map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
6182            }
6183        }
6184    } else {
6185        println!("skipping deploy directory: not found")
6186    }
6187
6188    Ok(())
6189}
6190
6191fn deploy(
6192    cfg_override: &ConfigOverride,
6193    program_name: Option<String>,
6194    program_keypair: Option<PathBuf>,
6195    verifiable: bool,
6196    no_idl: bool,
6197    solana_args: Vec<String>,
6198) -> Result<()> {
6199    // Execute the code within the workspace
6200    with_workspace(cfg_override, |cfg| -> Result<()> {
6201        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
6202        let keypair = cfg.provider.wallet.to_string();
6203
6204        cfg.run_hooks(HookType::PreDeploy)?;
6205        // Deploy the programs.
6206        println!("Deploying cluster: {url}");
6207        println!("Upgrade authority: {keypair}");
6208
6209        for program in cfg.get_programs(program_name)? {
6210            let binary_path = program.binary_path(verifiable)?;
6211
6212            println!("Deploying program {:?}...", program.lib_name);
6213            println!("Program path: {}...", binary_path.display());
6214
6215            let program_keypair_filepath = match program_keypair.as_ref() {
6216                Some(path) => path.clone(),
6217                None => program.keypair_file()?.path().clone(),
6218            };
6219
6220            // Deploy using our native implementation
6221            program::program_deploy(
6222                cfg_override,
6223                Some(strip_workspace_prefix(binary_path)),
6224                None, // program_name - not needed since we have filepath
6225                Some(strip_workspace_prefix(program_keypair_filepath)),
6226                None,  // upgrade_authority - uses wallet from config
6227                None,  // program_id - derived from program_keypair
6228                None,  // buffer
6229                None,  // max_len
6230                false, // use_rpc
6231                no_idl,
6232                false, // make_final
6233                solana_args.clone(),
6234            )?;
6235        }
6236
6237        println!("Deploy success");
6238        cfg.run_hooks(HookType::PostDeploy)?;
6239
6240        Ok(())
6241    })?
6242}
6243
6244fn upgrade(
6245    cfg_override: &ConfigOverride,
6246    program_id: Pubkey,
6247    program_filepath: PathBuf,
6248    max_retries: u32,
6249    solana_args: Vec<String>,
6250) -> Result<()> {
6251    // Use our native upgrade implementation
6252    program::program_upgrade(
6253        cfg_override,
6254        program_id,
6255        Some(program_filepath),
6256        None, // program_name - not needed since we have filepath
6257        None, // buffer
6258        None, // upgrade_authority - uses wallet from config
6259        max_retries,
6260        false, // use_rpc
6261        solana_args,
6262    )
6263}
6264
6265fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
6266    with_workspace(cfg_override, |cfg| -> Result<()> {
6267        println!("Running migration deploy script");
6268
6269        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
6270        let cur_dir = std::env::current_dir()?;
6271        let migrations_dir = cur_dir.join("migrations");
6272        let deploy_ts = Path::new("deploy.ts");
6273
6274        let use_ts = Path::new("tsconfig.json").exists() && migrations_dir.join(deploy_ts).exists();
6275
6276        if !Path::new(".anchor").exists() {
6277            fs::create_dir(".anchor")?;
6278        }
6279        std::env::set_current_dir(".anchor")?;
6280
6281        let exit = if use_ts {
6282            let module_path = migrations_dir.join(deploy_ts);
6283            let deploy_script_host_str =
6284                template::deploy_ts_script_host(&url, &module_path.display().to_string());
6285            fs::write(deploy_ts, deploy_script_host_str)?;
6286
6287            let pkg_manager_cmd =
6288                resolve_package_manager(cfg.toolchain.package_manager.clone())?.to_string();
6289
6290            std::process::Command::new(pkg_manager_cmd)
6291                .args([
6292                    "run",
6293                    "ts-node",
6294                    &fs::canonicalize(deploy_ts)?.to_string_lossy(),
6295                ])
6296                .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
6297                .stdout(Stdio::inherit())
6298                .stderr(Stdio::inherit())
6299                .output()?
6300        } else {
6301            let deploy_js = deploy_ts.with_extension("js");
6302            let module_path = migrations_dir.join(&deploy_js);
6303            let deploy_script_host_str =
6304                template::deploy_js_script_host(&url, &module_path.display().to_string());
6305            fs::write(&deploy_js, deploy_script_host_str)?;
6306
6307            std::process::Command::new("node")
6308                .arg(&deploy_js)
6309                .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
6310                .stdout(Stdio::inherit())
6311                .stderr(Stdio::inherit())
6312                .output()?
6313        };
6314
6315        if !exit.status.success() {
6316            eprintln!("Deploy failed.");
6317            std::process::exit(exit.status.code().unwrap());
6318        }
6319
6320        println!("Deploy complete.");
6321        Ok(())
6322    })?
6323}
6324
6325fn set_workspace_dir_or_exit() {
6326    // First try to find Anchor workspace
6327    let d = match Config::discover(&ConfigOverride::default()) {
6328        Err(err) => {
6329            println!("Workspace configuration error: {err}");
6330            std::process::exit(1);
6331        }
6332        Ok(d) => d,
6333    };
6334
6335    match d {
6336        None => {
6337            // No Anchor.toml found - check for Cargo workspace with Solana programs
6338            let current_dir = match std::env::current_dir() {
6339                Ok(dir) => dir,
6340                Err(_) => {
6341                    println!("Unable to determine current directory");
6342                    std::process::exit(1);
6343                }
6344            };
6345
6346            let cargo_toml_path = current_dir.join("Cargo.toml");
6347            if !cargo_toml_path.exists() {
6348                println!(
6349                    "Not in a Solana workspace. This command requires either Anchor.toml or a \
6350                     Cargo workspace with Solana programs."
6351                );
6352                std::process::exit(1);
6353            }
6354
6355            // Check if this is a workspace and has Solana programs
6356            match program::discover_solana_programs(None) {
6357                Ok(programs) if !programs.is_empty() => {
6358                    // Found Solana programs in Cargo workspace - stay in current directory
6359                    // (already in the right place)
6360                }
6361                _ => {
6362                    println!(
6363                        "Not in a Solana workspace. This command requires either Anchor.toml or a \
6364                         Cargo workspace with Solana programs."
6365                    );
6366                    std::process::exit(1);
6367                }
6368            }
6369        }
6370        Some(cfg) => {
6371            // Found Anchor.toml - change to workspace root
6372            match cfg.path().parent() {
6373                None => {
6374                    println!("Unable to make new program");
6375                }
6376                Some(parent) => {
6377                    if std::env::set_current_dir(parent).is_err() {
6378                        println!(
6379                            "Not in a Solana workspace. This command requires either Anchor.toml \
6380                             or a Cargo workspace with Solana programs."
6381                        );
6382                        std::process::exit(1);
6383                    }
6384                }
6385            };
6386        }
6387    }
6388}
6389
6390fn airdrop(cfg_override: &ConfigOverride, amount: f64, pubkey: Option<Pubkey>) -> Result<()> {
6391    // Get cluster URL and wallet path
6392    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
6393
6394    // Create RPC client with confirmed commitment
6395    let client = RpcClient::new_with_commitment(cluster_url, CommitmentConfig::confirmed());
6396
6397    // Determine recipient
6398    let recipient_pubkey = if let Some(pubkey) = pubkey {
6399        pubkey
6400    } else {
6401        // Load keypair from wallet path and get pubkey
6402        let keypair = Keypair::read_from_file(&wallet_path)
6403            .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
6404        keypair.pubkey()
6405    };
6406
6407    // Convert SOL to lamports
6408    let lamports = (amount * 1_000_000_000.0) as u64;
6409    let starting_balance = client
6410        .get_balance_with_commitment(&recipient_pubkey, CommitmentConfig::confirmed())?
6411        .value;
6412
6413    // Get recent blockhash for airdrop
6414    let recent_blockhash = client
6415        .get_latest_blockhash()
6416        .map_err(|e| anyhow!("Failed to get recent blockhash: {}", e))?;
6417
6418    // Request airdrop with blockhash
6419    println!("Requesting airdrop of {} SOL...", amount);
6420    let signature = client
6421        .request_airdrop_with_blockhash(&recipient_pubkey, lamports, &recent_blockhash)
6422        .map_err(|e| anyhow!("Airdrop request failed: {}", e))?;
6423
6424    println!("Signature: {}", signature);
6425
6426    // Wait for confirmation with the same blockhash used for the airdrop
6427    client
6428        .confirm_transaction_with_spinner(&signature, &recent_blockhash, client.commitment())
6429        .map_err(|e| anyhow!("Transaction confirmation failed: {}", e))?;
6430
6431    println!("Airdrop confirmed!");
6432
6433    // Get and display the new balance
6434    let balance = wait_for_airdrop_balance(&client, &recipient_pubkey, starting_balance, lamports)?;
6435    println!("Balance: {}", format_sol(balance));
6436
6437    Ok(())
6438}
6439
6440fn wait_for_airdrop_balance(
6441    client: &RpcClient,
6442    recipient_pubkey: &Pubkey,
6443    starting_balance: u64,
6444    lamports: u64,
6445) -> Result<u64> {
6446    let expected_balance = starting_balance.saturating_add(lamports);
6447    let mut last_balance = starting_balance;
6448
6449    for attempt in 0..10 {
6450        let balance = client
6451            .get_balance_with_commitment(recipient_pubkey, CommitmentConfig::confirmed())?
6452            .value;
6453        if balance >= expected_balance {
6454            return Ok(balance);
6455        }
6456        last_balance = balance;
6457
6458        if attempt < 9 {
6459            std::thread::sleep(std::time::Duration::from_millis(500));
6460        }
6461    }
6462
6463    eprintln!(
6464        "warning: confirmed balance has not reflected the airdrop yet; showing latest confirmed \
6465         balance"
6466    );
6467    Ok(last_balance)
6468}
6469
6470fn cluster(_cmd: ClusterCommand) -> Result<()> {
6471    println!("Cluster Endpoints:\n");
6472    println!("* Mainnet - https://api.mainnet-beta.solana.com");
6473    println!("* Devnet  - https://api.devnet.solana.com");
6474    println!("* Testnet - https://api.testnet.solana.com");
6475    Ok(())
6476}
6477
6478fn config_cmd(cfg_override: &ConfigOverride, cmd: ConfigCommand) -> Result<()> {
6479    match cmd {
6480        ConfigCommand::Get => config_get(cfg_override),
6481        ConfigCommand::Set { url, keypair } => config_set(cfg_override, url, keypair),
6482    }
6483}
6484
6485fn config_get(cfg_override: &ConfigOverride) -> Result<()> {
6486    with_workspace(cfg_override, |cfg| -> Result<()> {
6487        println!("Anchor Configuration:");
6488        println!();
6489        println!("Cluster: {}", cfg.provider.cluster.url());
6490        println!("Wallet:  {}", cfg.provider.wallet);
6491        Ok(())
6492    })?
6493}
6494
6495fn config_set(
6496    cfg_override: &ConfigOverride,
6497    url: Option<String>,
6498    keypair: Option<PathBuf>,
6499) -> Result<()> {
6500    // Find the Anchor.toml file
6501    let anchor_toml_path = match Config::discover(cfg_override)? {
6502        Some(cfg) => cfg.path().parent().unwrap().join("Anchor.toml"),
6503        None => bail!("Not in an Anchor workspace"),
6504    };
6505
6506    // Read the current Anchor.toml
6507    let mut toml_content =
6508        fs::read_to_string(&anchor_toml_path).context("Failed to read Anchor.toml")?;
6509    let mut toml_doc: toml::Value =
6510        toml::from_str(&toml_content).context("Failed to parse Anchor.toml")?;
6511
6512    let mut updated = false;
6513
6514    // Update cluster URL if provided
6515    if let Some(cluster_url) = url {
6516        let expanded_url = match cluster_url.as_str() {
6517            "m" => "https://api.mainnet-beta.solana.com".to_string(),
6518            "d" => "https://api.devnet.solana.com".to_string(),
6519            "t" => "https://api.testnet.solana.com".to_string(),
6520            "l" => "http://127.0.0.1:8899".to_string(),
6521            _ => cluster_url,
6522        };
6523
6524        if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) {
6525            provider.insert(
6526                "cluster".to_string(),
6527                toml::Value::String(expanded_url.clone()),
6528            );
6529            println!("Updated cluster to: {}", expanded_url);
6530            updated = true;
6531        }
6532    }
6533
6534    // Update wallet path if provided
6535    if let Some(keypair_path) = keypair {
6536        let expanded_path = shellexpand::tilde(&keypair_path.to_string_lossy()).to_string();
6537
6538        // Check if the wallet file exists
6539        if !Path::new(&expanded_path).exists() {
6540            eprintln!("Warning: Wallet file does not exist: {}", expanded_path);
6541        }
6542
6543        if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) {
6544            provider.insert(
6545                "wallet".to_string(),
6546                toml::Value::String(expanded_path.clone()),
6547            );
6548            println!("Updated wallet to: {}", expanded_path);
6549            updated = true;
6550        }
6551    }
6552
6553    if updated {
6554        // Write the updated config back to Anchor.toml
6555        toml_content =
6556            toml::to_string_pretty(&toml_doc).context("Failed to serialize Anchor.toml")?;
6557        fs::write(&anchor_toml_path, toml_content).context("Failed to write Anchor.toml")?;
6558        println!("\nConfiguration updated successfully!");
6559    } else {
6560        println!("No changes made. Use --url or --keypair to update settings.");
6561    }
6562
6563    Ok(())
6564}
6565
6566fn shell(cfg_override: &ConfigOverride) -> Result<()> {
6567    with_workspace(cfg_override, |cfg| -> Result<()> {
6568        let programs = {
6569            // Create idl map from all workspace programs.
6570            let mut idls: HashMap<String, Idl> = cfg
6571                .read_all_programs()?
6572                .iter()
6573                .filter(|program| program.idl.is_some())
6574                .map(|program| {
6575                    (
6576                        program.idl.as_ref().unwrap().metadata.name.clone(),
6577                        program.idl.clone().unwrap(),
6578                    )
6579                })
6580                .collect();
6581            // Insert all manually specified idls into the idl map.
6582            if let Some(programs) = cfg.programs.get(&cfg.provider.cluster) {
6583                let _ = programs
6584                    .iter()
6585                    .map(|(name, pd)| {
6586                        if let Some(idl_fp) = &pd.idl {
6587                            let file_str =
6588                                fs::read_to_string(idl_fp).expect("Unable to read IDL file");
6589                            let idl = serde_json::from_str(&file_str).expect("Idl not readable");
6590                            idls.insert(name.clone(), idl);
6591                        }
6592                    })
6593                    .collect::<Vec<_>>();
6594            }
6595
6596            // Finalize program list with all programs with IDLs.
6597            match cfg.programs.get(&cfg.provider.cluster) {
6598                None => Vec::new(),
6599                Some(programs) => programs
6600                    .iter()
6601                    .filter_map(|(name, program_deployment)| {
6602                        Some(ProgramWorkspace {
6603                            name: name.to_string(),
6604                            program_id: program_deployment.address,
6605                            idl: match idls.get(name) {
6606                                None => return None,
6607                                Some(idl) => idl.clone(),
6608                            },
6609                        })
6610                    })
6611                    .collect::<Vec<ProgramWorkspace>>(),
6612            }
6613        };
6614        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
6615        let js_code = template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?;
6616        let mut child = std::process::Command::new("node")
6617            .args(["-e", &js_code, "-i", "--experimental-repl-await"])
6618            .stdout(Stdio::inherit())
6619            .stderr(Stdio::inherit())
6620            .spawn()
6621            .map_err(|e| anyhow::format_err!("{}", e))?;
6622
6623        if !child.wait()?.success() {
6624            println!("Error running node shell");
6625            return Ok(());
6626        }
6627        Ok(())
6628    })?
6629}
6630
6631fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec<String>) -> Result<()> {
6632    with_workspace(cfg_override, |cfg| -> Result<()> {
6633        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
6634        let script_cmd = cfg.scripts.get(&script).ok_or_else(|| {
6635            let mut available_scripts: Vec<String> = cfg.scripts.keys().cloned().collect();
6636            available_scripts.sort();
6637            if available_scripts.is_empty() {
6638                anyhow!("Script '{script}' not found. No scripts defined in Anchor.toml.")
6639            } else {
6640                anyhow!(
6641                    "Script '{script}' not found.\n\nAvailable scripts:\n  {}",
6642                    available_scripts.join("\n  ")
6643                )
6644            }
6645        })?;
6646        let script_with_args = format!("{script_cmd} {}", script_args.join(" "));
6647        let exit = std::process::Command::new("bash")
6648            .arg("-c")
6649            .arg(&script_with_args)
6650            .env("ANCHOR_PROVIDER_URL", url)
6651            .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
6652            .stdout(Stdio::inherit())
6653            .stderr(Stdio::inherit())
6654            .output()
6655            .unwrap();
6656        if !exit.status.success() {
6657            std::process::exit(exit.status.code().unwrap_or(1));
6658        }
6659        Ok(())
6660    })?
6661}
6662
6663fn keys(cfg_override: &ConfigOverride, cmd: KeysCommand) -> Result<()> {
6664    match cmd {
6665        KeysCommand::List => keys_list(cfg_override),
6666        KeysCommand::Sync { program_name } => keys_sync(cfg_override, program_name),
6667    }
6668}
6669
6670fn keys_list(cfg_override: &ConfigOverride) -> Result<()> {
6671    with_workspace(cfg_override, |cfg| -> Result<()> {
6672        for program in cfg.read_all_programs()? {
6673            let pubkey = program.pubkey()?;
6674            println!("{}: {}", program.lib_name, pubkey);
6675        }
6676        Ok(())
6677    })?
6678}
6679
6680/// Sync program `declare_id!` pubkeys with the pubkey from `target/deploy/<KEYPAIR>.json`.
6681fn keys_sync(cfg_override: &ConfigOverride, program_name: Option<String>) -> Result<()> {
6682    with_workspace(cfg_override, |cfg| -> Result<()> {
6683        let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
6684            .multi_line(true)
6685            .build()
6686            .unwrap();
6687
6688        let cfg_cluster = cfg.provider.cluster.to_owned();
6689        println!("Syncing program ids for the configured cluster ({cfg_cluster})\n");
6690
6691        let mut changed_src = false;
6692        for program in cfg.get_programs(program_name)? {
6693            // Get the pubkey from the keypair file
6694            let actual_program_id = program.pubkey()?.to_string();
6695
6696            // Handle declaration in program files
6697            let src_path = program.path.join("src");
6698            let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
6699
6700            for path in files_to_check {
6701                let mut content = match fs::read_to_string(&path) {
6702                    Ok(content) => content,
6703                    Err(_) => continue,
6704                };
6705
6706                let incorrect_program_id = declare_id_regex
6707                    .captures(&content)
6708                    .and_then(|captures| captures.get(3))
6709                    .filter(|program_id_match| program_id_match.as_str() != actual_program_id);
6710                if let Some(program_id_match) = incorrect_program_id {
6711                    println!("Found incorrect program id declaration in {path:?}");
6712
6713                    // Update the program id
6714                    content.replace_range(program_id_match.range(), &actual_program_id);
6715                    fs::write(&path, content)?;
6716
6717                    changed_src = true;
6718                    println!("Updated to {actual_program_id}\n");
6719                    break;
6720                }
6721            }
6722
6723            // Handle declaration in Anchor.toml
6724            'outer: for (cluster, programs) in &mut cfg.programs {
6725                // Only change if the configured cluster matches the program's cluster
6726                if cluster != &cfg_cluster {
6727                    continue;
6728                }
6729
6730                for (name, deployment) in programs {
6731                    // Skip other programs
6732                    if name != &program.lib_name {
6733                        continue;
6734                    }
6735
6736                    if deployment.address.to_string() != actual_program_id {
6737                        println!(
6738                            "Found incorrect program id declaration in Anchor.toml for the \
6739                             program `{name}`"
6740                        );
6741
6742                        // Update the program id
6743                        deployment.address = Pubkey::try_from(actual_program_id.as_str()).unwrap();
6744                        fs::write(cfg.path(), cfg.to_string())?;
6745
6746                        println!("Updated to {actual_program_id}\n");
6747                        break 'outer;
6748                    }
6749                }
6750            }
6751        }
6752
6753        println!("All program id declarations are synced.");
6754        if changed_src {
6755            println!("Please rebuild the program to update the generated artifacts.")
6756        }
6757
6758        Ok(())
6759    })?
6760}
6761
6762enum ProgramIdComparison {
6763    Same,
6764    Mismatch {
6765        lib_name: String,
6766        actual_id: String,
6767        declared_id: String,
6768    },
6769}
6770
6771/// Check if there's a mismatch between the program keypair and the `declare_id!` in the source code.
6772/// Returns `false` if there is a mismatch.
6773fn check_program_id_mismatch(
6774    cfg: &WithPath<Config>,
6775    program_name: Option<String>,
6776) -> Result<ProgramIdComparison> {
6777    let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
6778        .multi_line(true)
6779        .build()
6780        .unwrap();
6781
6782    for program in cfg.get_programs(program_name)? {
6783        // Get the pubkey from the keypair file
6784        let actual_program_id = program.pubkey()?.to_string();
6785
6786        // Check declaration in program files
6787        let src_path = program.path.join("src");
6788        let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
6789
6790        for path in files_to_check {
6791            let content = match fs::read_to_string(&path) {
6792                Ok(content) => content,
6793                Err(_) => continue,
6794            };
6795
6796            let incorrect_program_id = declare_id_regex
6797                .captures(&content)
6798                .and_then(|captures| captures.get(3))
6799                .filter(|program_id_match| program_id_match.as_str() != actual_program_id);
6800
6801            if let Some(program_id_match) = incorrect_program_id {
6802                return Ok(ProgramIdComparison::Mismatch {
6803                    lib_name: program.lib_name,
6804                    actual_id: actual_program_id,
6805                    declared_id: program_id_match.as_str().to_string(),
6806                });
6807            }
6808        }
6809    }
6810
6811    Ok(ProgramIdComparison::Same)
6812}
6813
6814#[allow(clippy::too_many_arguments)]
6815fn localnet(
6816    cfg_override: &ConfigOverride,
6817    skip_build: bool,
6818    skip_deploy: bool,
6819    skip_lint: bool,
6820    ignore_keys: bool,
6821    validator_type: ValidatorType,
6822    env_vars: Vec<String>,
6823    cargo_args: Vec<String>,
6824) -> Result<()> {
6825    with_workspace(cfg_override, |cfg| -> Result<()> {
6826        // Build if needed.
6827        if !skip_build {
6828            build(
6829                cfg_override,
6830                false,
6831                None,
6832                None,
6833                false,
6834                skip_lint,
6835                ignore_keys,
6836                None,
6837                None,
6838                None,
6839                BootstrapMode::None,
6840                BuildSbfOptions::default(),
6841                None,
6842                None,
6843                env_vars,
6844                cargo_args,
6845                false,
6846            )?;
6847        }
6848
6849        let generated_accounts = generated_validator_accounts(cfg, &cfg.test_validator)?;
6850        let validator_handle: Option<Child> = match validator_type {
6851            ValidatorType::Surfpool => {
6852                let full_simnet_mode = true;
6853                let flags = Some(surfpool_flags(
6854                    cfg,
6855                    &cfg.surfpool_config,
6856                    full_simnet_mode,
6857                    skip_deploy,
6858                    None,
6859                    &generated_accounts,
6860                )?);
6861                Some(start_surfpool_validator(
6862                    flags,
6863                    &cfg.surfpool_config,
6864                    full_simnet_mode,
6865                )?)
6866            }
6867            ValidatorType::Legacy => {
6868                let flags = Some(validator_flags(
6869                    cfg,
6870                    &cfg.test_validator,
6871                    skip_deploy,
6872                    &generated_accounts,
6873                )?);
6874                Some(start_solana_test_validator(
6875                    cfg,
6876                    &cfg.test_validator,
6877                    flags,
6878                    false,
6879                )?)
6880            }
6881        };
6882
6883        // Setup log reader.
6884        let url = test_validator_rpc_url(&cfg.test_validator);
6885        let log_streams = match stream_logs(cfg, &url) {
6886            Ok(streams) => {
6887                println!(
6888                    "Log streams set up successfully ({} streams)",
6889                    streams.len()
6890                );
6891                Some(streams)
6892            }
6893            Err(e) => {
6894                eprintln!("Warning: Failed to setup program log streaming: {:#}", e);
6895                eprintln!("  Program logs will still be visible in the validator output.");
6896                None
6897            }
6898        };
6899
6900        std::io::stdin().lock().lines().next().unwrap().unwrap();
6901
6902        // Check all errors and shut down.
6903        if let Some(mut handle) = validator_handle {
6904            if let Err(err) = handle.kill() {
6905                println!("Failed to kill subprocess {}: {}", handle.id(), err);
6906            }
6907        }
6908
6909        // Explicitly shutdown log streams - closes WebSocket subscriptions
6910        if let Some(log_streams) = log_streams {
6911            for handle in log_streams {
6912                handle.shutdown();
6913            }
6914        }
6915
6916        Ok(())
6917    })?
6918}
6919
6920/// Return the cargo build artifacts directory. The successful result is
6921/// cached.
6922pub fn target_dir() -> Result<&'static Path> {
6923    static TARGET_DIR: OnceLock<PathBuf> = OnceLock::new();
6924    if let Some(path) = TARGET_DIR.get() {
6925        return Ok(path.as_path());
6926    }
6927    let path = target_dir_no_cache()?;
6928    let _ = TARGET_DIR.set(path);
6929    Ok(TARGET_DIR.get().expect("just set").as_path())
6930}
6931
6932/// Return the cargo build artifacts directory.
6933fn target_dir_no_cache() -> Result<PathBuf> {
6934    // `cargo metadata` produces a JSON blob from which we extract the
6935    // `target_directory` field.
6936    let output = std::process::Command::new("cargo")
6937        .args(["metadata", "--no-deps", "--format-version=1"])
6938        .output()
6939        .context("Failed to execute 'cargo metadata'")?;
6940
6941    if !output.status.success() {
6942        let stderr_msg = String::from_utf8_lossy(&output.stderr);
6943        bail!("'cargo metadata' failed with: {stderr_msg}");
6944    }
6945
6946    #[derive(Deserialize)]
6947    struct CargoMetadata {
6948        target_directory: PathBuf,
6949    }
6950
6951    let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
6952        .context("Failed to parse 'cargo metadata' output")?;
6953
6954    Ok(metadata.target_directory)
6955}
6956
6957// with_workspace ensures the current working directory is always the top level
6958// workspace directory, i.e., where the `Anchor.toml` file is located, before
6959// and after the closure invocation.
6960//
6961// The closure passed into this function must never change the working directory
6962// to be outside the workspace. Doing so will have undefined behavior.
6963pub(crate) fn with_workspace<R>(
6964    cfg_override: &ConfigOverride,
6965    f: impl FnOnce(&mut WithPath<Config>) -> R,
6966) -> Result<R> {
6967    set_workspace_dir_or_exit();
6968
6969    let mut cfg = Config::discover(cfg_override)
6970        .map_err(|e| anyhow!("Workspace configuration error: {}", e))?
6971        .ok_or_else(|| anyhow!("This command requires an Anchor workspace."))?;
6972
6973    let r = f(&mut cfg);
6974
6975    set_workspace_dir_or_exit();
6976
6977    Ok(r)
6978}
6979
6980fn is_hidden(entry: &walkdir::DirEntry) -> bool {
6981    entry
6982        .file_name()
6983        .to_str()
6984        .map(|s| s == "." || s.starts_with('.') || s == "target")
6985        .unwrap_or(false)
6986}
6987
6988fn logs_websocket_url(cfg_override: &ConfigOverride, cluster_url: &str) -> String {
6989    let ws_scheme_url = cluster_url
6990        .replace("https://", "wss://")
6991        .replace("http://", "ws://");
6992
6993    let is_local = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
6994    if !is_local {
6995        return ws_scheme_url;
6996    }
6997
6998    let default_ws_port = extract_url_port(cluster_url)
6999        .map(|port| port.saturating_add(1))
7000        .unwrap_or(DEFAULT_RPC_PORT + 1);
7001    let ws_port = Config::discover(cfg_override)
7002        .ok()
7003        .flatten()
7004        .and_then(|cfg| {
7005            cfg.surfpool_config
7006                .as_ref()
7007                .and_then(|surfpool| surfpool.ws_port)
7008        })
7009        .unwrap_or(default_ws_port);
7010
7011    replace_url_port(&ws_scheme_url, ws_port)
7012}
7013
7014fn extract_url_port(url: &str) -> Option<u16> {
7015    let (_, after_scheme) = url.split_once("://")?;
7016    let host_port_end = after_scheme.find('/').unwrap_or(after_scheme.len());
7017    let (_, port_str) = after_scheme[..host_port_end].rsplit_once(':')?;
7018    port_str.parse().ok()
7019}
7020
7021fn replace_url_port(url: &str, new_port: u16) -> String {
7022    let Some((scheme, rest)) = url.split_once("://") else {
7023        return url.to_string();
7024    };
7025    let (host_port_part, tail) = match rest.find('/') {
7026        Some(index) => (&rest[..index], &rest[index..]),
7027        None => (rest, ""),
7028    };
7029    let host = host_port_part
7030        .rsplit_once(':')
7031        .map(|(host, _)| host)
7032        .unwrap_or(host_port_part);
7033    format!("{scheme}://{host}:{new_port}{tail}")
7034}
7035
7036fn get_node_version() -> Result<Version> {
7037    let node_version = std::process::Command::new("node")
7038        .arg("--version")
7039        .stderr(Stdio::inherit())
7040        .output()
7041        .map_err(|e| anyhow::format_err!("node failed: {}", e))?;
7042    parse_node_version(std::str::from_utf8(&node_version.stdout)?)
7043}
7044
7045fn parse_node_version(output: &str) -> Result<Version> {
7046    let trimmed = output.trim();
7047    let without_v = trimmed.strip_prefix('v').unwrap_or(trimmed);
7048    Version::parse(without_v).map_err(Into::into)
7049}
7050
7051/// Default re-sign attempts when blockhashes expire mid-deploy.
7052/// Matches Agave's `solana program deploy` default (agave/cli/src/program.rs).
7053/// Each blockhash window is ~60s, so 5 → ~5 minutes of resign budget.
7054pub const DEFAULT_MAX_SIGN_ATTEMPTS: usize = 5;
7055
7056fn add_recommended_deployment_solana_args(
7057    client: &RpcClient,
7058    args: Vec<String>,
7059    write_locked_accounts: &[Pubkey],
7060) -> Result<Vec<String>> {
7061    let mut augmented_args = args.clone();
7062
7063    // If no priority fee is provided, calculate a recommended fee based on recent txs.
7064    if !args.contains(&"--with-compute-unit-price".to_string()) {
7065        let priority_fee = get_recommended_micro_lamport_fee(client, write_locked_accounts);
7066        augmented_args.push("--with-compute-unit-price".to_string());
7067        augmented_args.push(priority_fee.to_string());
7068    }
7069
7070    if !args.contains(&"--max-sign-attempts".to_string()) {
7071        augmented_args.push("--max-sign-attempts".to_string());
7072        augmented_args.push(DEFAULT_MAX_SIGN_ATTEMPTS.to_string());
7073    }
7074
7075    // Note: `--buffer` injection is handled by callers (program_deploy /
7076    // program_upgrade) so the path can be scoped per program
7077    // (`target/deploy/{name}-upgrade-buffer.json`). Doing it here would either
7078    // collide across concurrent program deploys or require threading
7079    // program_name down into this fee/sign-attempts helper.
7080
7081    Ok(augmented_args)
7082}
7083
7084fn get_node_dns_option() -> &'static str {
7085    let Ok(version) = get_node_version() else {
7086        return "";
7087    };
7088    let req = VersionReq::parse(">=16.4.0").unwrap();
7089    if req.matches(&version) {
7090        "--dns-result-order=ipv4first"
7091    } else {
7092        ""
7093    }
7094}
7095
7096// Remove the current workspace directory if it prefixes a string.
7097// This is used as a workaround for the Solana CLI using the uriparse crate to
7098// parse args but not handling percent encoding/decoding when using the path as
7099// a local filesystem path. Removing the workspace prefix handles most/all cases
7100// of spaces in keypair/binary paths, but this should be fixed in the Solana CLI
7101// and removed here.
7102fn strip_workspace_prefix(absolute_path: PathBuf) -> PathBuf {
7103    let workspace_prefix = std::env::current_dir().unwrap();
7104    absolute_path
7105        .strip_prefix(&workspace_prefix)
7106        .unwrap_or(&absolute_path)
7107        .into()
7108}
7109
7110/// Create a new [`RpcClient`] with `confirmed` commitment level instead of the default(finalized).
7111pub(crate) fn create_client<U: ToString>(url: U) -> RpcClient {
7112    RpcClient::new_with_commitment(url, CommitmentConfig::confirmed())
7113}
7114
7115fn address(cfg_override: &ConfigOverride) -> Result<()> {
7116    let (_cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
7117
7118    // Load keypair and get pubkey
7119    let keypair = Keypair::read_from_file(&wallet_path)
7120        .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
7121
7122    // Print the public key
7123    println!("{}", keypair.pubkey());
7124
7125    Ok(())
7126}
7127
7128fn balance(cfg_override: &ConfigOverride, pubkey: Option<Pubkey>, lamports: bool) -> Result<()> {
7129    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
7130
7131    // Create RPC client
7132    let client = RpcClient::new(cluster_url);
7133
7134    // Determine which account to check
7135    let account_pubkey = if let Some(pubkey) = pubkey {
7136        pubkey
7137    } else {
7138        // Load keypair from wallet path and get pubkey
7139        let keypair = Keypair::read_from_file(&wallet_path)
7140            .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
7141        keypair.pubkey()
7142    };
7143
7144    // Get balance
7145    let balance = client.get_balance(&account_pubkey)?;
7146
7147    // Format and display output
7148    if lamports {
7149        println!("{}", balance);
7150    } else {
7151        println!("{}", format_sol(balance));
7152    }
7153
7154    Ok(())
7155}
7156
7157fn epoch(cfg_override: &ConfigOverride) -> Result<()> {
7158    let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
7159
7160    // Create RPC client
7161    let client = RpcClient::new(cluster_url);
7162
7163    // Get epoch info
7164    let epoch_info = client.get_epoch_info()?;
7165
7166    // Print just the epoch number
7167    println!("{}", epoch_info.epoch);
7168
7169    Ok(())
7170}
7171
7172fn epoch_info(cfg_override: &ConfigOverride) -> Result<()> {
7173    let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
7174
7175    // Create RPC client
7176    let client = RpcClient::new(cluster_url);
7177
7178    // Get epoch info
7179    let epoch_info = client.get_epoch_info()?;
7180
7181    // Calculate epoch slot range
7182    let first_slot_in_epoch = epoch_info.absolute_slot - epoch_info.slot_index;
7183    let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch;
7184
7185    // Calculate completion stats
7186    let epoch_completed_percent =
7187        epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100.0;
7188    let remaining_slots = epoch_info.slots_in_epoch - epoch_info.slot_index;
7189
7190    // Display epoch information (matching Solana CLI format)
7191    println!("Block height: {}", epoch_info.block_height);
7192    println!("Slot: {}", epoch_info.absolute_slot);
7193    println!("Epoch: {}", epoch_info.epoch);
7194
7195    if let Some(tx_count) = epoch_info.transaction_count {
7196        println!("Transaction Count: {}", tx_count);
7197    }
7198
7199    println!(
7200        "Epoch Slot Range: [{}..{})",
7201        first_slot_in_epoch, last_slot_in_epoch
7202    );
7203    println!("Epoch Completed Percent: {:>3.3}%", epoch_completed_percent);
7204    println!(
7205        "Epoch Completed Slots: {}/{} ({} remaining)",
7206        epoch_info.slot_index, epoch_info.slots_in_epoch, remaining_slots
7207    );
7208
7209    // Try to calculate epoch completed time
7210    // Get average slot time from performance samples (aggregate up to 60 samples)
7211    if let Ok(samples) = client.get_recent_performance_samples(Some(60)) {
7212        // Aggregate all samples to calculate average slot time
7213        let (total_slots, total_secs) =
7214            samples.iter().fold((0u64, 0u64), |(slots, secs), sample| {
7215                (
7216                    slots.saturating_add(sample.num_slots),
7217                    secs.saturating_add(sample.sample_period_secs as u64),
7218                )
7219            });
7220
7221        if let Some(avg_slot_time_ms) = (total_secs * 1000).checked_div(total_slots) {
7222            // Calculate time_remaining using average slot time (always estimated)
7223            let remaining_secs = (remaining_slots * avg_slot_time_ms) / 1000;
7224
7225            // Calculate time_elapsed - try actual block times first, then estimate
7226            // Get the first actual block in the epoch and adjust for slot differences
7227            let start_block_time = client
7228                .get_blocks_with_limit(first_slot_in_epoch, 1)
7229                .ok()
7230                .and_then(|slots| slots.first().cloned())
7231                .and_then(|first_actual_block| {
7232                    client.get_block_time(first_actual_block).ok().map(|time| {
7233                        // Adjust backwards if first actual block is after expected start
7234                        let slot_diff = first_actual_block.saturating_sub(first_slot_in_epoch);
7235                        let time_adjustment = (slot_diff * avg_slot_time_ms / 1000) as i64;
7236                        time.saturating_sub(time_adjustment)
7237                    })
7238                });
7239
7240            let current_block_time = client.get_block_time(epoch_info.absolute_slot).ok();
7241
7242            let (elapsed_secs, is_estimated) = if let (Some(start_time), Some(current_time)) =
7243                (start_block_time, current_block_time)
7244            {
7245                // Use actual block times for elapsed
7246                ((current_time - start_time) as u64, false)
7247            } else {
7248                // Estimate elapsed using average slot time
7249                ((epoch_info.slot_index * avg_slot_time_ms) / 1000, true)
7250            };
7251
7252            // Total time = elapsed + remaining
7253            let total_secs = elapsed_secs + remaining_secs;
7254
7255            let estimated_marker = if is_estimated { "*" } else { "" };
7256            println!(
7257                "Epoch Completed Time: {}{}/{} ({} remaining)",
7258                format_duration_secs(elapsed_secs),
7259                estimated_marker,
7260                format_duration_secs(total_secs),
7261                format_duration_secs(remaining_secs)
7262            );
7263        }
7264    }
7265
7266    Ok(())
7267}
7268
7269/// Format seconds into human-readable duration (e.g., "1day 5h 49m 8s")
7270fn format_duration_secs(total_seconds: u64) -> String {
7271    let seconds = total_seconds % 60;
7272    let total_minutes = total_seconds / 60;
7273    let minutes = total_minutes % 60;
7274    let total_hours = total_minutes / 60;
7275    let hours = total_hours % 24;
7276    let days = total_hours / 24;
7277
7278    let mut parts = Vec::new();
7279    if days > 0 {
7280        parts.push(format!("{}day", days));
7281    }
7282    if hours > 0 {
7283        parts.push(format!("{}h", hours));
7284    }
7285    if minutes > 0 {
7286        parts.push(format!("{}m", minutes));
7287    }
7288    if seconds > 0 || parts.is_empty() {
7289        parts.push(format!("{}s", seconds));
7290    }
7291
7292    parts.join(" ")
7293}
7294
7295fn logs_subscribe(
7296    cfg_override: &ConfigOverride,
7297    include_votes: bool,
7298    address: Option<Vec<Pubkey>>,
7299) -> Result<()> {
7300    let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
7301    let ws_url = logs_websocket_url(cfg_override, &cluster_url);
7302
7303    println!("Connecting to {}", ws_url);
7304
7305    let filter = match (include_votes, address) {
7306        (true, Some(address)) => {
7307            RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect())
7308        }
7309        (true, None) => RpcTransactionLogsFilter::AllWithVotes,
7310        (false, Some(address)) => {
7311            RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect())
7312        }
7313        (false, None) => RpcTransactionLogsFilter::All,
7314    };
7315
7316    let (_client, receiver) = PubsubClient::logs_subscribe(
7317        &ws_url,
7318        filter,
7319        RpcTransactionLogsConfig {
7320            commitment: cfg_override.commitment.map(|c| CommitmentConfig {
7321                commitment: c.into(),
7322            }),
7323        },
7324    )?;
7325
7326    loop {
7327        match receiver.recv() {
7328            Ok(logs) => {
7329                println!("Transaction executed in slot {}:", logs.context.slot);
7330                println!("  Signature: {}", logs.value.signature);
7331                println!(
7332                    "  Status: {}",
7333                    logs.value
7334                        .err
7335                        .map(|err| err.to_string())
7336                        .unwrap_or_else(|| "Ok".to_string())
7337                );
7338                println!("  Log Messages:");
7339                for log in logs.value.logs {
7340                    println!("    {log}");
7341                }
7342            }
7343            Err(err) => {
7344                return Err(anyhow!("Disconnected: {err}"));
7345            }
7346        }
7347    }
7348}
7349
7350#[cfg(test)]
7351mod tests {
7352    use {
7353        super::*,
7354        anchor_lang_idl::types::{
7355            IdlErrorCode, IdlGenericArg, IdlInstructionAccount, IdlInstructionAccountItem,
7356            IdlMetadata, IdlPda, IdlSeed, IdlSeedAccount, IdlTypeDef, IdlTypeDefGeneric,
7357        },
7358        std::collections::{HashMap, HashSet},
7359        tempfile::tempdir,
7360    };
7361
7362    #[test]
7363    fn test_init_accepts_anchor_version() {
7364        let opts =
7365            Opts::try_parse_from(["anchor", "init", "example", "--anchor-version", "v2"]).unwrap();
7366
7367        let Command::Init { anchor_version, .. } = opts.command else {
7368            panic!("expected init command");
7369        };
7370
7371        assert_eq!(anchor_version, AnchorVersion::V2);
7372    }
7373
7374    #[test]
7375    fn test_new_accepts_anchor_version() {
7376        let opts =
7377            Opts::try_parse_from(["anchor", "new", "example", "--anchor-version", "v2"]).unwrap();
7378
7379        let Command::New { anchor_version, .. } = opts.command else {
7380            panic!("expected new command");
7381        };
7382
7383        assert_eq!(anchor_version, AnchorVersion::V2);
7384    }
7385
7386    #[test]
7387    fn test_build_accepts_build_sbf_options() {
7388        let opts = Opts::try_parse_from([
7389            "anchor",
7390            "build",
7391            "--tools-version",
7392            "v1.57",
7393            "--arch",
7394            "v2",
7395            "--",
7396            "--features",
7397            "extra",
7398        ])
7399        .unwrap();
7400
7401        let Command::Build {
7402            tools_version,
7403            arch,
7404            cargo_args,
7405            ..
7406        } = opts.command
7407        else {
7408            panic!("expected build command");
7409        };
7410
7411        assert_eq!(tools_version, "v1.57");
7412        assert_eq!(arch, "v2");
7413        assert_eq!(cargo_args, ["--features", "extra"].map(str::to_string));
7414    }
7415
7416    #[test]
7417    fn test_build_uses_default_build_sbf_options() {
7418        let opts = Opts::try_parse_from(["anchor", "build"]).unwrap();
7419
7420        let Command::Build {
7421            tools_version,
7422            arch,
7423            ..
7424        } = opts.command
7425        else {
7426            panic!("expected build command");
7427        };
7428
7429        assert_eq!(tools_version, DEFAULT_TOOLS_VERSION);
7430        assert_eq!(arch, default_build_arch());
7431    }
7432
7433    #[test]
7434    fn build_sbf_args_appends_forwarded_args_unchanged() {
7435        let build_sbf_options = BuildSbfOptions::new("v1.57".to_string(), "v2".to_string());
7436        let extra_args = vec![
7437            "--tools-version".to_string(),
7438            "v1.53".to_string(),
7439            "--arch".to_string(),
7440            "v1".to_string(),
7441        ];
7442
7443        let args = build_sbf_args(&build_sbf_options, &extra_args);
7444
7445        assert_eq!(
7446            args,
7447            [
7448                "build-sbf",
7449                "--tools-version",
7450                "v1.57",
7451                "--arch",
7452                "v2",
7453                "--tools-version",
7454                "v1.53",
7455                "--arch",
7456                "v1",
7457            ]
7458            .map(str::to_string)
7459        );
7460    }
7461
7462    #[test]
7463    fn build_sbf_options_from_build_command_uses_explicit_options() {
7464        let build_sbf_options =
7465            BuildSbfOptions::from_build_command("v1.57".to_string(), "v2".to_string());
7466        let expected = ["build-sbf", "--tools-version", "v1.57", "--arch", "v2"]
7467            .map(str::to_string)
7468            .to_vec();
7469
7470        assert_eq!(build_sbf_base_args(&build_sbf_options), expected);
7471    }
7472
7473    #[test]
7474    #[cfg(not(windows))]
7475    fn test_debugger_and_coverage_commands_parse() {
7476        let opts =
7477            Opts::try_parse_from(["anchor", "debugger", "initialize", "--skip-run"]).unwrap();
7478        let Command::Debugger {
7479            test_name,
7480            skip_run,
7481            ..
7482        } = opts.command
7483        else {
7484            panic!("expected debugger command");
7485        };
7486        assert_eq!(test_name.as_deref(), Some("initialize"));
7487        assert!(skip_run);
7488
7489        let opts =
7490            Opts::try_parse_from(["anchor", "coverage", "--skip-run", "--output", "lcov.info"])
7491                .unwrap();
7492        let Command::Coverage {
7493            skip_run, output, ..
7494        } = opts.command
7495        else {
7496            panic!("expected coverage command");
7497        };
7498        assert!(skip_run);
7499        assert_eq!(output, "lcov.info");
7500    }
7501
7502    #[test]
7503    fn test_validator_defaults_to_surfpool() {
7504        let opts = Opts::try_parse_from(["anchor", "test"]).unwrap();
7505        let Command::Test { validator, .. } = opts.command else {
7506            panic!("expected test command");
7507        };
7508        assert_eq!(validator, ValidatorType::Surfpool);
7509
7510        let opts = Opts::try_parse_from(["anchor", "localnet"]).unwrap();
7511        let Command::Localnet { validator, .. } = opts.command else {
7512            panic!("expected localnet command");
7513        };
7514        assert_eq!(validator, ValidatorType::Surfpool);
7515    }
7516
7517    #[test]
7518    fn test_codama_command_parses() {
7519        let opts = Opts::try_parse_from([
7520            "anchor",
7521            "codama",
7522            "generate",
7523            "-l",
7524            "rust,go",
7525            "-p",
7526            "clients",
7527            "target/idl/demo.json",
7528        ])
7529        .unwrap();
7530        let Command::Codama { subcmd } = opts.command else {
7531            panic!("expected codama command");
7532        };
7533        let codama::CodamaCommand::Generate {
7534            language,
7535            path,
7536            idl,
7537        } = subcmd
7538        else {
7539            panic!("expected codama generate command");
7540        };
7541        assert_eq!(language, vec![codama::Language::Rust, codama::Language::Go]);
7542        assert_eq!(path, "clients");
7543        assert_eq!(idl, "target/idl/demo.json");
7544    }
7545
7546    #[test]
7547    #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
7548    fn test_init_reserved_word() {
7549        init(
7550            &ConfigOverride {
7551                cluster: None,
7552                wallet: None,
7553                commitment: None,
7554            },
7555            "await".to_string(),
7556            true,
7557            true,
7558            None,
7559            false,
7560            ProgramTemplate::default(),
7561            AnchorVersion::default(),
7562            TestTemplate::default(),
7563            true,
7564            true,
7565        )
7566        .unwrap();
7567    }
7568
7569    #[test]
7570    #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
7571    fn test_init_reserved_word_from_syn() {
7572        init(
7573            &ConfigOverride {
7574                cluster: None,
7575                wallet: None,
7576                commitment: None,
7577            },
7578            "fn".to_string(),
7579            true,
7580            true,
7581            None,
7582            false,
7583            ProgramTemplate::default(),
7584            AnchorVersion::default(),
7585            TestTemplate::default(),
7586            true,
7587            true,
7588        )
7589        .unwrap();
7590    }
7591
7592    #[test]
7593    #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
7594    fn test_init_starting_with_digit() {
7595        init(
7596            &ConfigOverride {
7597                cluster: None,
7598                wallet: None,
7599                commitment: None,
7600            },
7601            "1project".to_string(),
7602            true,
7603            true,
7604            None,
7605            false,
7606            ProgramTemplate::default(),
7607            AnchorVersion::default(),
7608            TestTemplate::default(),
7609            true,
7610            true,
7611        )
7612        .unwrap();
7613    }
7614
7615    fn index_set(indices: &[usize]) -> HashSet<usize> {
7616        indices.iter().copied().collect()
7617    }
7618
7619    #[test]
7620    fn program_order_prefers_programs_with_more_anchor_program_deps() {
7621        let program_indices = index_set(&[0, 1]);
7622        let program_closures = HashMap::from([(0, index_set(&[1])), (1, index_set(&[]))]);
7623        let original_order = HashMap::from([(0, 0), (1, 1)]);
7624
7625        let ordered = order_program_indices_by_dependency_cache_heuristic(
7626            &program_indices,
7627            &program_closures,
7628            &original_order,
7629        );
7630
7631        assert_eq!(ordered, vec![0, 1]);
7632    }
7633
7634    #[test]
7635    fn program_order_uses_total_dependency_closure_as_tiebreaker() {
7636        let program_indices = index_set(&[0, 1, 2, 3]);
7637        let program_closures = HashMap::from([
7638            (0, index_set(&[2, 4])),
7639            (1, index_set(&[3])),
7640            (2, index_set(&[])),
7641            (3, index_set(&[])),
7642        ]);
7643        let original_order = HashMap::from([(0, 1), (1, 0), (2, 2), (3, 3)]);
7644
7645        let ordered = order_program_indices_by_dependency_cache_heuristic(
7646            &program_indices,
7647            &program_closures,
7648            &original_order,
7649        );
7650
7651        assert_eq!(ordered, vec![0, 1, 2, 3]);
7652    }
7653
7654    #[test]
7655    fn program_order_places_isolated_programs_first() {
7656        let program_indices = index_set(&[0, 1, 2]);
7657        let program_closures = HashMap::from([
7658            (0, index_set(&[])),
7659            (1, index_set(&[2])),
7660            (2, index_set(&[])),
7661        ]);
7662        let original_order = HashMap::from([(0, 0), (1, 1), (2, 2)]);
7663
7664        let ordered = order_program_indices_by_dependency_cache_heuristic(
7665            &program_indices,
7666            &program_closures,
7667            &original_order,
7668        );
7669
7670        assert_eq!(ordered, vec![0, 1, 2]);
7671    }
7672
7673    #[test]
7674    fn program_order_preserves_original_order_for_unrelated_programs() {
7675        let program_indices = index_set(&[0, 1, 2]);
7676        let program_closures = HashMap::from([
7677            (0, index_set(&[3])),
7678            (1, index_set(&[])),
7679            (2, index_set(&[])),
7680        ]);
7681        let original_order = HashMap::from([(0, 0), (1, 1), (2, 2)]);
7682
7683        let ordered = order_program_indices_by_dependency_cache_heuristic(
7684            &program_indices,
7685            &program_closures,
7686            &original_order,
7687        );
7688
7689        assert_eq!(ordered, vec![0, 1, 2]);
7690    }
7691
7692    #[test]
7693    fn test_predeploy_preserves_explicit_external_validator() {
7694        assert!(should_predeploy_before_test(false, false, false));
7695        assert!(should_predeploy_before_test(false, true, true));
7696        assert!(!should_predeploy_before_test(false, true, false));
7697        assert!(!should_predeploy_before_test(true, true, true));
7698    }
7699
7700    #[test]
7701    fn test_validator_plan_handles_in_process_template_skip() {
7702        assert_eq!(
7703            test_validator_plan(false, true, false, true),
7704            TestValidatorPlan {
7705                skip_local_validator: true,
7706                predeploy: false,
7707                stream_program_logs: true,
7708            }
7709        );
7710    }
7711
7712    #[test]
7713    fn test_validator_plan_preserves_explicit_external_validator() {
7714        assert_eq!(
7715            test_validator_plan(false, true, true, false),
7716            TestValidatorPlan {
7717                skip_local_validator: true,
7718                predeploy: true,
7719                stream_program_logs: true,
7720            }
7721        );
7722    }
7723
7724    #[test]
7725    fn surfpool_flags_do_not_force_runtime_features() {
7726        let dir = tempdir().unwrap();
7727        let cfg = WithPath::new(Config::default(), dir.path().join("Anchor.toml"));
7728        let flags = surfpool_flags(&cfg, &None, false, false, None, &[]).unwrap();
7729
7730        assert!(!flags.iter().any(|flag| flag == "--feature"));
7731    }
7732
7733    #[test]
7734    fn surfpool_flags_include_snapshot_for_generated_accounts() {
7735        let workspace = tempdir().unwrap();
7736        let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
7737        let test_validator = Some(TestValidator {
7738            validator: Some(crate::config::Validator {
7739                bind_address: "127.0.0.1".to_string(),
7740                ledger: ".anchor/test-ledger".to_string(),
7741                rpc_port: 18999,
7742                fund_accounts: Some(vec![crate::config::FundedAccount {
7743                    address: "new".to_string(),
7744                    lamports: Some(2_000_000_000),
7745                }]),
7746                ..Default::default()
7747            }),
7748            ..Default::default()
7749        });
7750
7751        let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
7752        let flags = surfpool_flags(&cfg, &None, false, false, None, &generated_accounts).unwrap();
7753
7754        let snapshot_index = flags.iter().position(|flag| flag == "--snapshot").unwrap();
7755        let snapshot_path = PathBuf::from(&flags[snapshot_index + 1]);
7756        let snapshot: JsonValue =
7757            serde_json::from_reader(File::open(&snapshot_path).unwrap()).unwrap();
7758
7759        assert!(snapshot_path.exists());
7760        assert!(snapshot
7761            .get(generated_accounts[0].pubkey.to_string())
7762            .is_some());
7763    }
7764
7765    #[test]
7766    fn test_jest_package_json_pins_uuid_for_commonjs() {
7767        for package_json in [
7768            template::package_json(true, "ISC".to_owned(), AnchorVersion::V1),
7769            template::ts_package_json(true, "ISC".to_owned(), AnchorVersion::V1),
7770        ] {
7771            let package: JsonValue = serde_json::from_str(&package_json).unwrap();
7772
7773            assert_eq!(package["overrides"]["uuid"], "^9.0.1");
7774            assert_eq!(package["resolutions"]["uuid"], "^9.0.1");
7775            assert_eq!(package["pnpm"]["overrides"]["uuid"], "^9.0.1");
7776        }
7777    }
7778
7779    #[test]
7780    fn parse_node_version_with_v_prefix() {
7781        let version = parse_node_version("v20.10.0\n").unwrap();
7782        assert_eq!(version.major, 20);
7783        assert_eq!(version.minor, 10);
7784        assert_eq!(version.patch, 0);
7785    }
7786
7787    #[test]
7788    fn parse_node_version_without_v_prefix() {
7789        let version = parse_node_version("20.10.0").unwrap();
7790        assert_eq!(version.major, 20);
7791    }
7792
7793    #[test]
7794    fn parse_node_version_ignores_surrounding_whitespace() {
7795        let version = parse_node_version("  v18.17.1  \n").unwrap();
7796        assert_eq!(version.major, 18);
7797        assert_eq!(version.minor, 17);
7798    }
7799
7800    #[test]
7801    fn parse_node_version_errors_on_garbage() {
7802        assert!(parse_node_version("not a version").is_err());
7803        assert!(parse_node_version("").is_err());
7804    }
7805
7806    #[test]
7807    fn extract_url_port_common_shapes() {
7808        assert_eq!(extract_url_port("http://127.0.0.1:8899"), Some(8899));
7809        assert_eq!(extract_url_port("http://127.0.0.1:8899/"), Some(8899));
7810        assert_eq!(extract_url_port("ws://localhost:8900/path?q=1"), Some(8900));
7811        assert_eq!(
7812            extract_url_port("https://api.mainnet-beta.solana.com"),
7813            None
7814        );
7815        assert_eq!(extract_url_port("http://127.0.0.1"), None);
7816        assert_eq!(extract_url_port("not a url"), None);
7817    }
7818
7819    #[test]
7820    fn replace_url_port_preserves_structure() {
7821        assert_eq!(
7822            replace_url_port("http://127.0.0.1:8899", 9001),
7823            "http://127.0.0.1:9001"
7824        );
7825        assert_eq!(
7826            replace_url_port("ws://127.0.0.1:8899/path?q=1", 9050),
7827            "ws://127.0.0.1:9050/path?q=1"
7828        );
7829        assert_eq!(
7830            replace_url_port("http://127.0.0.1", 8900),
7831            "http://127.0.0.1:8900"
7832        );
7833    }
7834
7835    #[test]
7836    fn idl_ts_preserves_literal_values() {
7837        let idl = Idl {
7838            address: "11111111111111111111111111111111".to_string(),
7839            metadata: IdlMetadata {
7840                name: "test_program".to_string(),
7841                version: "0.1.0".to_string(),
7842                spec: "0.1.0".to_string(),
7843                description: None,
7844                repository: None,
7845                dependencies: Vec::new(),
7846                contact: None,
7847                deployments: None,
7848            },
7849            docs: Vec::new(),
7850            instructions: vec![anchor_lang_idl::types::IdlInstruction {
7851                name: "do_thing".to_string(),
7852                docs: Vec::new(),
7853                discriminator: vec![0, 1, 2, 3, 4, 5, 6, 7],
7854                accounts: vec![IdlInstructionAccountItem::Single(IdlInstructionAccount {
7855                    name: "target_account".to_string(),
7856                    docs: Vec::new(),
7857                    writable: false,
7858                    signer: false,
7859                    optional: false,
7860                    address: None,
7861                    pda: Some(IdlPda {
7862                        seeds: vec![IdlSeed::Account(IdlSeedAccount {
7863                            path: "source_account.authority".to_string(),
7864                            account: Some("source_account".to_string()),
7865                        })],
7866                        program: None,
7867                    }),
7868                    relations: vec!["source_account".to_string()],
7869                })],
7870                args: vec![anchor_lang_idl::types::IdlField {
7871                    name: "some_arg".to_string(),
7872                    docs: Vec::new(),
7873                    ty: IdlType::U8,
7874                }],
7875                returns: None,
7876            }],
7877            accounts: vec![anchor_lang_idl::types::IdlAccount {
7878                name: "source_account".to_string(),
7879                discriminator: vec![8, 7, 6, 5, 4, 3, 2, 1],
7880            }],
7881            events: Vec::new(),
7882            errors: vec![IdlErrorCode {
7883                code: 6000,
7884                name: "Unauthorized".to_string(),
7885                msg: Some("Unauthorized".to_string()),
7886            }],
7887            types: vec![IdlTypeDef {
7888                name: "wrapper_type".to_string(),
7889                docs: Vec::new(),
7890                serialization: Default::default(),
7891                repr: None,
7892                generics: vec![IdlTypeDefGeneric::Type {
7893                    name: "item_type".to_string(),
7894                }],
7895                ty: IdlTypeDefTy::Type {
7896                    alias: IdlType::Defined {
7897                        name: "generic_holder".to_string(),
7898                        generics: vec![
7899                            IdlGenericArg::Type {
7900                                ty: IdlType::Generic("item_type".to_string()),
7901                            },
7902                            IdlGenericArg::Const {
7903                                value: "SEED_PREFIX".to_string(),
7904                            },
7905                        ],
7906                    },
7907                },
7908            }],
7909            constants: vec![anchor_lang_idl::types::IdlConst {
7910                name: "seed_prefix".to_string(),
7911                docs: Vec::new(),
7912                ty: IdlType::String,
7913                value: "SEED_PREFIX".to_string(),
7914            }],
7915        };
7916
7917        let ts = idl_ts(&idl).unwrap();
7918
7919        assert!(ts.contains(r#""name": "doThing""#));
7920        assert!(ts.contains(r#""name": "targetAccount""#));
7921        assert!(ts.contains(r#""path": "sourceAccount.authority""#));
7922        assert!(ts.contains(r#""account": "sourceAccount""#));
7923        assert!(ts.contains(r#""sourceAccount""#));
7924        assert!(ts.contains(r#""name": "someArg""#));
7925        assert!(ts.contains(r#""name": "sourceAccount""#));
7926        assert!(ts.contains(r#""name": "unauthorized""#));
7927        assert!(ts.contains(r#""msg": "Unauthorized""#));
7928        assert!(ts.contains(r#""name": "wrapperType""#));
7929        assert!(ts.contains(r#""name": "itemType""#));
7930        assert!(ts.contains(r#""name": "genericHolder""#));
7931        assert!(ts.contains(r#""generic": "itemType""#));
7932        assert!(ts.contains(r#""name": "seedPrefix""#));
7933        assert!(ts.contains(r#""value": "SEED_PREFIX""#));
7934    }
7935
7936    // ---------------------------------------------------------------------
7937    // `anchor idl convert` regression tests.
7938    // ---------------------------------------------------------------------
7939
7940    const TEST_PROGRAM_ID: Pubkey =
7941        solana_pubkey::pubkey!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
7942
7943    #[test]
7944    fn apply_program_id_override_current_spec_sets_top_level_address() {
7945        // Current-spec input. Must patch top-level `address` and keep the
7946        // `metadata.spec` field so the downstream parser still detects
7947        // the current spec.
7948        let idl = serde_json::json!({
7949            "address": "11111111111111111111111111111111",
7950            "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
7951            "instructions": [],
7952        })
7953        .to_string();
7954        let out = apply_program_id_override(idl.as_bytes(), TEST_PROGRAM_ID).unwrap();
7955        let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
7956        assert_eq!(v["address"], TEST_PROGRAM_ID.to_string());
7957        // Sibling metadata fields must survive.
7958        assert_eq!(v["metadata"]["spec"], "0.1.0");
7959        assert_eq!(v["metadata"]["name"], "demo");
7960        assert_eq!(v["metadata"]["version"], "0.1.0");
7961    }
7962
7963    #[test]
7964    fn apply_program_id_override_legacy_merges_into_existing_metadata() {
7965        // Legacy input with sibling metadata fields. Override must merge
7966        // into the existing `metadata` object, not replace it.
7967        let idl = serde_json::json!({
7968            "version": "0.1.0",
7969            "name": "demo",
7970            "instructions": [],
7971            "metadata": { "origin": "anchor", "address": "11111111111111111111111111111111" },
7972        })
7973        .to_string();
7974        let out = apply_program_id_override(idl.as_bytes(), TEST_PROGRAM_ID).unwrap();
7975        let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
7976        assert_eq!(v["metadata"]["address"], TEST_PROGRAM_ID.to_string());
7977        assert_eq!(v["metadata"]["origin"], "anchor");
7978    }
7979
7980    #[test]
7981    fn apply_program_id_override_legacy_no_metadata_creates_object() {
7982        // Legacy input without any metadata block. Override should create
7983        // a fresh `metadata.address` entry.
7984        let idl = serde_json::json!({
7985            "version": "0.1.0",
7986            "name": "demo",
7987            "instructions": [],
7988        })
7989        .to_string();
7990        let out = apply_program_id_override(idl.as_bytes(), TEST_PROGRAM_ID).unwrap();
7991        let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
7992        assert_eq!(v["metadata"]["address"], TEST_PROGRAM_ID.to_string());
7993    }
7994
7995    #[test]
7996    fn skip_deploy_preserves_legacy_validator_config_flags() {
7997        let cfg = WithPath::new(Config::default(), PathBuf::from("Anchor.toml"));
7998        let test_validator = Some(TestValidator {
7999            validator: Some(crate::config::Validator {
8000                bind_address: "127.0.0.1".to_string(),
8001                ledger: ".anchor/test-ledger".to_string(),
8002                rpc_port: 18999,
8003                warp_slot: Some(42),
8004                ..Default::default()
8005            }),
8006            ..Default::default()
8007        });
8008
8009        let flags = validator_flags(&cfg, &test_validator, true, &[]).unwrap();
8010
8011        assert!(flags
8012            .windows(2)
8013            .any(|args| args[0] == "--rpc-port" && args[1] == "18999"));
8014        assert!(flags
8015            .windows(2)
8016            .any(|args| args[0] == "--warp-slot" && args[1] == "42"));
8017        assert!(!flags.iter().any(|arg| arg == "--bpf-program"));
8018        assert!(!flags.iter().any(|arg| arg == "--upgradeable-program"));
8019    }
8020
8021    #[test]
8022    fn validator_flags_emits_extra_args() {
8023        let workspace = tempdir().unwrap();
8024        let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
8025        let expected = vec![
8026            "--rpc-pubsub-enable-block-subscription".to_string(),
8027            "--geyser-plugin-config".to_string(),
8028            "geyser.json".to_string(),
8029        ];
8030        let test_validator = Some(TestValidator {
8031            validator: Some(crate::config::Validator {
8032                bind_address: "127.0.0.1".to_string(),
8033                ledger: ".anchor/test-ledger".to_string(),
8034                rpc_port: 18999,
8035                extra_args: Some(expected.clone()),
8036                ..Default::default()
8037            }),
8038            ..Default::default()
8039        });
8040
8041        let flags = validator_flags(&cfg, &test_validator, true, &[]).unwrap();
8042
8043        assert!(flags
8044            .windows(expected.len())
8045            .any(|args| args == expected.as_slice()));
8046    }
8047
8048    #[test]
8049    fn skip_deploy_keeps_generated_account_flags() {
8050        let workspace = tempdir().unwrap();
8051        let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
8052        let funded_pubkey = Pubkey::new_unique();
8053        let test_validator = Some(TestValidator {
8054            validator: Some(crate::config::Validator {
8055                bind_address: "127.0.0.1".to_string(),
8056                ledger: ".anchor/test-ledger".to_string(),
8057                rpc_port: 18999,
8058                fund_accounts: Some(vec![crate::config::FundedAccount {
8059                    address: funded_pubkey.to_string(),
8060                    lamports: Some(2_000_000_000),
8061                }]),
8062                ..Default::default()
8063            }),
8064            ..Default::default()
8065        });
8066
8067        let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
8068        let flags = validator_flags(&cfg, &test_validator, true, &generated_accounts).unwrap();
8069        let expected_path = generated_accounts[0].file_path.display().to_string();
8070
8071        assert_eq!(generated_accounts.len(), 1);
8072        assert!(flags.windows(3).any(|args| {
8073            args[0] == "--account"
8074                && args[1] == funded_pubkey.to_string()
8075                && args[2] == expected_path
8076        }));
8077    }
8078
8079    #[test]
8080    fn token_account_requires_loaded_explicit_mint() {
8081        let workspace = tempdir().unwrap();
8082        let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
8083        let missing_mint = Pubkey::new_unique();
8084        let owner = Pubkey::new_unique();
8085        let test_validator = Some(TestValidator {
8086            validator: Some(crate::config::Validator {
8087                bind_address: "127.0.0.1".to_string(),
8088                ledger: ".anchor/test-ledger".to_string(),
8089                rpc_port: 18999,
8090                token_accounts: Some(vec![crate::config::TokenAccount {
8091                    mint: missing_mint.to_string(),
8092                    owner: owner.to_string(),
8093                    amount: 1,
8094                    address: None,
8095                }]),
8096                ..Default::default()
8097            }),
8098            ..Default::default()
8099        });
8100
8101        let err = generated_validator_accounts(&cfg, &test_validator).unwrap_err();
8102
8103        assert!(err.to_string().contains("token_account mint"));
8104    }
8105
8106    #[test]
8107    fn token_account_accepts_cloned_explicit_mint() {
8108        let workspace = tempdir().unwrap();
8109        let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
8110        let cloned_mint = Pubkey::new_unique();
8111        let owner = Pubkey::new_unique();
8112        let test_validator = Some(TestValidator {
8113            validator: Some(crate::config::Validator {
8114                bind_address: "127.0.0.1".to_string(),
8115                ledger: ".anchor/test-ledger".to_string(),
8116                rpc_port: 18999,
8117                clone: Some(vec![crate::config::CloneEntry {
8118                    address: cloned_mint.to_string(),
8119                }]),
8120                token_accounts: Some(vec![crate::config::TokenAccount {
8121                    mint: cloned_mint.to_string(),
8122                    owner: owner.to_string(),
8123                    amount: 1,
8124                    address: None,
8125                }]),
8126                ..Default::default()
8127            }),
8128            ..Default::default()
8129        });
8130
8131        let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
8132
8133        assert_eq!(generated_accounts.len(), 1);
8134        assert!(generated_accounts[0]
8135            .file_path
8136            .file_name()
8137            .and_then(|name| name.to_str())
8138            .is_some_and(|name| name.ends_with(".json")));
8139    }
8140
8141    #[test]
8142    fn multiple_new_funded_accounts_get_distinct_pubkeys() {
8143        let workspace = tempdir().unwrap();
8144        let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
8145        let test_validator = Some(TestValidator {
8146            validator: Some(crate::config::Validator {
8147                bind_address: "127.0.0.1".to_string(),
8148                ledger: ".anchor/test-ledger".to_string(),
8149                rpc_port: 18999,
8150                fund_accounts: Some(vec![
8151                    crate::config::FundedAccount {
8152                        address: "new".to_string(),
8153                        lamports: Some(15_000_000_000_000),
8154                    },
8155                    crate::config::FundedAccount {
8156                        address: "new".to_string(),
8157                        lamports: Some(20_000_000_000_000),
8158                    },
8159                ]),
8160                ..Default::default()
8161            }),
8162            ..Default::default()
8163        });
8164
8165        let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
8166
8167        assert_eq!(generated_accounts.len(), 2);
8168        assert_ne!(generated_accounts[0].pubkey, generated_accounts[1].pubkey);
8169    }
8170
8171    #[test]
8172    fn test_idl_ts_with_no_errors() {
8173        let idl = Idl {
8174            address: "11111111111111111111111111111111".to_string(),
8175            metadata: IdlMetadata {
8176                name: "test_program".to_string(),
8177                version: "0.1.0".to_string(),
8178                spec: "0.1.0".to_string(),
8179                description: None,
8180                repository: None,
8181                dependencies: vec![],
8182                contact: None,
8183                deployments: None,
8184            },
8185            docs: vec![],
8186            instructions: vec![],
8187            accounts: vec![],
8188            events: vec![],
8189            errors: vec![],
8190            types: vec![],
8191            constants: vec![],
8192        };
8193
8194        let result = idl_ts_errors(&idl);
8195        assert!(result.is_none());
8196    }
8197
8198    #[test]
8199    fn test_idl_ts_with_errors() {
8200        let idl = Idl {
8201            address: "11111111111111111111111111111111".to_string(),
8202            metadata: IdlMetadata {
8203                name: "test_program".to_string(),
8204                version: "0.1.0".to_string(),
8205                spec: "0.1.0".to_string(),
8206                description: None,
8207                repository: None,
8208                dependencies: vec![],
8209                contact: None,
8210                deployments: None,
8211            },
8212            docs: vec![],
8213            instructions: vec![],
8214            accounts: vec![],
8215            events: vec![],
8216            errors: vec![
8217                IdlErrorCode {
8218                    code: 6000,
8219                    name: "CustomError".to_string(),
8220                    msg: Some("This is a custom error".to_string()),
8221                },
8222                IdlErrorCode {
8223                    code: 6001,
8224                    name: "AnotherError".to_string(),
8225                    msg: None,
8226                },
8227            ],
8228            types: vec![],
8229            constants: vec![],
8230        };
8231
8232        let result = idl_ts_errors(&idl).unwrap();
8233
8234        assert!(result.contains("export const TestProgramErrorCode = {"));
8235        assert!(result.contains("CustomError: 6000"));
8236        assert!(result.contains("AnotherError: 6001"));
8237        assert!(result
8238            .contains("export type TestProgramErrorName = keyof typeof TestProgramErrorCode;"));
8239    }
8240
8241    #[test]
8242    fn test_idl_ts_error_name_formatting() {
8243        let idl = Idl {
8244            address: "11111111111111111111111111111111".to_string(),
8245            metadata: IdlMetadata {
8246                name: "test_program".to_string(),
8247                version: "0.1.0".to_string(),
8248                spec: "0.1.0".to_string(),
8249                description: None,
8250                repository: None,
8251                dependencies: vec![],
8252                contact: None,
8253                deployments: None,
8254            },
8255            docs: vec![],
8256            instructions: vec![],
8257            accounts: vec![],
8258            events: vec![],
8259            errors: vec![
8260                IdlErrorCode {
8261                    code: 6000,
8262                    name: "snake_case_error".to_string(),
8263                    msg: None,
8264                },
8265                IdlErrorCode {
8266                    code: 6001,
8267                    name: "SCREAMING_SNAKE_CASE".to_string(),
8268                    msg: None,
8269                },
8270                // `JSONError` and `JsonError` PascalCase to the same key.
8271                // They must stay distinct: emitting raw names avoids both the
8272                // duplicate-key TS1117 error and a mismatch with the on-chain
8273                // `errorCode.code` value.
8274                IdlErrorCode {
8275                    code: 6002,
8276                    name: "JSONError".to_string(),
8277                    msg: None,
8278                },
8279                IdlErrorCode {
8280                    code: 6003,
8281                    name: "JsonError".to_string(),
8282                    msg: None,
8283                },
8284            ],
8285            types: vec![],
8286            constants: vec![],
8287        };
8288
8289        let result = idl_ts_errors(&idl).unwrap();
8290
8291        assert!(result.contains("export const TestProgramErrorCode = {"));
8292        assert!(result.contains("  snake_case_error: 6000"));
8293        assert!(result.contains("  SCREAMING_SNAKE_CASE: 6001"));
8294        assert!(result.contains("  JSONError: 6002"));
8295        assert!(result.contains("  JsonError: 6003"));
8296        assert!(result
8297            .contains("export type TestProgramErrorName = keyof typeof TestProgramErrorCode;"));
8298    }
8299}