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