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, ValidatorType, WithPath, SHUTDOWN_WAIT,
6        STARTUP_WAIT, SURFPOOL_HOST,
7    },
8    abs_path::AbsolutePath,
9    anchor_cli_macros::AbsolutePath,
10    anchor_client::Cluster,
11    anchor_lang_idl::{
12        convert::convert_idl,
13        types::{Idl, IdlArrayLen, IdlDefinedFields, IdlType, IdlTypeDefTy},
14    },
15    anyhow::{anyhow, bail, Context, Result},
16    borsh::BorshDeserialize,
17    checks::{check_anchor_version, check_deps, check_idl_build_feature, check_overflow},
18    clap::{CommandFactory, Parser},
19    dirs::home_dir,
20    heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase},
21    regex::{Regex, RegexBuilder},
22    rust_template::{ProgramTemplate, TestTemplate},
23    semver::{Version, VersionReq},
24    serde::Deserialize,
25    serde_json::{json, Map, Value as JsonValue},
26    solana_cli_config::Config as SolanaCliConfig,
27    solana_commitment_config::CommitmentConfig,
28    solana_compute_budget_interface::ComputeBudgetInstruction,
29    solana_instruction::Instruction,
30    solana_keypair::Keypair,
31    solana_loader_v3_interface::state::UpgradeableLoaderState,
32    solana_pubkey::Pubkey,
33    solana_pubsub_client::pubsub_client::{PubsubClient, PubsubClientSubscription},
34    solana_rpc_client::rpc_client::RpcClient,
35    solana_rpc_client_api::{
36        config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter},
37        request::RpcRequest,
38        response::{Response as RpcResponse, RpcLogsResponse},
39    },
40    solana_signer::{EncodableKey, Signer},
41    solana_sdk_ids::bpf_loader_upgradeable,
42    std::{
43        collections::{BTreeMap, HashMap, HashSet},
44        ffi::OsString,
45        fs::{self, File},
46        io::prelude::*,
47        path::{Path, PathBuf},
48        process::{Child, ExitStatus, Stdio},
49        string::ToString,
50        sync::{LazyLock, OnceLock},
51    },
52};
53
54mod abs_path;
55mod account;
56mod checks;
57pub mod codama;
58pub mod config;
59#[cfg(not(windows))]
60pub mod coverage;
61#[cfg(not(windows))]
62pub mod debugger;
63#[cfg(not(windows))]
64mod flamegraph;
65mod keygen;
66mod metadata;
67#[cfg(not(windows))]
68mod profile;
69mod program;
70pub mod rust_template;
71
72// Version of the docker image.
73pub const VERSION: &str = env!("CARGO_PKG_VERSION");
74pub const DOCKER_BUILDER_VERSION: &str = VERSION;
75/// Default RPC port
76pub const DEFAULT_RPC_PORT: u16 = 8899;
77const DEFAULT_FAUCET_PORT: u16 = 9900;
78
79/// WebSocket port offset for solana-test-validator (RPC port + 1)
80pub const WEBSOCKET_PORT_OFFSET: u16 = 1;
81
82pub static AVM_HOME: LazyLock<PathBuf> = LazyLock::new(|| {
83    if let Ok(avm_home) = std::env::var("AVM_HOME") {
84        PathBuf::from(avm_home)
85    } else {
86        let mut user_home = dirs::home_dir().expect("Could not find home directory");
87        user_home.push(".avm");
88        user_home
89    }
90});
91
92#[derive(Debug, Parser, AbsolutePath)]
93#[clap(version = VERSION)]
94pub struct Opts {
95    #[clap(flatten)]
96    pub cfg_override: ConfigOverride,
97    #[clap(subcommand)]
98    pub command: Command,
99}
100
101#[derive(Debug, Parser, AbsolutePath)]
102pub enum Command {
103    /// Initializes a workspace.
104    Init {
105        /// Workspace name
106        name: String,
107        /// Use JavaScript instead of TypeScript
108        #[clap(short, long)]
109        javascript: bool,
110        /// Don't install JavaScript dependencies
111        #[clap(long)]
112        no_install: bool,
113        /// Package Manager to use. If omitted, detection cascades
114        /// `pnpm` → `yarn` → `npm` and picks the first one on PATH. When
115        /// set explicitly, the chosen binary must be installed — no
116        /// silent fallback.
117        #[clap(value_enum, long)]
118        package_manager: Option<PackageManager>,
119        /// Don't initialize git
120        #[clap(long)]
121        no_git: bool,
122        /// Rust program template to use
123        #[clap(value_enum, short, long, default_value = "multiple")]
124        template: ProgramTemplate,
125        /// Test template to use
126        #[clap(value_enum, long, default_value = "litesvm")]
127        test_template: TestTemplate,
128        /// Initialize even if there are files
129        #[clap(long, action)]
130        force: bool,
131        /// Install Solana agent skills
132        #[clap(long)]
133        install_agent_skills: bool,
134    },
135    /// Builds the workspace.
136    #[clap(name = "build", alias = "b")]
137    Build {
138        /// True if the build should not fail even if there are no "CHECK" comments
139        #[clap(long)]
140        skip_lint: bool,
141        /// Skip checking for program ID mismatch between keypair and declare_id
142        #[clap(long)]
143        ignore_keys: bool,
144        /// Do not build the IDL
145        #[clap(long)]
146        no_idl: bool,
147        /// Output directory for the IDL.
148        #[clap(short, long)]
149        idl: Option<String>,
150        /// Output directory for the TypeScript IDL.
151        #[clap(short = 't', long)]
152        idl_ts: Option<String>,
153        /// True if the build artifact needs to be deterministic and verifiable.
154        #[clap(short, long)]
155        verifiable: bool,
156        /// Name of the program to build
157        #[clap(short, long)]
158        program_name: Option<String>,
159        /// Version of the Solana toolchain to use. For --verifiable builds
160        /// only.
161        #[clap(short, long)]
162        solana_version: Option<String>,
163        /// Docker image to use. For --verifiable builds only.
164        #[clap(short, long)]
165        docker_image: Option<String>,
166        /// Bootstrap docker image from scratch, installing all requirements for
167        /// verifiable builds. Only works for debian-based images.
168        #[clap(value_enum, short, long, default_value = "none")]
169        bootstrap: BootstrapMode,
170        /// Environment variables to pass into the docker container
171        #[clap(short, long, required = false)]
172        env: Vec<String>,
173        /// Arguments to pass to the underlying `cargo build-sbf` command
174        #[clap(required = false, last = true)]
175        cargo_args: Vec<String>,
176        /// Suppress doc strings in IDL output
177        #[clap(long)]
178        no_docs: bool,
179    },
180    /// Expands macros (wrapper around cargo expand)
181    ///
182    /// Use it in a program folder to expand program
183    ///
184    /// Use it in a workspace but outside a program
185    /// folder to expand the entire workspace
186    Expand {
187        /// Expand only this program
188        #[clap(short, long)]
189        program_name: Option<String>,
190        /// Write to stdout
191        #[clap(long)]
192        stdout: bool,
193        /// Arguments to pass to the underlying `cargo expand` command
194        #[clap(required = false, last = true)]
195        cargo_args: Vec<String>,
196    },
197    /// Verifies the on-chain bytecode matches the locally compiled artifact.
198    /// Run this command inside a program subdirectory, i.e., in the dir
199    /// containing the program's Cargo.toml.
200    Verify {
201        /// The program ID to verify.
202        program_id: Pubkey,
203        /// The URL of the repository to verify against. Conflicts with `--current-dir`.
204        #[clap(long, conflicts_with = "current_dir")]
205        repo_url: Option<String>,
206        /// The commit hash to verify against. Requires `--repo-url`.
207        #[clap(long, requires = "repo_url")]
208        commit_hash: Option<String>,
209        /// Verify against the source code in the current directory. Conflicts with `--repo-url`.
210        #[clap(long)]
211        current_dir: bool,
212        /// Name of the program to run the command on. Defaults to the package name.
213        #[clap(long)]
214        program_name: Option<String>,
215        /// Any additional arguments to pass to `solana-verify`.
216        #[clap(raw = true)]
217        args: Vec<String>,
218    },
219    #[clap(name = "test", alias = "t")]
220    /// Runs integration tests.
221    Test {
222        /// Build and test only this program
223        #[clap(short, long)]
224        program_name: Option<String>,
225        /// Use this flag if you want to run tests against previously deployed
226        /// programs.
227        #[clap(long)]
228        skip_deploy: bool,
229        /// True if the build should not fail even if there are
230        /// no "CHECK" comments where normally required
231        #[clap(long)]
232        skip_lint: bool,
233        /// Flag to skip starting a local validator, if the configured cluster
234        /// url is a localnet.
235        #[clap(long)]
236        skip_local_validator: bool,
237        /// Flag to skip building the program in the workspace,
238        /// use this to save time when running test and the program code is not altered.
239        #[clap(long)]
240        skip_build: bool,
241        /// Do not build the IDL
242        #[clap(long)]
243        no_idl: bool,
244        /// Flag to keep the local validator running after tests
245        /// to be able to check the transactions.
246        #[clap(long)]
247        detach: bool,
248        /// Run the test suites under the specified path
249        #[clap(long)]
250        run: Vec<String>,
251        /// Validator type to use for local testing
252        #[clap(value_enum, long, default_value = "surfpool")]
253        validator: ValidatorType,
254        #[cfg(not(windows))]
255        /// Profile each test: record per-test SBF register traces and
256        /// render a flamegraph SVG per test under
257        /// `target/anchor-v2-profile/`. Forces a debug build (DWARF is
258        /// required for symbolication) and activates the `profile`
259        /// cargo feature. Rust tests only.
260        #[clap(long)]
261        profile: bool,
262        args: Vec<String>,
263        /// Environment variables to pass into the docker container
264        #[clap(short, long, required = false)]
265        env: Vec<String>,
266        /// Arguments to pass to the underlying `cargo build-sbf` command.
267        #[clap(required = false, last = true)]
268        cargo_args: Vec<String>,
269    },
270    /// Creates a new program.
271    New {
272        /// Program name
273        name: String,
274        /// Rust program template to use
275        #[clap(value_enum, short, long, default_value = "multiple")]
276        template: ProgramTemplate,
277        /// Create new program even if there is already one
278        #[clap(long, action)]
279        force: bool,
280    },
281    #[cfg(not(windows))]
282    /// Run tests under a foundry-style instruction-level debugger.
283    ///
284    /// Reuses the `anchor test --profile` trace pipeline: rebuilds with DWARF,
285    /// runs the test suite via `anchor-v2-testing` (LiteSVM in-process), then
286    /// opens a ratatui TUI over the captured SBF register traces instead of
287    /// rendering flamegraphs. Never touches a validator — LiteSVM is the only
288    /// runtime that produces the traces this consumes.
289    Debugger {
290        /// Filter captured traces to tests whose name contains this substring.
291        /// If omitted, the TUI opens with a picker across every captured
292        /// `(test, tx)` pair.
293        test_name: Option<String>,
294        /// Skip the build+test phase and open the TUI directly over whatever
295        /// traces already exist under `target/anchor-v2-profile/`.
296        #[clap(long)]
297        skip_run: bool,
298        /// Skip `cargo build-sbf`. Use when the `.so` is already up to date
299        /// — saves a rebuild but fails if the deploy artifact is missing.
300        #[clap(long)]
301        skip_build: bool,
302        /// Forwarded to the underlying `anchor test` invocation.
303        #[clap(long)]
304        skip_lint: bool,
305        /// Drive tests over sbpf's gdb-stub instead of reading dumped trace
306        /// files. Per-step register snapshots + exact CU are collected live
307        /// via the gdb remote serial protocol. 100-1000× slower than the
308        /// default register-tracing path, but needs no fork to emit CU
309        /// values (sbpf exposes `cu_remaining` as register 12).
310        #[clap(long)]
311        gdb: bool,
312        /// Arguments to pass to the underlying `cargo build-sbf` command.
313        #[clap(required = false, last = true)]
314        cargo_args: Vec<String>,
315    },
316    #[cfg(not(windows))]
317    /// Generate source-level code coverage from SBF register traces.
318    ///
319    /// Builds programs with DWARF debug info, runs the test suite with
320    /// litesvm register tracing enabled, then maps executed PCs to source
321    /// lines via addr2line and outputs LCOV.
322    Coverage {
323        /// Skip the build+test phase and generate coverage from existing
324        /// traces in `SBF_TRACE_DIR`.
325        #[clap(long)]
326        skip_run: bool,
327        /// Skip `cargo build-sbf`. Use when the `.so` is already up to date.
328        #[clap(long)]
329        skip_build: bool,
330        /// Output path for the LCOV file.
331        #[clap(long, default_value = "target/coverage/sbf.lcov")]
332        output: String,
333        /// Directory containing register trace files.
334        #[clap(long, default_value = "target/coverage/traces")]
335        trace_dir: String,
336        /// Arguments to pass to the underlying `cargo test` command.
337        #[clap(required = false, last = true)]
338        cargo_args: Vec<String>,
339    },
340    #[cfg(not(windows))]
341    /// Filter host LCOV before merging it with SBF coverage.
342    #[clap(name = "coverage-filter-host", hide = true)]
343    CoverageFilterHost {
344        /// LCOV generated from SBF register traces.
345        #[clap(long)]
346        sbf_lcov: String,
347        /// LCOV generated by cargo-llvm-cov on the host.
348        #[clap(long)]
349        host_lcov: String,
350        /// Output path for the filtered host LCOV.
351        #[clap(long)]
352        output: String,
353    },
354    /// Commands for interacting with interface definitions.
355    Idl {
356        #[clap(subcommand)]
357        subcmd: IdlCommand,
358    },
359    /// Remove all artifacts from the generated directories except program keypairs.
360    Clean,
361    /// Deploys each program in the workspace.
362    #[clap(hide = true)]
363    #[deprecated(since = "0.32.0", note = "use `anchor program deploy` instead")]
364    Deploy {
365        /// Only deploy this program
366        #[clap(short, long)]
367        program_name: Option<String>,
368        /// Keypair of the program (filepath) (requires program-name)
369        #[clap(long, requires = "program_name")]
370        program_keypair: Option<PathBuf>,
371        /// If true, deploy from path target/verifiable
372        #[clap(short, long)]
373        verifiable: bool,
374        /// Don't upload IDL during deployment (IDL is uploaded by default)
375        #[clap(long)]
376        no_idl: bool,
377        /// Arguments to pass to the underlying `solana program deploy` command.
378        #[clap(required = false, last = true)]
379        solana_args: Vec<String>,
380    },
381    /// Runs the deploy migration script.
382    Migrate,
383    /// Deploys, initializes an IDL, and migrates all in one command.
384    /// Upgrades a single program. The configured wallet must be the upgrade
385    /// authority.
386    #[clap(hide = true)]
387    #[deprecated(since = "0.32.0", note = "use `anchor program upgrade` instead")]
388    Upgrade {
389        /// The program to upgrade.
390        #[clap(short, long)]
391        program_id: Pubkey,
392        /// Filepath to the new program binary.
393        program_filepath: PathBuf,
394        /// Max times to retry on failure.
395        #[clap(long, default_value = "0")]
396        max_retries: u32,
397        /// Arguments to pass to the underlying `solana program deploy` command.
398        #[clap(required = false, last = true)]
399        solana_args: Vec<String>,
400    },
401    /// Request an airdrop of SOL
402    Airdrop {
403        /// Amount of SOL to airdrop
404        amount: f64,
405        /// Recipient address (defaults to configured wallet)
406        pubkey: Option<Pubkey>,
407    },
408    /// Cluster commands.
409    Cluster {
410        #[clap(subcommand)]
411        subcmd: ClusterCommand,
412    },
413    /// Configuration management commands.
414    Config {
415        #[clap(subcommand)]
416        subcmd: ConfigCommand,
417    },
418    /// Starts a node shell with an Anchor client setup according to the local
419    /// config.
420    Shell,
421    /// Runs the script defined by the current workspace's Anchor.toml.
422    Run {
423        /// The name of the script to run.
424        script: String,
425        /// Argument to pass to the underlying script.
426        #[clap(required = false, last = true)]
427        script_args: Vec<String>,
428    },
429    /// Program keypair commands.
430    Keys {
431        #[clap(subcommand)]
432        subcmd: KeysCommand,
433    },
434    /// Localnet commands.
435    Localnet {
436        /// Flag to skip building the program in the workspace,
437        /// use this to save time when running test and the program code is not altered.
438        #[clap(long)]
439        skip_build: bool,
440        /// Use this flag if you want to run tests against previously deployed
441        /// programs.
442        #[clap(long)]
443        skip_deploy: bool,
444        /// True if the build should not fail even if there are
445        /// no "CHECK" comments where normally required
446        #[clap(long)]
447        skip_lint: bool,
448        /// Skip checking for program ID mismatch between keypair and declare_id
449        #[clap(long)]
450        ignore_keys: bool,
451        /// Validator type to use for local testing
452        #[clap(value_enum, long, default_value = "surfpool")]
453        validator: ValidatorType,
454        /// Environment variables to pass into the docker container
455        #[clap(short, long, required = false)]
456        env: Vec<String>,
457        /// Arguments to pass to the underlying `cargo build-sbf` command.
458        #[clap(required = false, last = true)]
459        cargo_args: Vec<String>,
460    },
461    /// Fetch and deserialize an account using the IDL provided.
462    Account {
463        /// Account struct to deserialize (format: <program_name>.<Account>)
464        account_type: String,
465        /// Address of the account to deserialize
466        address: Pubkey,
467        /// Path of IDL to use (defaults to workspace IDL)
468        #[clap(long)]
469        idl: Option<PathBuf>,
470    },
471    /// Generates shell completions.
472    Completions {
473        #[clap(value_enum)]
474        shell: clap_complete::Shell,
475    },
476    /// Get your public key
477    Address,
478    /// Get your balance
479    Balance {
480        /// Account to check balance for (defaults to configured wallet)
481        pubkey: Option<Pubkey>,
482        /// Display balance in lamports instead of SOL
483        #[clap(long)]
484        lamports: bool,
485    },
486    /// Get current epoch
487    Epoch,
488    /// Get information about the current epoch
489    #[clap(name = "epoch-info")]
490    EpochInfo,
491    /// Stream transaction logs
492    Logs {
493        /// Include vote transactions when monitoring all transactions
494        #[clap(long)]
495        include_votes: bool,
496        /// Addresses to filter logs by
497        #[clap(long)]
498        address: Option<Vec<Pubkey>>,
499    },
500    /// Show the contents of an account
501    ShowAccount {
502        #[clap(flatten)]
503        cmd: account::ShowAccountCommand,
504    },
505    /// Keypair generation and management
506    Keygen {
507        #[clap(subcommand)]
508        subcmd: KeygenCommand,
509    },
510    /// Program deployment and management commands
511    Program {
512        #[clap(subcommand)]
513        subcmd: ProgramCommand,
514    },
515    /// Codama IDL integration commands
516    Codama {
517        #[clap(subcommand)]
518        subcmd: codama::CodamaCommand,
519    },
520}
521
522#[derive(Debug, Parser, AbsolutePath)]
523pub enum KeygenCommand {
524    /// Generate a new keypair
525    New {
526        /// Path to generated keypair file
527        #[clap(short = 'o', long)]
528        outfile: Option<PathBuf>,
529        /// Overwrite the output file if it exists
530        #[clap(short, long)]
531        force: bool,
532        /// Do not prompt for a passphrase
533        #[clap(long)]
534        no_passphrase: bool,
535        /// Do not display the generated pubkey
536        #[clap(long)]
537        silent: bool,
538        /// Number of words in the mnemonic phrase [possible values: 12, 15, 18, 21, 24]
539        #[clap(short = 'w', long, default_value = "12")]
540        word_count: usize,
541    },
542    /// Display the pubkey for a given keypair
543    Pubkey {
544        /// Keypair filepath
545        keypair: Option<PathBuf>,
546    },
547    /// Recover a keypair from a seed phrase
548    Recover {
549        /// Path to recovered keypair file
550        #[clap(short = 'o', long)]
551        outfile: Option<PathBuf>,
552        /// Overwrite the output file if it exists
553        #[clap(short, long)]
554        force: bool,
555        /// Skip seed phrase validation
556        #[clap(long)]
557        skip_seed_phrase_validation: bool,
558        /// Do not prompt for a passphrase
559        #[clap(long)]
560        no_passphrase: bool,
561    },
562    /// Verify a keypair can sign and verify a message
563    Verify {
564        /// Public key to verify
565        pubkey: Pubkey,
566        /// Keypair filepath (defaults to configured wallet)
567        keypair: Option<PathBuf>,
568    },
569}
570
571#[derive(Debug, Parser, AbsolutePath)]
572pub enum KeysCommand {
573    /// List all of the program keys.
574    List,
575    /// Sync program `declare_id!` pubkeys with the program's actual pubkey.
576    Sync {
577        /// Only sync the given program instead of all programs
578        #[clap(short, long)]
579        program_name: Option<String>,
580    },
581}
582
583#[derive(Debug, Parser, AbsolutePath)]
584pub enum ProgramCommand {
585    /// Deploy an upgradeable program
586    Deploy {
587        /// Program filepath (e.g., target/deploy/my_program.so).
588        /// If not provided, discovers programs from workspace
589        program_filepath: Option<PathBuf>,
590        /// Program name to deploy (from workspace). Used when program_filepath is not provided
591        #[clap(short, long)]
592        program_name: Option<String>,
593        /// Program keypair filepath (defaults to target/deploy/{program_name}-keypair.json)
594        #[clap(long)]
595        program_keypair: Option<PathBuf>,
596        /// Upgrade authority keypair (defaults to configured wallet)
597        #[clap(long)]
598        upgrade_authority: Option<String>,
599        /// Program id to deploy to (derived from program-keypair if not specified)
600        #[clap(long)]
601        program_id: Option<Pubkey>,
602        /// Buffer account to use for deployment
603        #[clap(long)]
604        buffer: Option<Pubkey>,
605        /// Maximum transaction length (BPF loader upgradeable limit)
606        #[clap(long)]
607        max_len: Option<usize>,
608        /// Don't upload IDL during deployment (IDL is uploaded by default)
609        #[clap(long)]
610        no_idl: bool,
611        /// Make the program immutable after deployment (cannot be upgraded)
612        #[clap(long = "final")]
613        make_final: bool,
614        /// Additional arguments to configure deployment (e.g., --with-compute-unit-price 1000)
615        #[clap(required = false, last = true)]
616        solana_args: Vec<String>,
617    },
618    /// Write a program into a buffer account
619    WriteBuffer {
620        /// Program filepath (e.g., target/deploy/my_program.so).
621        /// If not provided, discovers program from workspace using program_name
622        program_filepath: Option<PathBuf>,
623        /// Program name to write (from workspace). Used when program_filepath is not provided
624        #[clap(short, long)]
625        program_name: Option<String>,
626        /// Buffer account keypair (defaults to new keypair)
627        #[clap(long)]
628        buffer: Option<String>,
629        /// Buffer authority (defaults to configured wallet)
630        #[clap(long)]
631        buffer_authority: Option<String>,
632        /// Maximum transaction length
633        #[clap(long)]
634        max_len: Option<usize>,
635    },
636    /// Set a new buffer authority
637    SetBufferAuthority {
638        /// Buffer account address
639        buffer: Pubkey,
640        /// New buffer authority
641        new_buffer_authority: Pubkey,
642    },
643    /// Set a new program authority
644    SetUpgradeAuthority {
645        /// Program id
646        program_id: Pubkey,
647        /// New upgrade authority pubkey
648        #[clap(long)]
649        new_upgrade_authority: Option<Pubkey>,
650        /// New upgrade authority signer (keypair file). Required unless --skip-new-upgrade-authority-signer-check is used.
651        /// When provided, both current and new authority will sign (checked mode, recommended)
652        #[clap(long)]
653        new_upgrade_authority_signer: Option<String>,
654        /// Skip new upgrade authority signer check. Allows setting authority with only current authority signature.
655        /// WARNING: Less safe - use only if you're confident the pubkey is correct
656        #[clap(long)]
657        skip_new_upgrade_authority_signer_check: bool,
658        /// Make the program immutable (cannot be upgraded)
659        #[clap(long = "final")]
660        make_final: bool,
661        /// Current upgrade authority keypair (defaults to configured wallet)
662        #[clap(long)]
663        upgrade_authority: Option<String>,
664    },
665    /// Display information about a buffer or program
666    Show {
667        /// Account address (buffer or program)
668        account: Pubkey,
669        /// Get account information from the Solana config file
670        #[clap(long)]
671        get_programs: bool,
672        /// Get account information from the Solana config file
673        #[clap(long)]
674        get_buffers: bool,
675        /// Show all accounts
676        #[clap(long)]
677        all: bool,
678    },
679    /// Upgrade an upgradeable program
680    Upgrade {
681        /// Program id to upgrade
682        program_id: Pubkey,
683        /// Program filepath (e.g., target/deploy/my_program.so). If not provided, discovers from workspace
684        #[clap(long)]
685        program_filepath: Option<PathBuf>,
686        /// Program name to upgrade (from workspace). Used when program_filepath is not provided
687        #[clap(short, long)]
688        program_name: Option<String>,
689        /// Existing buffer account to upgrade from. If not provided, auto-discovers program from workspace
690        #[clap(long)]
691        buffer: Option<Pubkey>,
692        /// Upgrade authority (defaults to configured wallet)
693        #[clap(long)]
694        upgrade_authority: Option<String>,
695        /// Max times to retry on failure
696        #[clap(long, default_value = "0")]
697        max_retries: u32,
698        /// Additional arguments to configure deployment (e.g., --with-compute-unit-price 1000)
699        #[clap(required = false, last = true)]
700        solana_args: Vec<String>,
701    },
702    /// Write the program data to a file
703    Dump {
704        /// Program account address
705        account: Pubkey,
706        /// Output file path
707        output_file: String,
708    },
709    /// Close a program or buffer account and withdraw all lamports
710    Close {
711        /// Account address to close (buffer or program).
712        /// If not provided, discovers program from workspace using program_name
713        account: Option<Pubkey>,
714        /// Program name to close (from workspace). Used when account is not provided
715        #[clap(short, long)]
716        program_name: Option<String>,
717        /// Authority keypair (defaults to configured wallet)
718        #[clap(long)]
719        authority: Option<String>,
720        /// Recipient address for reclaimed lamports (defaults to authority)
721        #[clap(long)]
722        recipient: Option<Pubkey>,
723        /// Bypass warning prompts
724        #[clap(long)]
725        bypass_warning: bool,
726    },
727    /// Extend the length of an upgradeable program
728    Extend {
729        /// Program id to extend.
730        /// If not provided, discovers program from workspace using program_name
731        program_id: Option<Pubkey>,
732        /// Program name to extend (from workspace). Used when program_id is not provided
733        #[clap(short, long)]
734        program_name: Option<String>,
735        /// Additional bytes to allocate
736        additional_bytes: usize,
737    },
738}
739
740#[derive(Debug, Parser, AbsolutePath)]
741pub enum IdlCommand {
742    /// Initializes a program's IDL account. Can only be run once.
743    Init {
744        /// Program id to initialize IDL for.
745        /// If not provided, discovers program ID from IDL.
746        program_id: Option<Pubkey>,
747        #[clap(short, long)]
748        filepath: PathBuf,
749        #[clap(long)]
750        priority_fee: Option<u64>,
751        /// Create non-canonical metadata account (third-party metadata)
752        #[clap(long)]
753        non_canonical: bool,
754        /// Allow running against a localnet cluster (disabled by default)
755        #[clap(long)]
756        #[cfg(feature = "idl-localnet-testing")]
757        allow_localnet: bool,
758    },
759    /// Upgrades the IDL to the new file. An alias for first writing and then
760    /// then setting the idl buffer account.
761    Upgrade {
762        /// Program id to upgrade IDL for.
763        /// If not provided, discovers program ID from IDL.
764        program_id: Option<Pubkey>,
765        #[clap(short, long)]
766        filepath: PathBuf,
767        #[clap(long)]
768        priority_fee: Option<u64>,
769        /// Allow running against a localnet cluster (disabled by default)
770        #[clap(long)]
771        #[cfg(feature = "idl-localnet-testing")]
772        allow_localnet: bool,
773    },
774    /// Generates the IDL for the program using the compilation method.
775    #[clap(alias = "b")]
776    Build {
777        // Program name to build the IDL of(current dir's program if not specified)
778        #[clap(short, long)]
779        program_name: Option<String>,
780        /// Output file for the IDL (stdout if not specified)
781        #[clap(short, long)]
782        out: Option<String>,
783        /// Output file for the TypeScript IDL
784        #[clap(short = 't', long)]
785        out_ts: Option<String>,
786        /// Suppress doc strings in output
787        #[clap(long)]
788        no_docs: bool,
789        /// Do not check for safety comments
790        #[clap(long)]
791        skip_lint: bool,
792        /// Arguments to pass to the underlying `cargo test` command
793        #[clap(required = false, last = true)]
794        cargo_args: Vec<String>,
795    },
796    /// Fetches an IDL for the given program from a cluster.
797    Fetch {
798        program_id: Pubkey,
799        /// Output file for the IDL (stdout if not specified).
800        #[clap(short, long)]
801        out: Option<String>,
802        /// Fetch non-canonical metadata account (third-party metadata)
803        #[clap(long)]
804        non_canonical: bool,
805    },
806    /// Convert legacy IDLs (pre Anchor 0.30) to the new IDL spec
807    Convert {
808        /// Path to the IDL file
809        path: PathBuf,
810        /// Output file for the IDL (stdout if not specified)
811        #[clap(short, long)]
812        out: Option<PathBuf>,
813        /// Program id to initialize IDL for.
814        /// If not provided, discovers program ID from IDL.
815        #[clap(short, long)]
816        program_id: Option<Pubkey>,
817    },
818    /// Generate TypeScript type for the IDL
819    Type {
820        /// Path to the IDL file
821        path: PathBuf,
822        /// Output file for the IDL (stdout if not specified)
823        #[clap(short, long)]
824        out: Option<PathBuf>,
825    },
826    /// Close a metadata account and recover rent
827    Close {
828        /// The program ID
829        program_id: Pubkey,
830        /// The seed used for the metadata account (default: "idl")
831        #[clap(long, default_value = "idl")]
832        seed: String,
833        /// Priority fees in micro-lamports per compute unit
834        #[clap(long)]
835        priority_fee: Option<u64>,
836    },
837    /// Create a buffer account for metadata
838    CreateBuffer {
839        /// Path to the metadata file
840        #[clap(short, long)]
841        filepath: PathBuf,
842        /// Priority fees in micro-lamports per compute unit
843        #[clap(long)]
844        priority_fee: Option<u64>,
845    },
846    /// Set a new authority on a buffer account
847    SetBufferAuthority {
848        /// The buffer account address
849        buffer: Pubkey,
850        /// The new authority
851        #[clap(short, long)]
852        new_authority: Pubkey,
853        /// Priority fees in micro-lamports per compute unit
854        #[clap(long)]
855        priority_fee: Option<u64>,
856    },
857    /// Write metadata using a buffer account
858    WriteBuffer {
859        /// The program ID
860        program_id: Pubkey,
861        /// The buffer account address
862        #[clap(short, long)]
863        buffer: Pubkey,
864        /// The seed to use for the metadata account (default: "idl")
865        #[clap(long, default_value = "idl")]
866        seed: String,
867        /// Close the buffer after writing
868        #[clap(long)]
869        close_buffer: bool,
870        /// Priority fees in micro-lamports per compute unit
871        #[clap(long)]
872        priority_fee: Option<u64>,
873    },
874}
875
876#[derive(Debug, Parser, AbsolutePath)]
877pub enum ClusterCommand {
878    /// Prints common cluster urls.
879    List,
880}
881
882#[derive(Debug, Parser, AbsolutePath)]
883pub enum ConfigCommand {
884    /// Get configuration settings from the local Anchor.toml
885    Get,
886    /// Set configuration settings in the local Anchor.toml
887    Set {
888        /// Cluster to connect to (custom URL). Use -um, -ud, -ut, -ul for standard clusters
889        #[clap(short = 'u', long = "url")]
890        url: Option<String>,
891        /// Path to wallet keypair file to update the Anchor.toml file with
892        #[clap(short = 'k', long = "keypair")]
893        keypair: Option<PathBuf>,
894    },
895}
896
897fn get_keypair(path: &Path) -> Result<Keypair> {
898    solana_keypair::read_keypair_file(path)
899        .map_err(|_| anyhow!("Unable to read keypair file ({})", path.display()))
900}
901
902/// Format lamports as SOL with trailing zeros removed
903fn format_sol(lamports: u64) -> String {
904    let sol = lamports as f64 / 1_000_000_000.0;
905    let formatted = format!("{:.8}", sol);
906
907    // Remove trailing zeros and decimal point if not needed
908    let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
909    format!("{} SOL", trimmed)
910}
911
912/// Get cluster URL and wallet path from Anchor config, CLI overrides, or Solana CLI config
913fn get_cluster_and_wallet(cfg_override: &ConfigOverride) -> Result<(String, String)> {
914    // Try to get from Anchor workspace config first
915    if let Ok(Some(cfg)) = Config::discover(cfg_override) {
916        return Ok((
917            cfg.provider.cluster.url().to_string(),
918            cfg.provider.wallet.to_string(),
919        ));
920    }
921
922    // Try to load Solana CLI config
923    let (cluster_url, wallet_path) =
924        if let Some(config_file) = solana_cli_config::CONFIG_FILE.as_ref() {
925            match SolanaCliConfig::load(config_file) {
926                Ok(cli_config) => (
927                    cli_config.json_rpc_url.clone(),
928                    cli_config.keypair_path.clone(),
929                ),
930                Err(_) => {
931                    // Fallback to defaults if Solana CLI config doesn't exist
932                    (
933                        "https://api.mainnet-beta.solana.com".to_string(),
934                        dirs::home_dir()
935                            .map(|home| {
936                                home.join(".config/solana/id.json")
937                                    .to_string_lossy()
938                                    .to_string()
939                            })
940                            .unwrap_or_else(|| "~/.config/solana/id.json".to_string()),
941                    )
942                }
943            }
944        } else {
945            // If CONFIG_FILE is None, use defaults
946            (
947                "https://api.mainnet-beta.solana.com".to_string(),
948                dirs::home_dir()
949                    .map(|home| {
950                        home.join(".config/solana/id.json")
951                            .to_string_lossy()
952                            .to_string()
953                    })
954                    .unwrap_or_else(|| "~/.config/solana/id.json".to_string()),
955            )
956        };
957
958    // Apply cluster override if provided
959    let final_cluster = if let Some(cluster) = &cfg_override.cluster {
960        cluster.url().to_string()
961    } else {
962        cluster_url
963    };
964
965    Ok((final_cluster, wallet_path))
966}
967
968/// Get the recommended priority fee from the RPC client, falling back to 0 if unavailable
969pub fn get_recommended_micro_lamport_fee(client: &RpcClient) -> u64 {
970    let mut fees = match client.get_recent_prioritization_fees(&[]) {
971        // Fees may be empty or query may fail, e.g. on localnet
972        Err(e) => {
973            eprintln!("Warning: failed to fetch prioritization fees, defaulting to 0: {e}");
974            return 0;
975        }
976        Ok(f) if f.is_empty() => {
977            return 0;
978        }
979        Ok(f) => f,
980    };
981
982    // Get the median fee from the most recent 150 slots' prioritization fee
983    fees.sort_unstable_by_key(|fee| fee.prioritization_fee);
984    let median_index = fees.len() / 2;
985
986    if fees.len() % 2 == 0 {
987        (fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2
988    } else {
989        fees[median_index].prioritization_fee
990    }
991}
992
993/// Prepend a compute unit ix, if the priority fee is greater than 0.
994pub fn prepend_compute_unit_ix(
995    instructions: Vec<Instruction>,
996    client: &RpcClient,
997    priority_fee: Option<u64>,
998) -> Vec<Instruction> {
999    let priority_fee = priority_fee.unwrap_or_else(|| get_recommended_micro_lamport_fee(client));
1000
1001    if priority_fee > 0 {
1002        let mut instructions_appended = instructions.clone();
1003        instructions_appended.insert(
1004            0,
1005            ComputeBudgetInstruction::set_compute_unit_price(priority_fee),
1006        );
1007        instructions_appended
1008    } else {
1009        instructions
1010    }
1011}
1012
1013pub fn entry(opts: Opts) -> Result<()> {
1014    let opts = opts.absolute();
1015
1016    let restore_cbs = override_toolchain(&opts.cfg_override)?;
1017    let result = process_command(opts);
1018    restore_toolchain(restore_cbs)?;
1019
1020    result
1021}
1022
1023/// Functions to restore toolchain entries
1024type RestoreToolchainCallbacks = Vec<Box<dyn FnOnce() -> Result<()>>>;
1025
1026/// Override the toolchain from `Anchor.toml`.
1027///
1028/// Returns the previous versions to restore back to.
1029fn override_toolchain(cfg_override: &ConfigOverride) -> Result<RestoreToolchainCallbacks> {
1030    let mut restore_cbs: RestoreToolchainCallbacks = vec![];
1031
1032    let cfg = Config::discover(cfg_override)?;
1033    if let Some(cfg) = cfg {
1034        fn parse_version(text: &str) -> Option<String> {
1035            Some(
1036                Regex::new(r"(\d+\.\d+\.\S+)")
1037                    .unwrap()
1038                    .captures_iter(text)
1039                    .next()?
1040                    .get(0)?
1041                    .as_str()
1042                    .to_string(),
1043            )
1044        }
1045
1046        fn get_current_version(cmd_name: &str) -> Result<String> {
1047            let output = std::process::Command::new(cmd_name)
1048                .arg("--version")
1049                .output()?;
1050            if !output.status.success() {
1051                return Err(anyhow!("Failed to run `{cmd_name} --version`"));
1052            }
1053
1054            let output_version = std::str::from_utf8(&output.stdout)?;
1055            parse_version(output_version)
1056                .ok_or_else(|| anyhow!("Failed to parse the version of `{cmd_name}`"))
1057        }
1058
1059        if let Some(solana_version) = &cfg.toolchain.solana_version {
1060            let current_version = get_current_version("solana")?;
1061            if solana_version != &current_version {
1062                // We are overriding with `solana-install` command instead of using the binaries
1063                // from `~/.local/share/solana/install/releases` because we use multiple Solana
1064                // binaries in various commands.
1065                fn override_solana_version(version: String) -> Result<bool> {
1066                    // There is a deprecation warning message starting with `1.18.19` which causes
1067                    // parsing problems https://github.com/solana-foundation/anchor/issues/3147
1068                    let (cmd_name, domain) =
1069                        if Version::parse(&version)? < Version::parse("1.18.19")? {
1070                            ("solana-install", "solana.com")
1071                        } else {
1072                            ("agave-install", "anza.xyz")
1073                        };
1074
1075                    // Install the command if it's not installed
1076                    if get_current_version(cmd_name).is_err() {
1077                        // `solana-install` and `agave-install` are not usable at the same time i.e.
1078                        // using one of them makes the other unusable with the default installation,
1079                        // causing the installation process to run each time users switch between
1080                        // `agave` supported versions. For example, if the user's active Solana
1081                        // version is `1.18.17`, and he specifies `solana_version = "2.0.6"`, this
1082                        // code path will run each time an Anchor command gets executed.
1083                        eprintln!(
1084                            "Command not installed: `{cmd_name}`. \
1085                            See https://github.com/anza-xyz/agave/wiki/Agave-Transition, \
1086                            installing..."
1087                        );
1088                        let install_script = std::process::Command::new("curl")
1089                            .args([
1090                                "-sSfL",
1091                                &format!("https://release.{domain}/v{version}/install"),
1092                            ])
1093                            .output()?;
1094                        let is_successful = std::process::Command::new("sh")
1095                            .args(["-c", std::str::from_utf8(&install_script.stdout)?])
1096                            .spawn()?
1097                            .wait_with_output()?
1098                            .status
1099                            .success();
1100                        if !is_successful {
1101                            return Err(anyhow!("Failed to install `{cmd_name}`"));
1102                        }
1103                    }
1104
1105                    let output = std::process::Command::new(cmd_name).arg("list").output()?;
1106                    if !output.status.success() {
1107                        return Err(anyhow!("Failed to list installed `solana` versions"));
1108                    }
1109
1110                    // Hide the installation progress if the version is already installed
1111                    let is_installed = std::str::from_utf8(&output.stdout)?
1112                        .lines()
1113                        .filter_map(parse_version)
1114                        .any(|line_version| line_version == version);
1115                    let (stderr, stdout) = if is_installed {
1116                        (Stdio::null(), Stdio::null())
1117                    } else {
1118                        (Stdio::inherit(), Stdio::inherit())
1119                    };
1120
1121                    std::process::Command::new(cmd_name)
1122                        .arg("init")
1123                        .arg(&version)
1124                        .stderr(stderr)
1125                        .stdout(stdout)
1126                        .spawn()?
1127                        .wait()
1128                        .map(|status| status.success())
1129                        .map_err(|err| anyhow!("Failed to run `{cmd_name}` command: {err}"))
1130                }
1131
1132                match override_solana_version(solana_version.to_owned())? {
1133                    true => restore_cbs.push(Box::new(|| {
1134                        match override_solana_version(current_version)? {
1135                            true => Ok(()),
1136                            false => Err(anyhow!("Failed to restore `solana` version")),
1137                        }
1138                    })),
1139                    false => eprintln!(
1140                        "Failed to override `solana` version to {solana_version}, using \
1141                         {current_version} instead"
1142                    ),
1143                }
1144            }
1145        }
1146
1147        // Anchor version override should be handled last
1148        if let Some(anchor_version) = &cfg.toolchain.anchor_version {
1149            // Anchor binary name prefix(applies to binaries that are installed via `avm`)
1150            const ANCHOR_BINARY_PREFIX: &str = "anchor-";
1151
1152            // Get the current version from the executing binary name if possible because commit
1153            // based toolchain overrides do not have version information.
1154            let current_version = std::env::args()
1155                .next()
1156                .expect("First arg should exist")
1157                .parse::<PathBuf>()?
1158                .file_name()
1159                .and_then(|name| name.to_str())
1160                .expect("File name should be valid Unicode")
1161                .split_once(ANCHOR_BINARY_PREFIX)
1162                .map(|(_, version)| version)
1163                .unwrap_or(VERSION)
1164                .to_owned();
1165            if anchor_version != &current_version {
1166                let binary_path = home_dir()
1167                    .unwrap()
1168                    .join(".avm")
1169                    .join("bin")
1170                    .join(format!("{ANCHOR_BINARY_PREFIX}{anchor_version}"));
1171
1172                if !binary_path.exists() {
1173                    eprintln!(
1174                        "`anchor` {anchor_version} is not installed with `avm`. Installing...\n"
1175                    );
1176
1177                    if let Err(e) = install_with_avm(anchor_version, false) {
1178                        eprintln!(
1179                            "Failed to install `anchor`: {e}, using {current_version} instead"
1180                        );
1181                        return Ok(restore_cbs);
1182                    }
1183                }
1184
1185                let exit_code = std::process::Command::new(binary_path)
1186                    .args(std::env::args_os().skip(1))
1187                    .spawn()?
1188                    .wait()?
1189                    .code()
1190                    .unwrap_or(1);
1191                restore_toolchain(restore_cbs)?;
1192                std::process::exit(exit_code);
1193            }
1194        }
1195    }
1196
1197    Ok(restore_cbs)
1198}
1199
1200/// Installs Anchor using AVM, passing `--force` (and optionally) installing
1201/// `solana-verify`.
1202fn install_with_avm(version: &str, verify: bool) -> Result<()> {
1203    let mut cmd = std::process::Command::new("avm");
1204    cmd.arg("install");
1205    cmd.arg(version);
1206    cmd.arg("--force");
1207    if verify {
1208        cmd.arg("--verify");
1209    }
1210    let status = cmd.status().context("running AVM")?;
1211    if !status.success() {
1212        bail!("failed to install `anchor` {version} with avm");
1213    }
1214    Ok(())
1215}
1216
1217/// Restore toolchain to how it was before the command was run.
1218fn restore_toolchain(restore_cbs: RestoreToolchainCallbacks) -> Result<()> {
1219    for restore_toolchain in restore_cbs {
1220        if let Err(e) = restore_toolchain() {
1221            eprintln!("Toolchain error: {e}");
1222        }
1223    }
1224
1225    Ok(())
1226}
1227
1228/// Get the system's default license - what 'npm init' would use.
1229fn get_npm_init_license() -> Result<String> {
1230    let npm_init_license_output = std::process::Command::new("npm")
1231        .arg("config")
1232        .arg("get")
1233        .arg("init-license")
1234        .output()?;
1235
1236    if !npm_init_license_output.status.success() {
1237        return Err(anyhow!("Failed to get npm init license"));
1238    }
1239
1240    let license = String::from_utf8(npm_init_license_output.stdout)?;
1241    Ok(license.trim().to_string())
1242}
1243
1244fn process_command(opts: Opts) -> Result<()> {
1245    match opts.command {
1246        Command::Init {
1247            name,
1248            javascript,
1249            no_install,
1250            package_manager,
1251            no_git,
1252            template,
1253            test_template,
1254            force,
1255            install_agent_skills,
1256        } => init(
1257            &opts.cfg_override,
1258            name,
1259            javascript,
1260            no_install,
1261            package_manager,
1262            no_git,
1263            template,
1264            test_template,
1265            force,
1266            install_agent_skills,
1267        ),
1268        Command::New {
1269            name,
1270            template,
1271            force,
1272        } => new(&opts.cfg_override, name, template, force),
1273        Command::Build {
1274            no_idl,
1275            idl,
1276            idl_ts,
1277            verifiable,
1278            program_name,
1279            solana_version,
1280            docker_image,
1281            bootstrap,
1282            cargo_args,
1283            env,
1284            skip_lint,
1285            ignore_keys,
1286            no_docs,
1287        } => build(
1288            &opts.cfg_override,
1289            no_idl,
1290            idl,
1291            idl_ts,
1292            verifiable,
1293            skip_lint,
1294            ignore_keys,
1295            program_name,
1296            solana_version,
1297            docker_image,
1298            bootstrap,
1299            None,
1300            None,
1301            env,
1302            cargo_args,
1303            no_docs,
1304        ),
1305        Command::Verify {
1306            program_id,
1307            repo_url,
1308            commit_hash,
1309            current_dir,
1310            program_name,
1311            args,
1312        } => verify(
1313            program_id,
1314            repo_url,
1315            commit_hash,
1316            current_dir,
1317            program_name,
1318            args,
1319        ),
1320        Command::Clean => clean(&opts.cfg_override),
1321        #[allow(deprecated)]
1322        Command::Deploy {
1323            program_name,
1324            program_keypair,
1325            verifiable,
1326            no_idl,
1327            solana_args,
1328        } => {
1329            eprintln!(
1330                "Warning: 'anchor deploy' is deprecated. Use 'anchor program deploy' instead."
1331            );
1332            deploy(
1333                &opts.cfg_override,
1334                program_name,
1335                program_keypair,
1336                verifiable,
1337                no_idl,
1338                solana_args,
1339            )
1340        }
1341        Command::Expand {
1342            program_name,
1343            stdout,
1344            cargo_args,
1345        } => expand(&opts.cfg_override, program_name, stdout, &cargo_args),
1346        #[allow(deprecated)]
1347        Command::Upgrade {
1348            program_id,
1349            program_filepath,
1350            max_retries,
1351            solana_args,
1352        } => {
1353            eprintln!(
1354                "Warning: 'anchor upgrade' is deprecated. Use 'anchor program upgrade' instead."
1355            );
1356            upgrade(
1357                &opts.cfg_override,
1358                program_id,
1359                program_filepath,
1360                max_retries,
1361                solana_args,
1362            )
1363        }
1364        Command::Idl { subcmd } => idl(&opts.cfg_override, subcmd),
1365        Command::Migrate => migrate(&opts.cfg_override),
1366        Command::Test {
1367            program_name,
1368            skip_deploy,
1369            skip_local_validator,
1370            skip_build,
1371            no_idl,
1372            detach,
1373            run,
1374            validator,
1375            #[cfg(not(windows))]
1376            profile,
1377            args,
1378            env,
1379            cargo_args,
1380            skip_lint,
1381        } => {
1382            #[cfg(windows)]
1383            let profile = false;
1384
1385            test(
1386                &opts.cfg_override,
1387                program_name,
1388                skip_deploy,
1389                skip_local_validator,
1390                skip_build,
1391                skip_lint,
1392                no_idl,
1393                detach,
1394                run,
1395                validator,
1396                profile,
1397                false, // gdb — only `anchor debugger --gdb` enables this
1398                args,
1399                env,
1400                cargo_args,
1401            )
1402        }
1403        #[cfg(not(windows))]
1404        Command::Debugger {
1405            test_name,
1406            skip_run,
1407            skip_build,
1408            skip_lint,
1409            gdb,
1410            cargo_args,
1411        } => debugger(
1412            &opts.cfg_override,
1413            test_name,
1414            skip_run,
1415            skip_build,
1416            skip_lint,
1417            gdb,
1418            cargo_args,
1419        ),
1420        #[cfg(not(windows))]
1421        Command::Coverage {
1422            skip_run,
1423            skip_build,
1424            output,
1425            trace_dir,
1426            cargo_args,
1427        } => run_coverage(
1428            &opts.cfg_override,
1429            skip_run,
1430            skip_build,
1431            &output,
1432            &trace_dir,
1433            cargo_args,
1434        ),
1435        #[cfg(not(windows))]
1436        Command::CoverageFilterHost {
1437            sbf_lcov,
1438            host_lcov,
1439            output,
1440        } => coverage::filter_host_lcov(
1441            Path::new(&sbf_lcov),
1442            Path::new(&host_lcov),
1443            Path::new(&output),
1444        ),
1445        Command::Airdrop { amount, pubkey } => airdrop(&opts.cfg_override, amount, pubkey),
1446        Command::Cluster { subcmd } => cluster(subcmd),
1447        Command::Config { subcmd } => config_cmd(&opts.cfg_override, subcmd),
1448        Command::Shell => shell(&opts.cfg_override),
1449        Command::Run {
1450            script,
1451            script_args,
1452        } => run(&opts.cfg_override, script, script_args),
1453        Command::Keys { subcmd } => keys(&opts.cfg_override, subcmd),
1454        Command::Localnet {
1455            skip_build,
1456            skip_deploy,
1457            skip_lint,
1458            ignore_keys,
1459            validator,
1460            env,
1461            cargo_args,
1462        } => localnet(
1463            &opts.cfg_override,
1464            skip_build,
1465            skip_deploy,
1466            skip_lint,
1467            ignore_keys,
1468            validator,
1469            env,
1470            cargo_args,
1471        ),
1472        Command::Account {
1473            account_type,
1474            address,
1475            idl,
1476        } => account(&opts.cfg_override, account_type, address, idl),
1477        Command::Completions { shell } => {
1478            clap_complete::generate(
1479                shell,
1480                &mut Opts::command(),
1481                "anchor",
1482                &mut std::io::stdout(),
1483            );
1484            Ok(())
1485        }
1486        Command::Address => address(&opts.cfg_override),
1487        Command::Balance { pubkey, lamports } => balance(&opts.cfg_override, pubkey, lamports),
1488        Command::Epoch => epoch(&opts.cfg_override),
1489        Command::EpochInfo => epoch_info(&opts.cfg_override),
1490        Command::Logs {
1491            include_votes,
1492            address,
1493        } => logs_subscribe(&opts.cfg_override, include_votes, address),
1494        Command::ShowAccount { cmd } => account::show_account(&opts.cfg_override, cmd),
1495        Command::Keygen { subcmd } => keygen::keygen(&opts.cfg_override, subcmd),
1496        Command::Program { subcmd } => program::program(&opts.cfg_override, subcmd),
1497        Command::Codama { subcmd } => codama::entry(subcmd),
1498    }
1499}
1500
1501#[allow(clippy::too_many_arguments)]
1502fn init(
1503    cfg_override: &ConfigOverride,
1504    name: String,
1505    javascript: bool,
1506    no_install: bool,
1507    package_manager: Option<PackageManager>,
1508    no_git: bool,
1509    template: ProgramTemplate,
1510    test_template: TestTemplate,
1511    force: bool,
1512    install_agent_skills: bool,
1513) -> Result<()> {
1514    if !force && Config::discover(cfg_override)?.is_some() {
1515        return Err(anyhow!("Workspace already initialized"));
1516    }
1517
1518    // We need to format different cases for the dir and the name
1519    let rust_name = name.to_snake_case();
1520    let project_name = if name == rust_name {
1521        rust_name.clone()
1522    } else {
1523        name.to_kebab_case()
1524    };
1525
1526    // Additional keywords that have not been added to the `syn` crate as reserved words
1527    // https://github.com/dtolnay/syn/pull/1098
1528    let extra_keywords = ["async", "await", "try"];
1529    // Anchor converts to snake case before writing the program name
1530    if syn::parse_str::<syn::Ident>(&rust_name).is_err()
1531        || extra_keywords.contains(&rust_name.as_str())
1532    {
1533        return Err(anyhow!(
1534            "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.",
1535        ));
1536    }
1537
1538    if force {
1539        fs::create_dir_all(&project_name)?;
1540    } else {
1541        fs::create_dir(&project_name)?;
1542    }
1543    std::env::set_current_dir(&project_name)?;
1544    fs::create_dir_all("app")?;
1545
1546    let mut cfg = Config::default();
1547
1548    // Resolve once — errors early if an explicit choice is missing, or
1549    // waterfalls pnpm → yarn → npm when nothing was requested. The resolved
1550    // PM is persisted to Anchor.toml so subsequent `anchor test`/`deploy`
1551    // runs use the same one without re-running detection.
1552    let package_manager = resolve_package_manager(package_manager)?;
1553    let test_script = test_template.get_test_script(javascript, &package_manager);
1554    cfg.scripts.insert("test".to_owned(), test_script);
1555
1556    // In-process test templates (litesvm + mollusk) drive the Solana VM
1557    // inside the `cargo test` process, so auto-starting a validator at
1558    // `anchor test` time is pure overhead. Mark the workspace so the test
1559    // command knows to skip validator startup without the user having to pass
1560    // `--skip-local-validator` every run.
1561    if matches!(test_template, TestTemplate::Litesvm | TestTemplate::Mollusk) {
1562        cfg.skip_local_validator = Some(true);
1563    }
1564
1565    let package_manager_cmd = package_manager.to_string();
1566    cfg.toolchain.package_manager = Some(package_manager);
1567
1568    // Initialize .gitignore file
1569    fs::write(".gitignore", rust_template::git_ignore())?;
1570
1571    // Initialize .prettierignore file
1572    fs::write(".prettierignore", rust_template::prettier_ignore())?;
1573
1574    // Remove the default program if `--force` is passed
1575    if force {
1576        let default_program_dir = std::env::current_dir()?
1577            .join("programs")
1578            .join(&project_name);
1579        if default_program_dir.exists() {
1580            fs::remove_dir_all(default_program_dir)?;
1581        }
1582    }
1583
1584    // Build the program.
1585    rust_template::create_program(&project_name, template, Some(&test_template))?;
1586
1587    let program_id = rust_template::get_or_create_program_id(&rust_name, target_dir()?);
1588    let mut localnet = BTreeMap::new();
1589    localnet.insert(
1590        rust_name,
1591        ProgramDeployment {
1592            address: program_id,
1593            path: None,
1594            idl: None,
1595        },
1596    );
1597    cfg.programs.insert(Cluster::Localnet, localnet);
1598    let toml = cfg.to_string();
1599    fs::write("Anchor.toml", toml)?;
1600
1601    // Build the migrations directory.
1602    let migrations_path = Path::new("migrations");
1603    fs::create_dir_all(migrations_path)?;
1604
1605    let license = get_npm_init_license()?;
1606
1607    let jest = TestTemplate::Jest == test_template;
1608    if javascript {
1609        // Build javascript config
1610        let mut package_json = File::create("package.json")?;
1611        package_json.write_all(rust_template::package_json(jest, license).as_bytes())?;
1612
1613        let mut deploy = File::create(migrations_path.join("deploy.js"))?;
1614        deploy.write_all(rust_template::deploy_script().as_bytes())?;
1615    } else {
1616        // Build typescript config
1617        let mut ts_config = File::create("tsconfig.json")?;
1618        ts_config.write_all(rust_template::ts_config(jest).as_bytes())?;
1619
1620        let mut ts_package_json = File::create("package.json")?;
1621        ts_package_json.write_all(rust_template::ts_package_json(jest, license).as_bytes())?;
1622
1623        let mut deploy = File::create(migrations_path.join("deploy.ts"))?;
1624        deploy.write_all(rust_template::ts_deploy_script().as_bytes())?;
1625    }
1626
1627    test_template.create_test_files(&project_name, javascript, &program_id.to_string())?;
1628
1629    if !no_install {
1630        let output = install_node_modules(&package_manager_cmd)?;
1631        if !output.status.success() {
1632            eprintln!(
1633                "`{package_manager_cmd} install` failed (exit code {:?})",
1634                output.status.code()
1635            );
1636        }
1637    }
1638
1639    if !no_git {
1640        let git_result = std::process::Command::new("git")
1641            .arg("init")
1642            .stdout(Stdio::inherit())
1643            .stderr(Stdio::inherit())
1644            .output()
1645            .map_err(|e| anyhow::format_err!("git init failed: {}", e))?;
1646        if !git_result.status.success() {
1647            eprintln!("Failed to automatically initialize a new git repository");
1648        }
1649    }
1650
1651    if install_agent_skills {
1652        install_solana_skill();
1653    }
1654
1655    println!("{project_name} initialized");
1656
1657    Ok(())
1658}
1659
1660fn install_solana_skill() {
1661    const SKILL_REPO: &str = "https://github.com/solana-foundation/solana-dev-skill";
1662    const SKILL_NAME: &str = "solana-dev";
1663
1664    // Skip if globally installed (active across all projects already)
1665    if home_dir().is_some_and(|home| {
1666        home.join(".agents")
1667            .join("skills")
1668            .join(SKILL_NAME)
1669            .exists()
1670    }) {
1671        return;
1672    }
1673
1674    // Skip if already project-scoped (could be anchor init --force on existing folder)
1675    let project_path = Path::new(".agents").join("skills").join(SKILL_NAME);
1676    if project_path.exists() {
1677        return;
1678    }
1679
1680    println!("Installing Solana dev skill for Agents from {SKILL_REPO}");
1681
1682    let status = std::process::Command::new("npx")
1683        .args([
1684            "--yes",
1685            "skills@1.4.4",
1686            "add",
1687            SKILL_REPO,
1688            "--skill",
1689            "*",
1690            "-y",
1691        ])
1692        .stdout(Stdio::inherit())
1693        .stderr(Stdio::inherit())
1694        .status();
1695
1696    match status {
1697        Ok(s) if s.success() => {
1698            println!("Solana dev skill installed successfully");
1699        }
1700        _ => {
1701            eprintln!(
1702                "Warning: Failed to install Solana dev skill. Install manually with:\n  npx \
1703                 skills add {SKILL_REPO}"
1704            );
1705        }
1706    }
1707}
1708
1709/// Waterfall probed when no package manager is configured. Order matters —
1710/// pnpm is preferred (fastest install, disk-efficient), yarn for parity with
1711/// older Anchor defaults, npm as the universal fallback since it ships with
1712/// Node. Bun stays out of the waterfall: users who want it must opt in
1713/// explicitly since it's not commonly installed.
1714const PACKAGE_MANAGER_WATERFALL: &[PackageManager] = &[
1715    PackageManager::PNPM,
1716    PackageManager::Yarn,
1717    PackageManager::NPM,
1718];
1719
1720/// Check whether a package manager binary is on `PATH` without installing
1721/// anything. Runs `<cmd> --version` and discards output.
1722fn package_manager_available(pm: &PackageManager) -> bool {
1723    let cmd = pm.to_string();
1724    let mut command = if cfg!(target_os = "windows") {
1725        let mut c = std::process::Command::new("cmd");
1726        c.arg(format!("/C {cmd} --version"));
1727        c
1728    } else {
1729        let mut c = std::process::Command::new(&cmd);
1730        c.arg("--version");
1731        c
1732    };
1733    command
1734        .stdout(Stdio::null())
1735        .stderr(Stdio::null())
1736        .status()
1737        .map(|s| s.success())
1738        .unwrap_or(false)
1739}
1740
1741/// Pick the concrete package manager to use.
1742///
1743/// - `Some(pm)`: user stated a preference (CLI flag or `Anchor.toml`). The
1744///   binary must be on PATH; missing binary is a hard error so the user isn't
1745///   silently redirected to something they didn't pick.
1746/// - `None`: probe the `PACKAGE_MANAGER_WATERFALL` in order and take the first
1747///   hit. If the first probe in the waterfall was skipped, emit a warning so
1748///   the user knows which binaries were missing.
1749///
1750/// Errors only when nothing in the waterfall is installed (no pnpm, no yarn,
1751/// no npm — extremely unusual, but worth a clear message).
1752fn resolve_package_manager(explicit: Option<PackageManager>) -> Result<PackageManager> {
1753    if let Some(pm) = explicit {
1754        if !package_manager_available(&pm) {
1755            return Err(anyhow!(
1756                "`{pm}` was requested but is not on PATH. Install it or pick a different package \
1757                 manager with `--package-manager`."
1758            ));
1759        }
1760        return Ok(pm);
1761    }
1762
1763    let mut skipped = Vec::new();
1764    for candidate in PACKAGE_MANAGER_WATERFALL {
1765        if package_manager_available(candidate) {
1766            if !skipped.is_empty() {
1767                let missing = skipped
1768                    .iter()
1769                    .map(|pm: &PackageManager| pm.to_string())
1770                    .collect::<Vec<_>>()
1771                    .join(", ");
1772                eprintln!("warning: {missing} not found on PATH, using `{candidate}` instead");
1773            }
1774            return Ok(candidate.clone());
1775        }
1776        skipped.push(candidate.clone());
1777    }
1778
1779    Err(anyhow!(
1780        "No supported package manager found on PATH (tried pnpm, yarn, npm). Install one of them, \
1781         or re-run with `--no-install`."
1782    ))
1783}
1784
1785fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
1786    if cfg!(target_os = "windows") {
1787        std::process::Command::new("cmd")
1788            .arg(format!("/C {cmd} install"))
1789            .stdout(Stdio::inherit())
1790            .stderr(Stdio::inherit())
1791            .output()
1792            .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e))
1793    } else {
1794        std::process::Command::new(cmd)
1795            .arg("install")
1796            .stdout(Stdio::inherit())
1797            .stderr(Stdio::inherit())
1798            .output()
1799            .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e))
1800    }
1801}
1802
1803// Creates a new program crate in the `programs/<name>` directory.
1804fn new(
1805    cfg_override: &ConfigOverride,
1806    name: String,
1807    template: ProgramTemplate,
1808    force: bool,
1809) -> Result<()> {
1810    with_workspace(cfg_override, |cfg| -> Result<()> {
1811        match cfg.path().parent() {
1812            None => {
1813                println!("Unable to make new program");
1814            }
1815            Some(parent) => {
1816                std::env::set_current_dir(parent)?;
1817
1818                let cluster = cfg.provider.cluster.clone();
1819                let programs = cfg.programs.entry(cluster).or_default();
1820                if programs.contains_key(&name) {
1821                    if !force {
1822                        return Err(anyhow!("Program already exists"));
1823                    }
1824
1825                    // Delete all files within the program folder
1826                    fs::remove_dir_all(std::env::current_dir()?.join("programs").join(&name))?;
1827                }
1828
1829                rust_template::create_program(&name, template, None)?;
1830
1831                programs.insert(
1832                    name.clone(),
1833                    ProgramDeployment {
1834                        address: rust_template::get_or_create_program_id(&name, target_dir()?),
1835                        path: None,
1836                        idl: None,
1837                    },
1838                );
1839
1840                let toml = cfg.to_string();
1841                fs::write("Anchor.toml", toml)?;
1842
1843                println!("Created new program.");
1844            }
1845        };
1846        Ok(())
1847    })?
1848}
1849
1850/// Array of (path, content) tuple.
1851pub type Files = Vec<(PathBuf, String)>;
1852
1853/// Create files from the given (path, content) tuple array.
1854///
1855/// # Example
1856///
1857/// ```rust,no_run
1858/// # use anchor_cli::create_files;
1859/// # use std::path::PathBuf;
1860/// # fn main() -> anyhow::Result<()> {
1861/// let files = vec![(PathBuf::from("programs/my_program/src/lib.rs"), "// Content".to_string())];
1862/// create_files(&files)?;
1863/// # Ok(())
1864/// # }
1865/// ```
1866pub fn create_files(files: &Files) -> Result<()> {
1867    for (path, content) in files {
1868        let path = path
1869            .display()
1870            .to_string()
1871            .replace('/', std::path::MAIN_SEPARATOR_STR);
1872        let path = Path::new(&path);
1873        if path.exists() {
1874            continue;
1875        }
1876
1877        match path.extension() {
1878            Some(_) => {
1879                fs::create_dir_all(path.parent().unwrap())?;
1880                fs::write(path, content)?;
1881            }
1882            None => fs::create_dir_all(path)?,
1883        }
1884    }
1885
1886    Ok(())
1887}
1888
1889/// Override or create files from the given (path, content) tuple array.
1890///
1891/// # Example
1892///
1893/// ```rust,no_run
1894/// # use anchor_cli::override_or_create_files;
1895/// # use std::path::PathBuf;
1896/// # fn main() -> anyhow::Result<()> {
1897/// let files = vec![(PathBuf::from("test.rs"), "// Content".to_string())];
1898/// override_or_create_files(&files)?;
1899/// # Ok(())
1900/// # }
1901/// ```
1902pub fn override_or_create_files(files: &Files) -> Result<()> {
1903    for (path, content) in files {
1904        let path = Path::new(path);
1905        if path.exists() {
1906            let mut f = fs::OpenOptions::new()
1907                .write(true)
1908                .truncate(true)
1909                .open(path)?;
1910            f.write_all(content.as_bytes())?;
1911            f.flush()?;
1912        } else {
1913            fs::create_dir_all(path.parent().unwrap())?;
1914            fs::write(path, content)?;
1915        }
1916    }
1917
1918    Ok(())
1919}
1920
1921pub fn expand(
1922    cfg_override: &ConfigOverride,
1923    program_name: Option<String>,
1924    stdout: bool,
1925    cargo_args: &[String],
1926) -> Result<()> {
1927    // Change to the workspace member directory, if needed.
1928    if let Some(program_name) = program_name.as_ref() {
1929        cd_member(cfg_override, program_name)?;
1930    }
1931
1932    let workspace_cfg = Config::discover(cfg_override)?
1933        .ok_or_else(|| anyhow!("The 'anchor expand' command requires an Anchor workspace."))?;
1934    let cfg_parent = workspace_cfg.path().parent().expect("Invalid Anchor.toml");
1935    let cargo = Manifest::discover()?;
1936
1937    let expansions_path = cfg_parent.join(".anchor").join("expanded-macros");
1938    fs::create_dir_all(&expansions_path)?;
1939
1940    match cargo {
1941        // No Cargo.toml found, expand entire workspace
1942        None => expand_all(&workspace_cfg, expansions_path, stdout, cargo_args),
1943        // Cargo.toml is at root of workspace, expand entire workspace
1944        Some(cargo) if cargo.path().parent() == workspace_cfg.path().parent() => {
1945            expand_all(&workspace_cfg, expansions_path, stdout, cargo_args)
1946        }
1947        // Reaching this arm means Cargo.toml belongs to a single package. Expand it.
1948        Some(cargo) => expand_program(
1949            // If we found Cargo.toml, it must be in a directory so unwrap is safe
1950            cargo.path().parent().unwrap().to_path_buf(),
1951            expansions_path,
1952            stdout,
1953            cargo_args,
1954        ),
1955    }
1956}
1957
1958fn expand_all(
1959    workspace_cfg: &WithPath<Config>,
1960    expansions_path: PathBuf,
1961    stdout: bool,
1962    cargo_args: &[String],
1963) -> Result<()> {
1964    let cur_dir = std::env::current_dir()?;
1965    for p in workspace_cfg.get_rust_program_list()? {
1966        expand_program(p, expansions_path.clone(), stdout, cargo_args)?;
1967    }
1968    std::env::set_current_dir(cur_dir)?;
1969    Ok(())
1970}
1971
1972fn expand_program(
1973    program_path: PathBuf,
1974    expansions_path: PathBuf,
1975    stdout: bool,
1976    cargo_args: &[String],
1977) -> Result<()> {
1978    let cargo = Manifest::from_path(program_path.join("Cargo.toml"))
1979        .map_err(|_| anyhow!("Could not find Cargo.toml for program"))?;
1980    let package_name = &cargo
1981        .package
1982        .as_ref()
1983        .ok_or_else(|| anyhow!("Cargo config is missing a package"))?
1984        .name;
1985
1986    let mut cmd = std::process::Command::new("cargo");
1987    cmd.arg("expand")
1988        .arg("--target-dir")
1989        .arg(expansions_path.join("expand-target"))
1990        .arg("--package")
1991        .arg(package_name)
1992        .args(cargo_args);
1993
1994    let handle_err = |err| anyhow!("Failed to run `cargo expand`: {err}");
1995    let exit_on_err = |exit_status: ExitStatus| {
1996        if !exit_status.success() {
1997            eprintln!("'anchor expand' failed. Perhaps you have not installed 'cargo-expand'? https://github.com/dtolnay/cargo-expand#installation");
1998            std::process::exit(exit_status.code().unwrap_or(1));
1999        }
2000    };
2001
2002    if stdout {
2003        let status = cmd.status().map_err(handle_err)?;
2004        exit_on_err(status);
2005    } else {
2006        let output = cmd.stderr(Stdio::inherit()).output().map_err(handle_err)?;
2007        exit_on_err(output.status);
2008
2009        let program_expansions_path = expansions_path.join(package_name);
2010        fs::create_dir_all(&program_expansions_path)?;
2011
2012        let version = cargo.version();
2013        let time = chrono::Utc::now().to_string().replace(' ', "_");
2014        let file_path = program_expansions_path.join(format!("{package_name}-{version}-{time}.rs"));
2015        fs::write(&file_path, &output.stdout)?;
2016
2017        println!(
2018            "Expanded {} into file {}\n",
2019            package_name,
2020            file_path.to_string_lossy()
2021        );
2022    }
2023
2024    Ok(())
2025}
2026
2027#[allow(clippy::too_many_arguments)]
2028pub fn build(
2029    cfg_override: &ConfigOverride,
2030    no_idl: bool,
2031    idl: Option<String>,
2032    idl_ts: Option<String>,
2033    verifiable: bool,
2034    skip_lint: bool,
2035    ignore_keys: bool,
2036    program_name: Option<String>,
2037    solana_version: Option<String>,
2038    docker_image: Option<String>,
2039    bootstrap: BootstrapMode,
2040    stdout: Option<File>, // Used for the package registry server.
2041    stderr: Option<File>, // Used for the package registry server.
2042    env_vars: Vec<String>,
2043    cargo_args: Vec<String>,
2044    no_docs: bool,
2045) -> Result<()> {
2046    // Change to the workspace member directory, if needed.
2047    if let Some(program_name) = program_name.as_ref() {
2048        cd_member(cfg_override, program_name)?;
2049    }
2050    let cfg = Config::discover(cfg_override)?
2051        .ok_or_else(|| anyhow!("The 'anchor build' command requires an Anchor workspace."))?;
2052    let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
2053
2054    // Require overflow checks
2055    let workspace_cargo_toml_path = cfg_parent.join("Cargo.toml");
2056    if workspace_cargo_toml_path.exists() {
2057        check_overflow(workspace_cargo_toml_path)?;
2058    }
2059
2060    // Check whether there is a mismatch between CLI and crate/package versions
2061    check_anchor_version(&cfg).ok();
2062    check_deps(&cfg).ok();
2063
2064    // Check for program ID mismatches before building (skip if --ignore-keys is used), Always skipped in anchor test
2065    if !ignore_keys {
2066        check_program_id_mismatch(&cfg, program_name.clone())?;
2067    }
2068
2069    let idl_out = match idl {
2070        Some(idl) => Some(PathBuf::from(idl)),
2071        None => Some(target_dir()?.join("idl")),
2072    };
2073    fs::create_dir_all(idl_out.as_ref().unwrap())?;
2074
2075    let idl_ts_out = match idl_ts {
2076        Some(idl_ts) => Some(PathBuf::from(idl_ts)),
2077        None => Some(target_dir()?.join("types")),
2078    };
2079    fs::create_dir_all(idl_ts_out.as_ref().unwrap())?;
2080
2081    if !cfg.workspace.types.is_empty() {
2082        fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
2083    };
2084
2085    cfg.run_hooks(HookType::PreBuild)?;
2086
2087    let cargo = Manifest::discover()?;
2088    let build_config = BuildConfig {
2089        verifiable,
2090        solana_version: solana_version.or_else(|| cfg.toolchain.solana_version.clone()),
2091        docker_image: docker_image.unwrap_or_else(|| cfg.docker()),
2092        bootstrap,
2093    };
2094    match cargo {
2095        // No Cargo.toml so build the entire workspace.
2096        None => build_all(
2097            &cfg,
2098            cfg.path(),
2099            no_idl,
2100            idl_out.clone(),
2101            idl_ts_out.clone(),
2102            &build_config,
2103            stdout,
2104            stderr,
2105            env_vars,
2106            cargo_args,
2107            skip_lint,
2108            no_docs,
2109        )?,
2110        // If the Cargo.toml is at the root, build the entire workspace.
2111        Some(cargo) if cargo.path().parent() == cfg.path().parent() => build_all(
2112            &cfg,
2113            cfg.path(),
2114            no_idl,
2115            idl_out.clone(),
2116            idl_ts_out.clone(),
2117            &build_config,
2118            stdout,
2119            stderr,
2120            env_vars,
2121            cargo_args,
2122            skip_lint,
2123            no_docs,
2124        )?,
2125        // Cargo.toml represents a single package. Build it.
2126        Some(cargo) => build_rust_cwd(
2127            &cfg,
2128            cargo.path().to_path_buf(),
2129            no_idl,
2130            idl_out.clone(),
2131            idl_ts_out.clone(),
2132            &build_config,
2133            stdout,
2134            stderr,
2135            env_vars,
2136            cargo_args,
2137            skip_lint,
2138            no_docs,
2139        )?,
2140    }
2141    cfg.run_hooks(HookType::PostBuild)?;
2142
2143    // Auto-generate Codama clients when `[clients] auto = true` is set in
2144    // `Anchor.toml`. We do this after `PostBuild` so user hooks can still
2145    // mutate the IDL (or skip it via `--no-idl`) before the renderers run.
2146    if cfg.clients.auto && !no_idl {
2147        if let Some(idl_dir) = idl_out.as_ref() {
2148            let idl_paths = collect_idl_files(idl_dir)?;
2149            codama::auto_generate_for_workspace(&cfg.clients, cfg_parent, &idl_paths)?;
2150        }
2151    }
2152
2153    set_workspace_dir_or_exit();
2154
2155    Ok(())
2156}
2157
2158/// List every `*.json` IDL file directly inside `idl_dir`, sorted for
2159/// deterministic ordering. Used by the `[clients] auto = true` hook to feed
2160/// each program's IDL into Codama after `anchor build` finishes.
2161fn collect_idl_files(idl_dir: &Path) -> Result<Vec<PathBuf>> {
2162    let mut idls = Vec::new();
2163    if !idl_dir.exists() {
2164        return Ok(idls);
2165    }
2166    for entry in
2167        fs::read_dir(idl_dir).with_context(|| format!("Failed to read `{}`", idl_dir.display()))?
2168    {
2169        let entry = entry?;
2170        let path = entry.path();
2171        if path.is_file() && path.extension().is_some_and(|e| e == "json") {
2172            idls.push(path);
2173        }
2174    }
2175    idls.sort();
2176    Ok(idls)
2177}
2178
2179#[allow(clippy::too_many_arguments)]
2180fn build_all(
2181    cfg: &WithPath<Config>,
2182    cfg_path: &Path,
2183    no_idl: bool,
2184    idl_out: Option<PathBuf>,
2185    idl_ts_out: Option<PathBuf>,
2186    build_config: &BuildConfig,
2187    stdout: Option<File>, // Used for the package registry server.
2188    stderr: Option<File>, // Used for the package registry server.
2189    env_vars: Vec<String>,
2190    cargo_args: Vec<String>,
2191    skip_lint: bool,
2192    no_docs: bool,
2193) -> Result<()> {
2194    let cur_dir = std::env::current_dir()?;
2195    let r = match cfg_path.parent() {
2196        None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())),
2197        Some(_parent) => {
2198            for p in cfg.get_rust_program_list()? {
2199                build_rust_cwd(
2200                    cfg,
2201                    p.join("Cargo.toml"),
2202                    no_idl,
2203                    idl_out.clone(),
2204                    idl_ts_out.clone(),
2205                    build_config,
2206                    stdout.as_ref().map(|f| f.try_clone()).transpose()?,
2207                    stderr.as_ref().map(|f| f.try_clone()).transpose()?,
2208                    env_vars.clone(),
2209                    cargo_args.clone(),
2210                    skip_lint,
2211                    no_docs,
2212                )?;
2213            }
2214            Ok(())
2215        }
2216    };
2217    std::env::set_current_dir(cur_dir)?;
2218    r
2219}
2220
2221// Runs the build command outside of a workspace.
2222#[allow(clippy::too_many_arguments)]
2223fn build_rust_cwd(
2224    cfg: &WithPath<Config>,
2225    cargo_toml: PathBuf,
2226    no_idl: bool,
2227    idl_out: Option<PathBuf>,
2228    idl_ts_out: Option<PathBuf>,
2229    build_config: &BuildConfig,
2230    stdout: Option<File>,
2231    stderr: Option<File>,
2232    env_vars: Vec<String>,
2233    cargo_args: Vec<String>,
2234    skip_lint: bool,
2235    no_docs: bool,
2236) -> Result<()> {
2237    match cargo_toml.parent() {
2238        None => return Err(anyhow!("Unable to find parent")),
2239        Some(p) => std::env::set_current_dir(p)?,
2240    };
2241    match build_config.verifiable {
2242        false => _build_rust_cwd(
2243            cfg, no_idl, idl_out, idl_ts_out, skip_lint, no_docs, cargo_args,
2244        ),
2245        true => build_cwd_verifiable(
2246            cfg,
2247            cargo_toml,
2248            build_config,
2249            stdout,
2250            stderr,
2251            skip_lint,
2252            env_vars,
2253            cargo_args,
2254            no_docs,
2255        ),
2256    }
2257}
2258
2259// Builds an anchor program in a docker image and copies the build artifacts
2260// into the `target/` directory.
2261#[allow(clippy::too_many_arguments)]
2262fn build_cwd_verifiable(
2263    cfg: &WithPath<Config>,
2264    cargo_toml: PathBuf,
2265    build_config: &BuildConfig,
2266    stdout: Option<File>,
2267    stderr: Option<File>,
2268    skip_lint: bool,
2269    env_vars: Vec<String>,
2270    cargo_args: Vec<String>,
2271    no_docs: bool,
2272) -> Result<()> {
2273    // Create output dirs.
2274    let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
2275    let target_dir = target_dir()?;
2276    fs::create_dir_all(target_dir.join("verifiable"))?;
2277    fs::create_dir_all(target_dir.join("idl"))?;
2278    fs::create_dir_all(target_dir.join("types"))?;
2279    if !&cfg.workspace.types.is_empty() {
2280        fs::create_dir_all(workspace_dir.join(&cfg.workspace.types))?;
2281    }
2282
2283    let container_name = "anchor-program";
2284
2285    // Build the binary in docker.
2286    let result = docker_build(
2287        cfg,
2288        container_name,
2289        cargo_toml,
2290        build_config,
2291        stdout,
2292        stderr,
2293        env_vars,
2294        cargo_args.clone(),
2295    );
2296
2297    match &result {
2298        Err(e) => {
2299            eprintln!("Error during Docker build: {e:?}");
2300        }
2301        Ok(_) => {
2302            // Build the idl.
2303            println!("Extracting the IDL");
2304            let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
2305            // Write out the JSON file.
2306            println!("Writing the IDL file");
2307            let out_file = target_dir
2308                .join("idl")
2309                .join(&idl.metadata.name)
2310                .with_extension("json");
2311            write_idl(&idl, OutFile::File(out_file))?;
2312
2313            // Write out the TypeScript type.
2314            println!("Writing the .ts file");
2315            let ts_file = target_dir
2316                .join("types")
2317                .join(&idl.metadata.name)
2318                .with_extension("ts");
2319            fs::write(&ts_file, idl_ts(&idl)?)?;
2320
2321            // Copy out the TypeScript type.
2322            if !&cfg.workspace.types.is_empty() {
2323                fs::copy(
2324                    ts_file,
2325                    workspace_dir
2326                        .join(&cfg.workspace.types)
2327                        .join(idl.metadata.name)
2328                        .with_extension("ts"),
2329                )?;
2330            }
2331
2332            println!("Build success");
2333        }
2334    }
2335
2336    result
2337}
2338
2339#[allow(clippy::too_many_arguments)]
2340fn docker_build(
2341    cfg: &WithPath<Config>,
2342    container_name: &str,
2343    cargo_toml: PathBuf,
2344    build_config: &BuildConfig,
2345    stdout: Option<File>,
2346    stderr: Option<File>,
2347    env_vars: Vec<String>,
2348    cargo_args: Vec<String>,
2349) -> Result<()> {
2350    let binary_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
2351
2352    // Docker vars.
2353    let workdir = Path::new("/workdir");
2354    let volume_mount = format!(
2355        "{}:{}",
2356        cfg.path().parent().unwrap().canonicalize()?.display(),
2357        workdir.to_str().unwrap(),
2358    );
2359    println!("Using image {:?}", build_config.docker_image);
2360
2361    // Start the docker image running detached in the background.
2362    let target_dir = workdir.join("docker-target");
2363    println!("Run docker image");
2364    let exit = std::process::Command::new("docker")
2365        .args([
2366            "run",
2367            "-it",
2368            "-d",
2369            "--name",
2370            container_name,
2371            "--env",
2372            &format!(
2373                "CARGO_TARGET_DIR={}",
2374                target_dir.as_path().to_str().unwrap()
2375            ),
2376            "-v",
2377            &volume_mount,
2378            "-w",
2379            workdir.to_str().unwrap(),
2380            &build_config.docker_image,
2381            "bash",
2382        ])
2383        .stdout(Stdio::inherit())
2384        .stderr(Stdio::inherit())
2385        .output()
2386        .map_err(|e| anyhow::format_err!("Docker build failed: {}", e))?;
2387    if !exit.status.success() {
2388        return Err(anyhow!("Failed to build program"));
2389    }
2390
2391    let result = docker_prep(container_name, build_config).and_then(|_| {
2392        let cfg_parent = cfg.path().parent().unwrap();
2393        docker_build_bpf(
2394            container_name,
2395            cargo_toml.as_path(),
2396            cfg_parent,
2397            target_dir.as_path(),
2398            binary_name,
2399            stdout,
2400            stderr,
2401            env_vars,
2402            cargo_args,
2403        )
2404    });
2405
2406    // Cleanup regardless of errors
2407    docker_cleanup(container_name, target_dir.as_path())?;
2408
2409    // Done.
2410    result
2411}
2412
2413fn docker_prep(container_name: &str, build_config: &BuildConfig) -> Result<()> {
2414    // Set the solana version in the container, if given. Otherwise use the
2415    // default.
2416    match build_config.bootstrap {
2417        BootstrapMode::Debian => {
2418            // Install build requirements
2419            docker_exec(container_name, &["apt", "update"])?;
2420            docker_exec(
2421                container_name,
2422                &["apt", "install", "-y", "curl", "build-essential"],
2423            )?;
2424
2425            // Install Rust
2426            docker_exec(
2427                container_name,
2428                &["curl", "https://sh.rustup.rs", "-sfo", "rustup.sh"],
2429            )?;
2430            docker_exec(container_name, &["sh", "rustup.sh", "-y"])?;
2431            docker_exec(container_name, &["rm", "-f", "rustup.sh"])?;
2432        }
2433        BootstrapMode::None => {}
2434    }
2435
2436    if let Some(solana_version) = &build_config.solana_version {
2437        println!("Using solana version: {solana_version}");
2438
2439        // Install Solana CLI
2440        docker_exec(
2441            container_name,
2442            &[
2443                "curl",
2444                "-sSfL",
2445                &format!("https://release.anza.xyz/v{solana_version}/install",),
2446                "-o",
2447                "solana_installer.sh",
2448            ],
2449        )?;
2450        docker_exec(container_name, &["sh", "solana_installer.sh"])?;
2451        docker_exec(container_name, &["rm", "-f", "solana_installer.sh"])?;
2452    }
2453    Ok(())
2454}
2455
2456#[allow(clippy::too_many_arguments)]
2457fn docker_build_bpf(
2458    container_name: &str,
2459    cargo_toml: &Path,
2460    cfg_parent: &Path,
2461    target_dir: &Path,
2462    binary_name: String,
2463    stdout: Option<File>,
2464    stderr: Option<File>,
2465    env_vars: Vec<String>,
2466    cargo_args: Vec<String>,
2467) -> Result<()> {
2468    let manifest_path =
2469        pathdiff::diff_paths(cargo_toml.canonicalize()?, cfg_parent.canonicalize()?)
2470            .ok_or_else(|| anyhow!("Unable to diff paths"))?;
2471    println!(
2472        "Building {} manifest: {:?}",
2473        binary_name,
2474        manifest_path.display()
2475    );
2476
2477    // Execute the build.
2478    let exit = std::process::Command::new("docker")
2479        .args([
2480            "exec",
2481            "--env",
2482            "PATH=/root/.local/share/solana/install/active_release/bin:/root/.cargo/bin:/usr/\
2483             local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
2484        ])
2485        .args(
2486            env_vars
2487                .iter()
2488                .map(|x| ["--env", x.as_str()])
2489                .collect::<Vec<[&str; 2]>>()
2490                .concat(),
2491        )
2492        .args([container_name, "cargo"])
2493        .args(BUILD_SUBCOMMAND)
2494        .args(["--manifest-path", &manifest_path.display().to_string()])
2495        .args(cargo_args)
2496        .stdout(match stdout {
2497            None => Stdio::inherit(),
2498            Some(f) => f.into(),
2499        })
2500        .stderr(match stderr {
2501            None => Stdio::inherit(),
2502            Some(f) => f.into(),
2503        })
2504        .output()
2505        .map_err(|e| anyhow::format_err!("Docker build failed: {}", e))?;
2506    if !exit.status.success() {
2507        return Err(anyhow!("Failed to build program"));
2508    }
2509
2510    // Copy the binary out of the docker image.
2511    println!("Copying out the build artifacts");
2512    let out_file = crate::target_dir()?
2513        .join("verifiable")
2514        .join(&binary_name)
2515        .with_extension("so")
2516        .display()
2517        .to_string();
2518
2519    // This requires the target directory of any built program to be located at
2520    // the root of the workspace.
2521    let mut bin_path = target_dir.join("deploy");
2522    bin_path.push(format!("{binary_name}.so"));
2523    let bin_artifact = format!(
2524        "{}:{}",
2525        container_name,
2526        bin_path.as_path().to_str().unwrap()
2527    );
2528    let exit = std::process::Command::new("docker")
2529        .args(["cp", &bin_artifact, &out_file])
2530        .stdout(Stdio::inherit())
2531        .stderr(Stdio::inherit())
2532        .output()
2533        .map_err(|e| anyhow::format_err!("{}", e))?;
2534    if !exit.status.success() {
2535        Err(anyhow!(
2536            "Failed to copy binary out of docker. Is the target directory set correctly?"
2537        ))
2538    } else {
2539        Ok(())
2540    }
2541}
2542
2543fn docker_cleanup(container_name: &str, target_dir: &Path) -> Result<()> {
2544    // Wipe the generated docker-target dir.
2545    println!("Cleaning up the docker target directory");
2546    docker_exec(container_name, &["rm", "-rf", target_dir.to_str().unwrap()])?;
2547
2548    // Remove the docker image.
2549    println!("Removing the docker container");
2550    let exit = std::process::Command::new("docker")
2551        .args(["rm", "-f", container_name])
2552        .stdout(Stdio::inherit())
2553        .stderr(Stdio::inherit())
2554        .output()
2555        .map_err(|e| anyhow::format_err!("{}", e))?;
2556    if !exit.status.success() {
2557        println!("Unable to remove the docker container");
2558        std::process::exit(exit.status.code().unwrap_or(1));
2559    }
2560    Ok(())
2561}
2562
2563fn docker_exec(container_name: &str, args: &[&str]) -> Result<()> {
2564    let exit = std::process::Command::new("docker")
2565        .args([&["exec", container_name], args].concat())
2566        .stdout(Stdio::inherit())
2567        .stderr(Stdio::inherit())
2568        .output()
2569        .map_err(|e| anyhow!("Failed to run command \"{:?}\": {:?}", args, e))?;
2570    if !exit.status.success() {
2571        Err(anyhow!("Failed to run command: {:?}", args))
2572    } else {
2573        Ok(())
2574    }
2575}
2576
2577#[allow(clippy::too_many_arguments)]
2578fn _build_rust_cwd(
2579    cfg: &WithPath<Config>,
2580    no_idl: bool,
2581    idl_out: Option<PathBuf>,
2582    idl_ts_out: Option<PathBuf>,
2583    skip_lint: bool,
2584    no_docs: bool,
2585    cargo_args: Vec<String>,
2586) -> Result<()> {
2587    // Fail fast on missing `idl-build` feature before the ~10s SBF compile
2588    // so a misconfigured manifest surfaces immediately with the exact
2589    // snippet to add, not after a full build cycle.
2590    if !no_idl {
2591        check_idl_build_feature()?;
2592    }
2593
2594    // Preserves the historical behavior of `process::exit`-ing on build
2595    // failure rather than propagating an Err — `anchor build` shells out,
2596    // so a build failure should bubble straight to the user's exit code.
2597    if let Err(e) = cargo_build_sbf(None, &cargo_args) {
2598        eprintln!("{e}");
2599        std::process::exit(1);
2600    }
2601
2602    // Generate IDL
2603    if !no_idl {
2604        let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
2605
2606        // JSON out path.
2607        let out = match idl_out {
2608            None => PathBuf::from(".")
2609                .join(&idl.metadata.name)
2610                .with_extension("json"),
2611            Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("json")),
2612        };
2613        // TS out path.
2614        let ts_out = match idl_ts_out {
2615            None => PathBuf::from(".")
2616                .join(&idl.metadata.name)
2617                .with_extension("ts"),
2618            Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("ts")),
2619        };
2620
2621        // Write out the JSON file.
2622        write_idl(&idl, OutFile::File(out))?;
2623        // Write out the TypeScript type.
2624        fs::write(&ts_out, idl_ts(&idl)?)?;
2625
2626        // Copy out the TypeScript type.
2627        let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
2628        if !&cfg.workspace.types.is_empty() {
2629            fs::copy(
2630                &ts_out,
2631                cfg_parent
2632                    .join(&cfg.workspace.types)
2633                    .join(&idl.metadata.name)
2634                    .with_extension("ts"),
2635            )?;
2636        }
2637    }
2638
2639    Ok(())
2640}
2641
2642/// Subcommand + toolchain pin passed to cargo for every SBF build the CLI
2643/// invokes. `--tools-version v1.52` is the SBF platform-tools release the
2644/// rest of anchor's stack is tested against; using anything else risks
2645/// link errors against the bundled stdlib (e.g. `feature edition2024 is
2646/// required` when the user's host toolchain is too old to build modern
2647/// transitive deps).
2648pub const BUILD_SUBCOMMAND: &[&str] = &["build-sbf", "--tools-version", "v1.52"];
2649
2650/// Shell out to `cargo build-sbf` with our pinned toolchain
2651/// ([`BUILD_SUBCOMMAND`]) plus any user-supplied `extra_args`.
2652///
2653/// Optional `cwd` is honored when set, otherwise inherits the parent's
2654/// current dir. Stdio is inherited so the user sees the build live.
2655/// Returns `Err` on non-zero exit instead of `std::process::exit`-ing,
2656/// so callers (e.g. `anchor debugger` loose mode) can decide whether to
2657/// continue.
2658pub fn cargo_build_sbf(cwd: Option<&Path>, extra_args: &[String]) -> Result<()> {
2659    let mut cmd = std::process::Command::new("cargo");
2660    if let Some(d) = cwd {
2661        cmd.current_dir(d);
2662    }
2663    let status = cmd
2664        .args(BUILD_SUBCOMMAND)
2665        .args(extra_args)
2666        .stdout(Stdio::inherit())
2667        .stderr(Stdio::inherit())
2668        .status()
2669        .map_err(|e| anyhow!("spawn `cargo build-sbf`: {e}"))?;
2670    if !status.success() {
2671        return Err(anyhow!(
2672            "`cargo build-sbf` failed (exit {:?})",
2673            status.code()
2674        ));
2675    }
2676    Ok(())
2677}
2678
2679pub fn verify(
2680    program_id: Pubkey,
2681    repo_url: Option<String>,
2682    commit_hash: Option<String>,
2683    current_dir: bool,
2684    program_name: Option<String>,
2685    args: Vec<String>,
2686) -> Result<()> {
2687    let mut command_args = Vec::new();
2688
2689    match (current_dir, repo_url) {
2690        (true, _) => {
2691            let current_path = std::env::current_dir()?
2692                .to_str()
2693                .ok_or_else(|| anyhow!("Invalid current directory path"))?
2694                .to_owned();
2695            command_args.push(current_path);
2696            command_args.push("--current-dir".into());
2697        }
2698        (false, Some(url)) => {
2699            command_args.push(url);
2700        }
2701        (false, None) => {
2702            return Err(anyhow!(
2703                "You must provide either --repo-url or --current-dir"
2704            ));
2705        }
2706    }
2707
2708    if let Some(commit) = commit_hash {
2709        command_args.push("--commit-hash".into());
2710        command_args.push(commit);
2711    }
2712
2713    if let Some(name) = program_name {
2714        command_args.push("--library-name".into());
2715        command_args.push(name);
2716    }
2717
2718    command_args.push("--program-id".into());
2719    command_args.push(program_id.to_string());
2720
2721    command_args.extend(args);
2722
2723    println!("Verifying program {program_id}");
2724    let verify_path = AVM_HOME.join("bin").join("solana-verify");
2725    if !verify_path.exists() {
2726        install_with_avm(env!("CARGO_PKG_VERSION"), true)
2727            .context("installing Anchor with solana-verify")?;
2728    }
2729
2730    let status = std::process::Command::new(verify_path)
2731        .arg("verify-from-repo")
2732        .args(&command_args)
2733        .stdout(std::process::Stdio::inherit())
2734        .stderr(std::process::Stdio::inherit())
2735        .status()
2736        .with_context(|| "Failed to run `solana-verify`")?;
2737
2738    if !status.success() {
2739        return Err(anyhow!("Failed to verify program"));
2740    }
2741
2742    Ok(())
2743}
2744
2745fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> {
2746    // Change directories to the given `program_name`, using either Anchor or Cargo workspace
2747    let programs = program::get_programs_from_workspace(cfg_override, None)?;
2748
2749    for program in programs {
2750        let cargo_toml = program.path.join("Cargo.toml");
2751        if !cargo_toml.exists() {
2752            return Err(anyhow!(
2753                "Did not find Cargo.toml at the path: {}",
2754                program.path.display()
2755            ));
2756        }
2757
2758        let manifest = Manifest::from_path(&cargo_toml)?;
2759        let pkg_name = manifest.package().name();
2760        if program_name == pkg_name || program_name == program.lib_name {
2761            std::env::set_current_dir(&program.path)?;
2762            return Ok(());
2763        }
2764    }
2765
2766    Err(anyhow!("{} is not part of the workspace", program_name,))
2767}
2768
2769fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
2770    match subcmd {
2771        IdlCommand::Init {
2772            program_id,
2773            filepath,
2774            priority_fee,
2775            non_canonical,
2776            #[cfg(feature = "idl-localnet-testing")]
2777            allow_localnet,
2778        } => {
2779            #[cfg(feature = "idl-localnet-testing")]
2780            let allow_localnet = allow_localnet;
2781            #[cfg(not(feature = "idl-localnet-testing"))]
2782            let allow_localnet = false;
2783            idl_init(
2784                program_id,
2785                cfg_override,
2786                filepath,
2787                priority_fee,
2788                non_canonical,
2789                allow_localnet,
2790            )
2791        }
2792        IdlCommand::Upgrade {
2793            program_id,
2794            filepath,
2795            priority_fee,
2796            #[cfg(feature = "idl-localnet-testing")]
2797            allow_localnet,
2798        } => {
2799            #[cfg(feature = "idl-localnet-testing")]
2800            let allow_localnet = allow_localnet;
2801            #[cfg(not(feature = "idl-localnet-testing"))]
2802            let allow_localnet = false;
2803            idl_upgrade(
2804                program_id,
2805                cfg_override,
2806                filepath,
2807                priority_fee,
2808                allow_localnet,
2809            )
2810        }
2811        IdlCommand::Build {
2812            program_name,
2813            out,
2814            out_ts,
2815            no_docs,
2816            skip_lint,
2817            cargo_args,
2818        } => idl_build(
2819            cfg_override,
2820            program_name,
2821            out,
2822            out_ts,
2823            no_docs,
2824            skip_lint,
2825            cargo_args,
2826        ),
2827        IdlCommand::Fetch {
2828            program_id: address,
2829            out,
2830            non_canonical,
2831        } => idl_fetch(cfg_override, address, out, non_canonical),
2832        IdlCommand::Convert {
2833            path,
2834            out,
2835            program_id,
2836        } => idl_convert(path, out, program_id),
2837        IdlCommand::Type { path, out } => idl_type(path, out),
2838        IdlCommand::Close {
2839            program_id,
2840            seed,
2841            priority_fee,
2842        } => idl_close_metadata(cfg_override, program_id, seed, priority_fee),
2843        IdlCommand::CreateBuffer {
2844            filepath,
2845            priority_fee,
2846        } => idl_create_buffer(cfg_override, filepath, priority_fee),
2847        IdlCommand::SetBufferAuthority {
2848            buffer,
2849            new_authority,
2850            priority_fee,
2851        } => idl_set_buffer_authority(cfg_override, buffer, new_authority, priority_fee),
2852        IdlCommand::WriteBuffer {
2853            program_id,
2854            buffer,
2855            seed,
2856            close_buffer,
2857            priority_fee,
2858        } => idl_write_buffer_metadata(
2859            cfg_override,
2860            program_id,
2861            buffer,
2862            seed,
2863            close_buffer,
2864            priority_fee,
2865        ),
2866    }
2867}
2868
2869fn idl_init(
2870    program_id: Option<Pubkey>,
2871    cfg_override: &ConfigOverride,
2872    idl_filepath: PathBuf,
2873    priority_fee: Option<u64>,
2874    non_canonical: bool,
2875    allow_localnet: bool,
2876) -> Result<()> {
2877    // Get cluster URL and wallet path from Anchor config
2878    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
2879
2880    let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
2881    if is_localnet && !allow_localnet {
2882        #[cfg(feature = "idl-localnet-testing")]
2883        println!(
2884            "Skipping IDL initialization on localnet. To deploy on localnet, use --allow-localnet"
2885        );
2886        #[cfg(not(feature = "idl-localnet-testing"))]
2887        println!("Skipping IDL initialization on localnet");
2888        return Ok(());
2889    }
2890
2891    let program_id = match program_id {
2892        Some(id) => id.to_string(),
2893        _ => {
2894            let idl = fs::read(&idl_filepath)?;
2895            let idl = convert_idl(&idl)?;
2896            idl.address
2897        }
2898    };
2899
2900    let command = metadata::IdlCommand::funded(
2901        cluster_url,
2902        wallet_path,
2903        priority_fee,
2904        metadata::FundedIdlSubcommand::Write {
2905            program_id,
2906            idl_filepath: idl_filepath
2907                .to_str()
2908                .ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
2909                .to_string(),
2910            non_canonical,
2911        },
2912    );
2913
2914    if !command.status()?.success() {
2915        return Err(anyhow!("Failed to initialize IDL"));
2916    }
2917
2918    println!("IDL initialized.");
2919    Ok(())
2920}
2921
2922// Currently identical to `idl_init`, other than not accepting `non_canonical`
2923fn idl_upgrade(
2924    program_id: Option<Pubkey>,
2925    cfg_override: &ConfigOverride,
2926    idl_filepath: PathBuf,
2927    priority_fee: Option<u64>,
2928    allow_localnet: bool,
2929) -> Result<()> {
2930    // Get cluster URL and wallet path from Anchor config
2931    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
2932
2933    let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
2934    if is_localnet && !allow_localnet {
2935        #[cfg(feature = "idl-localnet-testing")]
2936        println!("Skipping IDL upgrade on localnet. To deploy on localnet, use --allow-localnet");
2937        #[cfg(not(feature = "idl-localnet-testing"))]
2938        println!("Skipping IDL upgrade on localnet");
2939        return Ok(());
2940    }
2941
2942    let program_id = match program_id {
2943        Some(id) => id.to_string(),
2944        _ => {
2945            let idl = fs::read(&idl_filepath)?;
2946            let idl = convert_idl(&idl)?;
2947            idl.address
2948        }
2949    };
2950
2951    let command = metadata::IdlCommand::funded(
2952        cluster_url,
2953        wallet_path,
2954        priority_fee,
2955        metadata::FundedIdlSubcommand::Write {
2956            program_id,
2957            idl_filepath: idl_filepath
2958                .to_str()
2959                .ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
2960                .to_string(),
2961            non_canonical: false,
2962        },
2963    );
2964    if !command.status()?.success() {
2965        return Err(anyhow!("Failed to upgrade IDL"));
2966    }
2967
2968    println!("IDL upgraded.");
2969    Ok(())
2970}
2971
2972fn idl_build(
2973    cfg_override: &ConfigOverride,
2974    program_name: Option<String>,
2975    out: Option<String>,
2976    out_ts: Option<String>,
2977    no_docs: bool,
2978    skip_lint: bool,
2979    cargo_args: Vec<String>,
2980) -> Result<()> {
2981    let cfg = Config::discover(cfg_override)?
2982        .ok_or_else(|| anyhow!("The 'anchor idl build' command requires an Anchor workspace."))?;
2983    let current_dir = std::env::current_dir()?;
2984    let program_path = match program_name {
2985        Some(name) => cfg.get_program(&name)?.path,
2986        None => {
2987            let programs = cfg.read_all_programs()?;
2988            if programs.len() == 1 {
2989                programs.into_iter().next().unwrap().path
2990            } else {
2991                programs
2992                    .into_iter()
2993                    .find(|program| program.path == current_dir)
2994                    .ok_or_else(|| anyhow!("Not in a program directory"))?
2995                    .path
2996            }
2997        }
2998    };
2999    std::env::set_current_dir(program_path)?;
3000    let idl = generate_idl(&cfg, skip_lint, no_docs, &cargo_args)?;
3001    std::env::set_current_dir(current_dir)?;
3002
3003    let out = match out {
3004        Some(path) => OutFile::File(PathBuf::from(path)),
3005        None => OutFile::Stdout,
3006    };
3007    write_idl(&idl, out)?;
3008
3009    if let Some(path) = out_ts {
3010        fs::write(path, idl_ts(&idl)?)?;
3011    }
3012
3013    Ok(())
3014}
3015
3016/// Generate IDL with method decided by whether manifest file has `idl-build` feature or not.
3017fn generate_idl(
3018    cfg: &WithPath<Config>,
3019    skip_lint: bool,
3020    no_docs: bool,
3021    cargo_args: &[String],
3022) -> Result<Idl> {
3023    check_idl_build_feature()?;
3024
3025    anchor_lang_idl::build::IdlBuilder::new()
3026        .resolution(cfg.features.resolution)
3027        .skip_lint(cfg.features.skip_lint || skip_lint)
3028        .no_docs(no_docs)
3029        .cargo_args(cargo_args.into())
3030        .build()
3031}
3032
3033fn idl_fetch(
3034    cfg_override: &ConfigOverride,
3035    address: Pubkey,
3036    out: Option<String>,
3037    non_canonical: bool,
3038) -> Result<()> {
3039    let (cluster_url, _) = get_cluster_and_wallet(cfg_override)?;
3040    let command = metadata::IdlCommand::unfunded(
3041        cluster_url,
3042        metadata::UnfundedIdlSubcommand::Fetch {
3043            program_id: address.to_string(),
3044            out,
3045            non_canonical,
3046        },
3047    );
3048
3049    if !command.status()?.success() {
3050        return Err(anyhow!("Failed to fetch IDL"));
3051    }
3052    Ok(())
3053}
3054
3055fn idl_convert(path: PathBuf, out: Option<PathBuf>, program_id: Option<Pubkey>) -> Result<()> {
3056    let idl = fs::read(path)?;
3057
3058    // Set the `metadata.address` field based on the given `program_id`
3059    let idl = match program_id {
3060        Some(program_id) => {
3061            let mut idl = serde_json::from_slice::<serde_json::Value>(&idl)?;
3062            idl.as_object_mut()
3063                .ok_or_else(|| anyhow!("IDL must be an object"))?
3064                .insert(
3065                    "metadata".into(),
3066                    serde_json::json!({ "address": program_id.to_string() }),
3067                );
3068            serde_json::to_vec(&idl)?
3069        }
3070        _ => idl,
3071    };
3072
3073    let idl = convert_idl(&idl)?;
3074    let out = match out {
3075        None => OutFile::Stdout,
3076        Some(out) => OutFile::File(out),
3077    };
3078    write_idl(&idl, out)
3079}
3080
3081fn idl_type(path: PathBuf, out: Option<PathBuf>) -> Result<()> {
3082    let idl = fs::read(path)?;
3083    let idl = convert_idl(&idl)?;
3084    let types = idl_ts(&idl)?;
3085    match out {
3086        Some(out) => fs::write(out, types)?,
3087        _ => println!("{types}"),
3088    };
3089    Ok(())
3090}
3091
3092fn idl_close_metadata(
3093    cfg_override: &ConfigOverride,
3094    program_id: Pubkey,
3095    seed: String,
3096    priority_fee: Option<u64>,
3097) -> Result<()> {
3098    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3099    let command = metadata::IdlCommand::funded(
3100        cluster_url,
3101        wallet_path,
3102        priority_fee,
3103        metadata::FundedIdlSubcommand::Close {
3104            program_id: program_id.to_string(),
3105            seed,
3106        },
3107    );
3108
3109    if !command.status()?.success() {
3110        return Err(anyhow!("Failed to close metadata account"));
3111    }
3112
3113    println!("Metadata account closed successfully.");
3114    Ok(())
3115}
3116
3117fn idl_create_buffer(
3118    cfg_override: &ConfigOverride,
3119    filepath: PathBuf,
3120    priority_fee: Option<u64>,
3121) -> Result<()> {
3122    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3123    let command = metadata::IdlCommand::funded(
3124        cluster_url,
3125        wallet_path,
3126        priority_fee,
3127        metadata::FundedIdlSubcommand::CreateBuffer {
3128            filepath: filepath
3129                .to_str()
3130                .ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
3131                .to_string(),
3132        },
3133    );
3134
3135    if !command.status()?.success() {
3136        return Err(anyhow!("Failed to create buffer"));
3137    }
3138
3139    println!("Buffer created successfully.");
3140    Ok(())
3141}
3142
3143fn idl_set_buffer_authority(
3144    cfg_override: &ConfigOverride,
3145    buffer: Pubkey,
3146    new_authority: Pubkey,
3147    priority_fee: Option<u64>,
3148) -> Result<()> {
3149    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3150    let command = metadata::IdlCommand::funded(
3151        cluster_url,
3152        wallet_path,
3153        priority_fee,
3154        metadata::FundedIdlSubcommand::SetBufferAuthority {
3155            buffer: buffer.to_string(),
3156            new_authority: new_authority.to_string(),
3157        },
3158    );
3159
3160    if !command.status()?.success() {
3161        return Err(anyhow!("Failed to set buffer authority"));
3162    }
3163
3164    println!("Buffer authority set successfully.");
3165    Ok(())
3166}
3167
3168fn idl_write_buffer_metadata(
3169    cfg_override: &ConfigOverride,
3170    program_id: Pubkey,
3171    buffer: Pubkey,
3172    seed: String,
3173    close_buffer: bool,
3174    priority_fee: Option<u64>,
3175) -> Result<()> {
3176    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
3177    let command = metadata::IdlCommand::funded(
3178        cluster_url,
3179        wallet_path,
3180        priority_fee,
3181        metadata::FundedIdlSubcommand::WriteBuffer {
3182            program_id: program_id.to_string(),
3183            buffer: buffer.to_string(),
3184            seed,
3185            close_buffer,
3186        },
3187    );
3188
3189    if !command.status()?.success() {
3190        return Err(anyhow!("Failed to write metadata using buffer"));
3191    }
3192
3193    println!("Metadata written successfully using buffer.");
3194    Ok(())
3195}
3196
3197fn idl_ts(idl: &Idl) -> Result<String> {
3198    let idl_name = &idl.metadata.name;
3199    let type_name = idl_name.to_pascal_case();
3200    let mut camel_idl = serde_json::to_value(idl)?;
3201    camel_case_idl_identifiers(&mut camel_idl);
3202    let camel_idl = serde_json::to_string_pretty(&serde_json::from_value::<Idl>(camel_idl)?)?;
3203
3204    Ok(format!(
3205        r#"/**
3206 * Program IDL in camelCase format in order to be used in JS/TS.
3207 *
3208 * Note that this is only a type helper and is not the actual IDL. The original
3209 * IDL can be found at `target/idl/{idl_name}.json`.
3210 */
3211export type {type_name} = {camel_idl};
3212"#
3213    ))
3214}
3215
3216fn camel_case_idl_identifiers(value: &mut JsonValue) {
3217    match value {
3218        JsonValue::Array(values) => {
3219            for value in values {
3220                camel_case_idl_identifiers(value);
3221            }
3222        }
3223        JsonValue::Object(map) => {
3224            for (key, value) in map {
3225                if is_idl_identifier_key(key) {
3226                    camel_case_idl_identifier(value);
3227                } else {
3228                    camel_case_idl_identifiers(value);
3229                }
3230            }
3231        }
3232        _ => {}
3233    }
3234}
3235
3236fn camel_case_idl_identifier(value: &mut JsonValue) {
3237    match value {
3238        JsonValue::String(s) => {
3239            if Pubkey::try_from(s.as_str()).is_err() {
3240                *s = s
3241                    .split('.')
3242                    .map(ToLowerCamelCase::to_lower_camel_case)
3243                    .collect::<Vec<_>>()
3244                    .join(".");
3245            }
3246        }
3247        JsonValue::Array(values) => {
3248            for value in values {
3249                camel_case_idl_identifier(value);
3250            }
3251        }
3252        _ => camel_case_idl_identifiers(value),
3253    }
3254}
3255
3256fn is_idl_identifier_key(key: &str) -> bool {
3257    matches!(key, "name" | "path" | "account" | "relations" | "generic")
3258}
3259
3260fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
3261    let idl_json = serde_json::to_string_pretty(idl)?;
3262    match out {
3263        OutFile::Stdout => println!("{idl_json}"),
3264        OutFile::File(out) => fs::write(out, idl_json)?,
3265    };
3266
3267    Ok(())
3268}
3269fn account(
3270    cfg_override: &ConfigOverride,
3271    account_type: String,
3272    address: Pubkey,
3273    idl_filepath: Option<PathBuf>,
3274) -> Result<()> {
3275    let (program_name, account_type_name) = account_type
3276        .split_once('.') // Split at first occurrence of dot
3277        .and_then(|(x, y)| y.find('.').map_or_else(|| Some((x, y)), |_| None)) // ensures no dots in second substring
3278        .ok_or_else(|| {
3279            anyhow!(
3280                "Please enter the account struct in the following format: <program_name>.<Account>",
3281            )
3282        })?;
3283
3284    let idl = idl_filepath.map_or_else(
3285        || {
3286            Config::discover(cfg_override)?
3287                .ok_or_else(|| {
3288                    anyhow!(
3289                        "The 'anchor account' command requires an Anchor workspace with \
3290                         Anchor.toml for IDL type generation."
3291                    )
3292                })?
3293                .read_all_programs()
3294                .expect("Workspace must contain atleast one program.")
3295                .into_iter()
3296                .find(|p| p.lib_name == *program_name)
3297                .ok_or_else(|| anyhow!("Program {program_name} not found in workspace."))
3298                .map(|p| p.idl)?
3299                .ok_or_else(|| {
3300                    anyhow!(
3301                        "IDL not found. Please build the program atleast once to generate the IDL."
3302                    )
3303                })
3304        },
3305        |idl_path| {
3306            let idl = fs::read(idl_path)?;
3307            let idl = convert_idl(&idl)?;
3308            if idl.metadata.name != program_name {
3309                return Err(anyhow!("IDL does not match program {program_name}."));
3310            }
3311
3312            Ok(idl)
3313        },
3314    )?;
3315
3316    let cluster = match &cfg_override.cluster {
3317        Some(cluster) => cluster.clone(),
3318        None => Config::discover(cfg_override)?
3319            .map(|cfg| cfg.provider.cluster.clone())
3320            .unwrap_or(Cluster::Localnet),
3321    };
3322
3323    let data = create_client(cluster.url()).get_account_data(&address)?;
3324    let disc_len = idl
3325        .accounts
3326        .iter()
3327        .find(|acc| acc.name == account_type_name)
3328        .map(|acc| acc.discriminator.len())
3329        .ok_or_else(|| anyhow!("Account `{account_type_name}` not found in IDL"))?;
3330    let mut data_view = &data[disc_len..];
3331
3332    let deserialized_json =
3333        deserialize_idl_defined_type_to_json(&idl, account_type_name, &mut data_view)?;
3334
3335    println!(
3336        "{}",
3337        serde_json::to_string_pretty(&deserialized_json).unwrap()
3338    );
3339
3340    Ok(())
3341}
3342
3343// Deserializes user defined IDL types by munching the account data(recursively).
3344fn deserialize_idl_defined_type_to_json(
3345    idl: &Idl,
3346    defined_type_name: &str,
3347    data: &mut &[u8],
3348) -> Result<JsonValue, anyhow::Error> {
3349    let defined_type = &idl
3350        .accounts
3351        .iter()
3352        .find(|acc| acc.name == defined_type_name)
3353        .and_then(|acc| idl.types.iter().find(|ty| ty.name == acc.name))
3354        .or_else(|| idl.types.iter().find(|ty| ty.name == defined_type_name))
3355        .ok_or_else(|| anyhow!("Type `{}` not found in IDL.", defined_type_name))?
3356        .ty;
3357
3358    let mut deserialized_fields = Map::new();
3359
3360    match defined_type {
3361        IdlTypeDefTy::Struct { fields } => {
3362            if let Some(fields) = fields {
3363                match fields {
3364                    IdlDefinedFields::Named(fields) => {
3365                        for field in fields {
3366                            deserialized_fields.insert(
3367                                field.name.clone(),
3368                                deserialize_idl_type_to_json(&field.ty, data, idl)?,
3369                            );
3370                        }
3371                    }
3372                    IdlDefinedFields::Tuple(fields) => {
3373                        let mut values = Vec::new();
3374                        for field in fields {
3375                            values.push(deserialize_idl_type_to_json(field, data, idl)?);
3376                        }
3377                        deserialized_fields
3378                            .insert(defined_type_name.to_owned(), JsonValue::Array(values));
3379                    }
3380                }
3381            }
3382        }
3383        IdlTypeDefTy::Enum { variants } => {
3384            let repr = <u8 as BorshDeserialize>::deserialize(data)?;
3385
3386            let variant = variants
3387                .get(repr as usize)
3388                .ok_or_else(|| anyhow!("Error while deserializing enum variant {repr}"))?;
3389
3390            let mut value = json!({});
3391
3392            if let Some(enum_field) = &variant.fields {
3393                match enum_field {
3394                    IdlDefinedFields::Named(fields) => {
3395                        let mut values = Map::new();
3396                        for field in fields {
3397                            values.insert(
3398                                field.name.clone(),
3399                                deserialize_idl_type_to_json(&field.ty, data, idl)?,
3400                            );
3401                        }
3402                        value = JsonValue::Object(values);
3403                    }
3404                    IdlDefinedFields::Tuple(fields) => {
3405                        let mut values = Vec::new();
3406                        for field in fields {
3407                            values.push(deserialize_idl_type_to_json(field, data, idl)?);
3408                        }
3409                        value = JsonValue::Array(values);
3410                    }
3411                }
3412            }
3413
3414            deserialized_fields.insert(variant.name.clone(), value);
3415        }
3416        IdlTypeDefTy::Type { alias } => {
3417            return deserialize_idl_type_to_json(alias, data, idl);
3418        }
3419    }
3420
3421    Ok(JsonValue::Object(deserialized_fields))
3422}
3423
3424// Deserializes a primitive type using BorshDeserialize
3425fn deserialize_idl_type_to_json(
3426    idl_type: &IdlType,
3427    data: &mut &[u8],
3428    parent_idl: &Idl,
3429) -> Result<JsonValue, anyhow::Error> {
3430    if data.is_empty() {
3431        return Err(anyhow::anyhow!("Unable to parse from empty bytes"));
3432    }
3433
3434    Ok(match idl_type {
3435        IdlType::Bool => json!(<bool as BorshDeserialize>::deserialize(data)?),
3436        IdlType::U8 => {
3437            json!(<u8 as BorshDeserialize>::deserialize(data)?)
3438        }
3439        IdlType::I8 => {
3440            json!(<i8 as BorshDeserialize>::deserialize(data)?)
3441        }
3442        IdlType::U16 => {
3443            json!(<u16 as BorshDeserialize>::deserialize(data)?)
3444        }
3445        IdlType::I16 => {
3446            json!(<i16 as BorshDeserialize>::deserialize(data)?)
3447        }
3448        IdlType::U32 => {
3449            json!(<u32 as BorshDeserialize>::deserialize(data)?)
3450        }
3451        IdlType::I32 => {
3452            json!(<i32 as BorshDeserialize>::deserialize(data)?)
3453        }
3454        IdlType::F32 => json!(<f32 as BorshDeserialize>::deserialize(data)?),
3455        IdlType::U64 => {
3456            json!(<u64 as BorshDeserialize>::deserialize(data)?)
3457        }
3458        IdlType::I64 => {
3459            json!(<i64 as BorshDeserialize>::deserialize(data)?)
3460        }
3461        IdlType::F64 => json!(<f64 as BorshDeserialize>::deserialize(data)?),
3462        IdlType::U128 => {
3463            json!(<u128 as BorshDeserialize>::deserialize(data)?)
3464        }
3465        IdlType::I128 => {
3466            json!(<i128 as BorshDeserialize>::deserialize(data)?)
3467        }
3468        IdlType::U256 => todo!("Upon completion of u256 IDL standard"),
3469        IdlType::I256 => todo!("Upon completion of i256 IDL standard"),
3470        IdlType::Bytes => JsonValue::Array(
3471            <Vec<u8> as BorshDeserialize>::deserialize(data)?
3472                .iter()
3473                .map(|i| json!(*i))
3474                .collect(),
3475        ),
3476        IdlType::String => json!(<String as BorshDeserialize>::deserialize(data)?),
3477        IdlType::Pubkey => {
3478            json!(<Pubkey as BorshDeserialize>::deserialize(data)?.to_string())
3479        }
3480        IdlType::Array(ty, size) => match size {
3481            IdlArrayLen::Value(size) => {
3482                let mut array_data: Vec<JsonValue> = Vec::with_capacity(*size);
3483
3484                for _ in 0..*size {
3485                    array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
3486                }
3487
3488                JsonValue::Array(array_data)
3489            }
3490            // TODO:
3491            IdlArrayLen::Generic(_) => unimplemented!("Generic array length is not yet supported"),
3492        },
3493        IdlType::Option(ty) => {
3494            let is_present = <u8 as BorshDeserialize>::deserialize(data)?;
3495
3496            if is_present == 0 {
3497                JsonValue::String("None".to_string())
3498            } else {
3499                deserialize_idl_type_to_json(ty, data, parent_idl)?
3500            }
3501        }
3502        IdlType::Vec(ty) => {
3503            let size: usize = <u32 as BorshDeserialize>::deserialize(data)?
3504                .try_into()
3505                .unwrap();
3506
3507            let mut vec_data: Vec<JsonValue> = Vec::with_capacity(size);
3508
3509            for _ in 0..size {
3510                vec_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
3511            }
3512
3513            JsonValue::Array(vec_data)
3514        }
3515        IdlType::Defined {
3516            name,
3517            generics: _generics,
3518        } => {
3519            // TODO: Generics
3520            deserialize_idl_defined_type_to_json(parent_idl, name, data)?
3521        }
3522        IdlType::Generic(generic) => json!(generic),
3523        _ => unimplemented!("{idl_type:?}"),
3524    })
3525}
3526
3527enum OutFile {
3528    Stdout,
3529    File(PathBuf),
3530}
3531
3532// Builds, deploys, and tests all workspace programs in a single command.
3533#[allow(clippy::too_many_arguments)]
3534fn test(
3535    cfg_override: &ConfigOverride,
3536    program_name: Option<String>,
3537    skip_deploy: bool,
3538    skip_local_validator: bool,
3539    skip_build: bool,
3540    skip_lint: bool,
3541    no_idl: bool,
3542    detach: bool,
3543    tests_to_run: Vec<String>,
3544    validator_type: ValidatorType,
3545    profile: bool,
3546    gdb: bool,
3547    extra_args: Vec<String>,
3548    env_vars: Vec<String>,
3549    cargo_args: Vec<String>,
3550) -> Result<()> {
3551    #[cfg(windows)]
3552    let _ = (profile, gdb);
3553
3554    let test_paths = tests_to_run
3555        .iter()
3556        .map(|path| {
3557            PathBuf::from(path)
3558                .canonicalize()
3559                .map_err(|_| anyhow!("Wrong path {}", path))
3560        })
3561        .collect::<Result<Vec<_>, _>>()?;
3562
3563    with_workspace(cfg_override, |cfg| -> Result<()> {
3564        // Set validator type based on CLI choice
3565        cfg.validator = Some(validator_type);
3566
3567        // Honor the persistent `skip_local_validator` flag from Anchor.toml
3568        // (emitted by `anchor init` for in-process templates) in addition to
3569        // the ad-hoc CLI flag.
3570        let skip_local_validator =
3571            skip_local_validator || cfg.skip_local_validator.unwrap_or(false);
3572
3573        // --profile setup: clear stale traces + point `anchor-v2-testing`
3574        // at our profile directory before the child `cargo test` runs.
3575        #[cfg(not(windows))]
3576        let workspace_root = cfg.path().parent().unwrap().to_owned();
3577        #[cfg(not(windows))]
3578        let profile_dir = workspace_root.join(crate::profile::DEFAULT_PROFILE_DIR);
3579        #[cfg(not(windows))]
3580        let _gdb_guard: Option<crate::debugger::gdb::GdbDriver> = if profile {
3581            let _ = fs::remove_dir_all(&profile_dir);
3582            std::env::set_var("ANCHOR_PROFILE_DIR", &profile_dir);
3583
3584            // Force DWARF into the release profile so the flamegraph
3585            // symbolicator can resolve inline frames. `CARGO_PROFILE_*`
3586            // is scoped to a specific cargo profile — only `cargo
3587            // build-sbf` (release) sees this; the IDL build (test
3588            // profile) and other cargo invocations are unaffected, so
3589            // the flag can't leak into commands that don't accept it.
3590            std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
3591
3592            // Rewrite `cargo test` → `cargo test --features profile` so the
3593            // user's test project picks up `anchor_v2_testing`'s profile
3594            // feature. For gdb mode also append `--test-threads=1` (libtest
3595            // flag, must come after `--`) because sbpf's `VM_DEBUG_PORT` is
3596            // process-wide env state and parallel tests race on bind.
3597            if let Some(test_script) = cfg.scripts.get_mut("test") {
3598                if test_script.contains("cargo test") {
3599                    *test_script =
3600                        test_script.replacen("cargo test", "cargo test --features profile", 1);
3601                    if gdb {
3602                        let sep = if test_script.contains(" -- ") {
3603                            " "
3604                        } else {
3605                            " -- "
3606                        };
3607                        *test_script = format!("{test_script}{sep}--test-threads=1");
3608                    }
3609                } else {
3610                    eprintln!(
3611                        "warning: --profile requires the `test` script in Anchor.toml to invoke \
3612                         `cargo test`; got: {test_script:?}. Profiling will not activate."
3613                    );
3614                }
3615            } else {
3616                eprintln!(
3617                    "warning: --profile requires a [scripts] test entry in Anchor.toml; none \
3618                     found. Profiling will not activate."
3619                );
3620            }
3621
3622            if gdb {
3623                let driver = crate::debugger::gdb::start_gdb_driver(&profile_dir)?;
3624                std::env::set_var(crate::debugger::gdb::SOCKET_ENV, driver.sock_path());
3625                std::env::set_var("RUST_TEST_THREADS", "1");
3626                Some(driver)
3627            } else {
3628                None
3629            }
3630        } else {
3631            None
3632        };
3633
3634        // Build if needed. Note: we don't inject `--debug` for --profile
3635        // because that flag leaks from `cargo_args` into other cargo
3636        // invocations that don't accept it (IDL build). The flamegraph
3637        // renderer falls back to the unstripped binary in
3638        // `target/sbpf-solana-solana/release/` for symbolication, which
3639        // works without debug info — users who want inline frames can
3640        // add `cargo build-sbf --debug` via their own workflow.
3641        if !skip_build {
3642            build(
3643                cfg_override,
3644                no_idl,
3645                None,
3646                None,
3647                false,
3648                skip_lint,
3649                true,
3650                program_name.clone(),
3651                None,
3652                None,
3653                BootstrapMode::None,
3654                None,
3655                None,
3656                env_vars,
3657                cargo_args,
3658                false,
3659            )?;
3660        }
3661
3662        let root = cfg.path().parent().unwrap().to_owned();
3663        cfg.add_test_config(root, test_paths)?;
3664
3665        // Deploy to the cluster unless told to skip. Skip the preemptive
3666        // `deploy()` (RPC upload via `solana program deploy`) on localnet:
3667        // the validator (surfpool / solana-test-validator) hasn't been
3668        // started yet — that happens later in `run_test_suite` — and it
3669        // loads programs itself via `surfpool_flags` / `validator_flags`.
3670        // Note: `skip_deploy` itself is preserved so surfpool's runbook
3671        // gating in `surfpool_flags` still respects the user's intent.
3672        let is_localnet = cfg.provider.cluster == Cluster::Localnet;
3673        if !skip_deploy && !is_localnet {
3674            deploy(cfg_override, None, None, false, true, vec![])?;
3675        }
3676
3677        cfg.run_hooks(HookType::PreTest)?;
3678
3679        let mut is_first_suite = true;
3680        if let Some(test_script) = cfg.scripts.get_mut("test") {
3681            is_first_suite = false;
3682
3683            match program_name {
3684                Some(program_name) => {
3685                    if let Some((from, to)) = Regex::new("\\s(tests/\\S+\\.(js|ts))")
3686                        .unwrap()
3687                        .captures_iter(&test_script.clone())
3688                        .last()
3689                        .and_then(|c| c.get(1).zip(c.get(2)))
3690                        .map(|(mtch, ext)| {
3691                            (
3692                                mtch.as_str(),
3693                                format!("tests/{program_name}.{}", ext.as_str()),
3694                            )
3695                        })
3696                    {
3697                        println!("\nRunning tests of program `{program_name}`!");
3698                        // Replace the last path to the program name's path
3699                        *test_script = test_script.replace(from, &to);
3700                    }
3701                }
3702                _ => println!(
3703                    "\nFound a 'test' script in the Anchor.toml. Running it as a test suite!"
3704                ),
3705            }
3706
3707            run_test_suite(
3708                cfg,
3709                cfg.path(),
3710                is_localnet,
3711                skip_local_validator,
3712                skip_deploy,
3713                detach,
3714                validator_type,
3715                &cfg.test_validator,
3716                &cfg.scripts,
3717                &extra_args,
3718                &cfg.surfpool_config,
3719            )?;
3720        }
3721        if let Some(test_config) = &cfg.test_config {
3722            for test_suite in test_config.iter() {
3723                if !is_first_suite {
3724                    std::thread::sleep(std::time::Duration::from_millis(
3725                        test_suite
3726                            .1
3727                            .test
3728                            .as_ref()
3729                            .map(|val| val.shutdown_wait)
3730                            .unwrap_or(SHUTDOWN_WAIT) as u64,
3731                    ));
3732                } else {
3733                    is_first_suite = false;
3734                }
3735
3736                run_test_suite(
3737                    cfg,
3738                    test_suite.0,
3739                    is_localnet,
3740                    skip_local_validator,
3741                    skip_deploy,
3742                    detach,
3743                    validator_type,
3744                    &test_suite.1.test,
3745                    &test_suite.1.scripts,
3746                    &extra_args,
3747                    &cfg.surfpool_config,
3748                )?;
3749            }
3750        }
3751        cfg.run_hooks(HookType::PostTest)?;
3752
3753        #[cfg(not(windows))]
3754        if profile {
3755            render_profile(cfg, &profile_dir)?;
3756        }
3757
3758        Ok(())
3759    })?
3760}
3761
3762/// Run the test suite (with profile tracing enabled) and then launch the
3763/// foundry-style SBF instruction stepper over the traces it produced.
3764///
3765/// Reuses the `--profile` environment setup so the trace-writing half stays
3766/// lock-stepped with the flamegraph renderer. `--skip-run` skips the
3767/// `anchor test` phase and opens the TUI directly — useful when iterating
3768/// on the TUI itself without paying for a rebuild each time.
3769#[allow(clippy::too_many_arguments)]
3770#[cfg(not(windows))]
3771fn debugger(
3772    cfg_override: &ConfigOverride,
3773    test_name: Option<String>,
3774    skip_run: bool,
3775    skip_build: bool,
3776    skip_lint: bool,
3777    gdb: bool,
3778    cargo_args: Vec<String>,
3779) -> Result<()> {
3780    // Dispatch on workspace shape:
3781    //
3782    //   - Anchor.toml present  → existing flow (build via `anchor build`,
3783    //     test via the `[scripts.test]` script, deploy mapping from
3784    //     `[programs.localnet]`).
3785    //   - No Anchor.toml       → loose mode: cargo workspace discovery,
3786    //     `cargo test --features profile` directly, deploy mapping from
3787    //     `target/deploy/*-keypair.json`.
3788    //
3789    // We probe Config::discover instead of with_workspace because the
3790    // latter exits the process on miss.
3791    let has_anchor_toml = match Config::discover(cfg_override) {
3792        Ok(Some(_)) => true,
3793        Ok(None) => false,
3794        Err(e) => {
3795            return Err(anyhow!("failed to probe for Anchor.toml: {e}"));
3796        }
3797    };
3798
3799    if has_anchor_toml {
3800        debugger_anchor_workspace(
3801            cfg_override,
3802            test_name,
3803            skip_run,
3804            skip_build,
3805            skip_lint,
3806            gdb,
3807            cargo_args,
3808        )
3809    } else {
3810        debugger_loose(
3811            cfg_override,
3812            test_name,
3813            skip_run,
3814            skip_build,
3815            gdb,
3816            cargo_args,
3817        )
3818    }
3819}
3820
3821#[allow(clippy::too_many_arguments)]
3822#[cfg(not(windows))]
3823fn debugger_anchor_workspace(
3824    cfg_override: &ConfigOverride,
3825    test_name: Option<String>,
3826    skip_run: bool,
3827    skip_build: bool,
3828    skip_lint: bool,
3829    gdb: bool,
3830    cargo_args: Vec<String>,
3831) -> Result<()> {
3832    if !skip_run {
3833        // LiteSVM is the only runtime that emits the register traces the
3834        // debugger consumes, so `anchor debugger` hardcodes both
3835        // `skip_deploy` and `skip_local_validator` to true — we neither
3836        // touch nor require a running validator.
3837        test(
3838            cfg_override,
3839            None,       // program_name
3840            true,       // skip_deploy
3841            true,       // skip_local_validator
3842            skip_build, // skip_build
3843            skip_lint,  // skip_lint
3844            true,       // no_idl — debugger never uses the IDL
3845            false,      // detach
3846            Vec::new(), // tests_to_run
3847            ValidatorType::Surfpool,
3848            true,       // profile — always on for --debugger
3849            gdb,        // gdb — drives traces via sbpf gdb-stub instead of inline
3850            Vec::new(), // extra_args
3851            Vec::new(), // env_vars
3852            cargo_args,
3853        )?;
3854    }
3855
3856    with_workspace(cfg_override, |cfg| -> Result<()> {
3857        let workspace_root = cfg.path().parent().unwrap().to_owned();
3858        let profile_dir = workspace_root.join(crate::profile::DEFAULT_PROFILE_DIR);
3859        let (pubkey_to_so, sources) = resolve_anchor_workspace_programs(cfg);
3860
3861        if pubkey_to_so.is_empty() {
3862            return Err(anyhow!(
3863                "no programs resolved for the debugger.\n\nEither declare them in Anchor.toml:\n  \
3864                 [programs.localnet]\n  <name> = \"<pubkey>\"\n\nor run `anchor build` so \
3865                 `target/deploy/<name>-keypair.json` exists."
3866            ));
3867        }
3868
3869        println!("\nResolved programs:");
3870        for (pk, so) in &pubkey_to_so {
3871            let src = sources.get(pk).copied().unwrap_or("unknown");
3872            println!("  {pk}  →  {}  [{src}]", display_path_relative_to_cwd(so));
3873        }
3874
3875        debugger::run(
3876            &profile_dir,
3877            &pubkey_to_so,
3878            Some(&workspace_root),
3879            None,
3880            test_name.as_deref(),
3881        )
3882    })?
3883}
3884
3885/// `anchor debugger` outside an Anchor workspace. Runs in any cargo
3886/// workspace whose member crates use `anchor-v2-testing` for tests.
3887///
3888/// Sanity-checks the workspace shape before doing anything destructive
3889/// (clearing the trace dir, running cargo test) so the user gets a clear
3890/// error early rather than a "no traces" mystery later.
3891#[allow(clippy::too_many_arguments)]
3892#[cfg(not(windows))]
3893fn debugger_loose(
3894    _cfg_override: &ConfigOverride,
3895    test_name: Option<String>,
3896    skip_run: bool,
3897    skip_build: bool,
3898    gdb: bool,
3899    cargo_args: Vec<String>,
3900) -> Result<()> {
3901    let cwd = std::env::current_dir().context("read current directory")?;
3902    let ws = debugger::loose::LooseWorkspace::discover(cwd)?;
3903
3904    if !skip_run {
3905        // Pre-flight checks before destructive ops. Anchor.toml flow
3906        // tolerates these because `anchor build` would have caught them;
3907        // here we own the contract.
3908        ws.check_dev_dep()?;
3909    }
3910    let profile_feature = ws.detect_profile_feature()?;
3911
3912    let profile_dir = ws.root.join(debugger::loose_profile_dir_name());
3913
3914    if !skip_run {
3915        debugger::loose::clear_profile_dir(&profile_dir)?;
3916        // Force DWARF into the SBF release profile so the source pane can
3917        // resolve PC → (file, line) via addr2line. Mirrors the env the
3918        // Anchor.toml flow sets in `test()`. Scoped to `CARGO_PROFILE_*`
3919        // so only the SBF build sees it — host-mode `cargo test` is
3920        // unaffected.
3921        std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
3922
3923        // The Solana cargo passes `-Zremap-cwd-prefix=` (empty) to rustc,
3924        // which strips DW_AT_comp_dir from the DWARF and makes all source
3925        // paths relative. This breaks the debugger's source pane when
3926        // multiple crates share filenames like `src/lib.rs`. We fix this
3927        // by setting RUSTC_WRAPPER to the anchor binary itself — a hidden
3928        // wrapper mode (see `debugger::rustc_wrapper`) that rewrites the
3929        // flag to `-Zremap-cwd-prefix=$CWD`, preserving absolute paths.
3930        let anchor_exe =
3931            std::env::current_exe().context("resolve anchor binary path for RUSTC_WRAPPER")?;
3932        std::env::set_var("RUSTC_WRAPPER", &anchor_exe);
3933        std::env::set_var(debugger::rustc_wrapper::WRAPPER_SENTINEL, "1");
3934
3935        // Build the SBF program first so `target/deploy/<name>.so` exists
3936        // (post-linked, sbpf-loadable). Skipping build-sbf is the #1 cause
3937        // of "everything runs but the disasm pane is empty" — the test
3938        // imports include_bytes!("../target/deploy/X.so") which depends
3939        // on this step. `--skip-build` opts out for fast TUI iteration.
3940        if !skip_build {
3941            // Reuse the same cargo-build-sbf invocation `anchor build`
3942            // uses (toolchain pinned via `BUILD_SUBCOMMAND`). Invoke from
3943            // the package's manifest dir — `cargo build-sbf` doesn't
3944            // accept top-level `-p`, so per-package selection is by cwd.
3945            let build_cwd = ws.cargo_invocation_dir();
3946            eprintln!("running `cargo build-sbf` from {}", build_cwd.display());
3947            cargo_build_sbf(Some(build_cwd), &cargo_args)?;
3948        }
3949
3950        // Clear the wrapper env before `cargo test` — the host-side test
3951        // build doesn't need it and RUSTC_WRAPPER would slow it down.
3952        std::env::remove_var("RUSTC_WRAPPER");
3953        std::env::remove_var(debugger::rustc_wrapper::WRAPPER_SENTINEL);
3954
3955        eprintln!(
3956            "running `cargo test{gdb} --features {profile_feature}{pkg}{filter}` from {dir}",
3957            gdb = if gdb { " [gdb mode]" } else { "" },
3958            pkg = ws
3959                .current_package
3960                .as_deref()
3961                .map(|p| format!(" -p {p}"))
3962                .unwrap_or_default(),
3963            filter = test_name
3964                .as_deref()
3965                .map(|f| format!(" -- {f}"))
3966                .unwrap_or_default(),
3967            dir = ws.cargo_invocation_dir().display(),
3968        );
3969        if gdb {
3970            debugger::gdb::run_gdb_mode(
3971                ws.cargo_invocation_dir(),
3972                ws.current_package.as_deref(),
3973                &profile_feature,
3974                &profile_dir,
3975                test_name.as_deref(),
3976            )?;
3977        } else {
3978            debugger::loose::run_cargo_test(
3979                ws.cargo_invocation_dir(),
3980                ws.current_package.as_deref(),
3981                &profile_feature,
3982                &profile_dir,
3983                test_name.as_deref(),
3984            )?;
3985        }
3986    }
3987
3988    let pubkey_to_so = debugger::loose::discover_programs(&ws.root, ws.current_package.as_deref())?;
3989    if pubkey_to_so.is_empty() {
3990        eprintln!(
3991            "warning: no programs found under {}/target/deploy/.\nELFs are required for \
3992             source/disasm symbolication. The debugger will still open but the static disasm pane \
3993             will be empty.",
3994            ws.root.display()
3995        );
3996    }
3997
3998    if !profile_dir.exists() {
3999        return Err(anyhow!(
4000            "no traces produced at {}.\n\nDid the test actually run? Check that:\n- the test \
4001             calls `anchor_v2_testing::svm()` (NOT `LiteSVM::new()`)\n- the `{profile_feature}` \
4002             feature is enabled in the test build\n- the test sent at least one transaction that \
4003             hit a BPF program",
4004            profile_dir.display()
4005        ));
4006    }
4007
4008    debugger::run(
4009        &profile_dir,
4010        &pubkey_to_so,
4011        Some(&ws.root),
4012        Some(&ws.cwd),
4013        test_name.as_deref(),
4014    )
4015}
4016
4017#[cfg(not(windows))]
4018fn run_coverage(
4019    _cfg_override: &ConfigOverride,
4020    skip_run: bool,
4021    skip_build: bool,
4022    output: &str,
4023    trace_dir: &str,
4024    cargo_args: Vec<String>,
4025) -> Result<()> {
4026    let cwd = std::env::current_dir().context("read current directory")?;
4027    let ws = debugger::loose::LooseWorkspace::discover(cwd)?;
4028
4029    let trace_path = ws.root.join(trace_dir);
4030    let output_path = ws.root.join(output);
4031
4032    if !skip_run {
4033        // Force DWARF into the SBF release profile so addr2line can resolve
4034        // PC → (file, line). Scoped to `CARGO_PROFILE_*` so only the SBF
4035        // build sees it — host-mode `cargo test` is unaffected.
4036        std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
4037
4038        // Solana's cargo passes `-Zremap-cwd-prefix=` (empty) which strips
4039        // `DW_AT_comp_dir`, making DWARF paths ambiguous when multiple
4040        // crates share filenames like `src/lib.rs`. The wrapper rewrites
4041        // the flag to `-Zremap-cwd-prefix=$CWD` so paths stay absolute.
4042        //
4043        // Stays set through `cargo test` below because tests-v2's
4044        // `build_program()` spawns `cargo build-sbf` from within the test
4045        // harness — those subprocess builds must inherit the wrapper to
4046        // produce DWARF with preserved paths.
4047        let anchor_exe =
4048            std::env::current_exe().context("resolve anchor binary path for RUSTC_WRAPPER")?;
4049        std::env::set_var("RUSTC_WRAPPER", &anchor_exe);
4050        std::env::set_var(debugger::rustc_wrapper::WRAPPER_SENTINEL, "1");
4051
4052        if !skip_build {
4053            let build_cwd = ws.cargo_invocation_dir();
4054            eprintln!("building programs with DWARF...");
4055            cargo_build_sbf(Some(build_cwd), &[])?;
4056        }
4057
4058        // Clear previous traces.
4059        if trace_path.exists() {
4060            fs::remove_dir_all(&trace_path)?;
4061        }
4062        fs::create_dir_all(&trace_path)?;
4063
4064        // Two register-tracing paths are in play, distinguished by whether
4065        // the current crate declares a `profile` feature that flips on
4066        // `anchor-v2-testing/profile`:
4067        //
4068        //   - With profile feature (anchor init template, bench programs):
4069        //     `anchor_v2_testing::svm()` installs the TestNameCallback that
4070        //     writes per-test nested traces under `ANCHOR_PROFILE_DIR`.
4071        //     Same pipeline that `anchor debugger` consumes.
4072        //   - Without (tests-v2): litesvm's stock register-tracing, enabled
4073        //     in Cargo.toml, fires when `SBF_TRACE_DIR` is set. Flat
4074        //     hash-keyed trace files in one dir.
4075        //
4076        // The coverage tool reads `.regs`/`.program_id` files recursively,
4077        // so both layouts work.
4078        let profile_feature = ws.detect_profile_feature().ok();
4079        eprintln!("running tests with register tracing...");
4080        let mut cmd = std::process::Command::new("cargo");
4081        cmd.current_dir(ws.cargo_invocation_dir()).arg("test");
4082        if let Some(feature) = &profile_feature {
4083            cmd.env("ANCHOR_PROFILE_DIR", &trace_path)
4084                .arg("--features")
4085                .arg(feature);
4086        } else {
4087            cmd.env("SBF_TRACE_DIR", &trace_path);
4088        }
4089        if let Some(pkg) = &ws.current_package {
4090            cmd.arg("-p").arg(pkg);
4091        }
4092        cmd.args(&cargo_args);
4093        let status = cmd.status().context("spawn cargo test")?;
4094        if !status.success() {
4095            return Err(anyhow::anyhow!("cargo test failed"));
4096        }
4097    }
4098
4099    if !trace_path.exists() {
4100        return Err(anyhow::anyhow!(
4101            "no traces at {}. Run without --skip-run first.",
4102            trace_path.display()
4103        ));
4104    }
4105
4106    // Discover program_id → deployed .so via declare_id! source scan
4107    // (same as debugger). The unstripped .so is resolved automatically per
4108    // program by walking up to its workspace root.
4109    let programs = debugger::loose::discover_programs(&ws.root, ws.current_package.as_deref())?;
4110    if programs.is_empty() {
4111        return Err(anyhow::anyhow!(
4112            "no programs found. Ensure declare_id!() is present in source.",
4113        ));
4114    }
4115
4116    if let Some(parent) = output_path.parent() {
4117        fs::create_dir_all(parent)?;
4118    }
4119    coverage::generate_lcov(&trace_path, &programs, Some(&ws.root), &output_path)?;
4120
4121    Ok(())
4122}
4123
4124/// Render path `p` as cwd-relative when possible, falling back to absolute.
4125#[cfg(not(windows))]
4126fn display_path_relative_to_cwd(p: &Path) -> String {
4127    std::env::current_dir()
4128        .ok()
4129        .as_deref()
4130        .and_then(|c| p.strip_prefix(c).ok())
4131        .map(|rel| rel.display().to_string())
4132        .unwrap_or_else(|| p.display().to_string())
4133}
4134
4135/// Build pubkey → deployed `.so` map for an Anchor.toml workspace.
4136///
4137/// Two sources, in priority order:
4138///   1. `[programs.*]` in `Anchor.toml` — explicit, authoritative.
4139///   2. `target/deploy/*-keypair.json` (loose-mode discovery) —
4140///      filled-in for projects whose Anchor.toml omits the program
4141///      mapping. Anchor.toml always wins on conflict.
4142///
4143/// Returns `(pubkey_to_so, sources)` where `sources[pk]` is `"Anchor.toml"`
4144/// or `"target/deploy"` for diagnostics.
4145#[cfg(not(windows))]
4146fn resolve_anchor_workspace_programs(
4147    cfg: &WithPath<Config>,
4148) -> (BTreeMap<String, PathBuf>, BTreeMap<String, &'static str>) {
4149    let workspace_root = cfg.path().parent().unwrap();
4150    let deploy_dir = workspace_root.join("target").join("deploy");
4151    let mut pubkey_to_so: BTreeMap<String, PathBuf> = BTreeMap::new();
4152    let mut sources: BTreeMap<String, &'static str> = BTreeMap::new();
4153    for programs in cfg.programs.values() {
4154        for (name, deployment) in programs {
4155            let pk = deployment.address.to_string();
4156            pubkey_to_so.insert(pk.clone(), deploy_dir.join(format!("{name}.so")));
4157            sources.insert(pk, "Anchor.toml");
4158        }
4159    }
4160    if let Ok(discovered) = debugger::loose::discover_programs(workspace_root, None) {
4161        for (pk, so) in discovered {
4162            if !pubkey_to_so.contains_key(&pk) {
4163                pubkey_to_so.insert(pk.clone(), so);
4164                sources.insert(pk, "target/deploy");
4165            }
4166        }
4167    }
4168    (pubkey_to_so, sources)
4169}
4170
4171/// Walks the per-test trace directories left behind by
4172/// `anchor-v2-testing`'s profile callback and renders one flamegraph
4173/// SVG per transaction under `<profile_dir>/<test>__tx<N>.svg`.
4174///
4175/// CPIs are symbolicated against the right ELF per invocation — a
4176/// tx that calls into spl-token shows spl-token's frames alongside
4177/// the program under test, not dropped or lumped under `[unknown]`.
4178#[cfg(not(windows))]
4179fn render_profile(cfg: &WithPath<Config>, profile_dir: &Path) -> Result<()> {
4180    let workspace_root = cfg.path().parent().unwrap().to_owned();
4181    let (pubkey_to_so, _sources) = resolve_anchor_workspace_programs(cfg);
4182
4183    let rendered = profile::render_all_tests(profile_dir, Some(&workspace_root), &pubkey_to_so)
4184        .context("failed to render flamegraphs from trace directory")?;
4185
4186    if rendered.is_empty() {
4187        eprintln!(
4188            "warning: no per-test trace directories found under {}. Did your tests call \
4189             `anchor_v2_testing::svm()` with the `profile` feature?",
4190            profile_dir.display()
4191        );
4192        return Ok(());
4193    }
4194
4195    let mut sorted: Vec<&profile::RenderedTest> = rendered.iter().collect();
4196    sorted.sort_by(|a, b| a.test_name.cmp(&b.test_name));
4197
4198    // Column width for the two-column single-tx layout. Multi-tx tests are
4199    // rendered nested so they don't influence alignment.
4200    let max_name = sorted
4201        .iter()
4202        .filter(|t| t.svg_paths.len() == 1)
4203        .map(|t| t.test_name.len())
4204        .max()
4205        .unwrap_or(0);
4206
4207    println!("\nFlamegraphs:");
4208    for test in &sorted {
4209        if test.svg_paths.len() == 1 {
4210            println!(
4211                "  {:<width$}  →  {}",
4212                test.test_name,
4213                display_path_relative_to_cwd(&test.svg_paths[0]),
4214                width = max_name,
4215            );
4216        } else {
4217            println!("  {}", test.test_name);
4218            for (i, svg) in test.svg_paths.iter().enumerate() {
4219                println!("    tx{}  →  {}", i + 1, display_path_relative_to_cwd(svg));
4220            }
4221        }
4222    }
4223
4224    Ok(())
4225}
4226
4227#[allow(clippy::too_many_arguments)]
4228fn run_test_suite(
4229    cfg: &WithPath<Config>,
4230    test_suite_path: impl AsRef<Path>,
4231    is_localnet: bool,
4232    skip_local_validator: bool,
4233    skip_deploy: bool,
4234    detach: bool,
4235    validator_type: ValidatorType,
4236    test_validator: &Option<TestValidator>,
4237    scripts: &ScriptsConfig,
4238    extra_args: &[String],
4239    surfpool_config: &Option<SurfpoolConfig>,
4240) -> Result<()> {
4241    println!("\nRunning test suite: {:#?}\n", test_suite_path.as_ref());
4242    let mut validator_handle = None;
4243    if is_localnet && !skip_local_validator {
4244        match validator_type {
4245            ValidatorType::Surfpool => {
4246                let full_simnet_mode = false;
4247                let flags = Some(surfpool_flags(
4248                    cfg,
4249                    surfpool_config,
4250                    full_simnet_mode,
4251                    skip_deploy,
4252                    Some(test_suite_path.as_ref()),
4253                )?);
4254                validator_handle = Some(start_surfpool_validator(
4255                    flags,
4256                    surfpool_config,
4257                    full_simnet_mode,
4258                )?);
4259            }
4260            ValidatorType::Legacy => {
4261                let flags = match skip_deploy {
4262                    true => None,
4263                    false => Some(validator_flags(cfg, test_validator)?),
4264                };
4265                validator_handle = Some(start_solana_test_validator(
4266                    cfg,
4267                    test_validator,
4268                    flags,
4269                    true,
4270                )?);
4271            }
4272        }
4273    }
4274    let url = cluster_url(cfg, test_validator, surfpool_config);
4275
4276    let node_options = format!(
4277        "{} {}",
4278        match std::env::var_os("NODE_OPTIONS") {
4279            Some(value) => value
4280                .into_string()
4281                .map_err(std::env::VarError::NotUnicode)?,
4282            None => "".to_owned(),
4283        },
4284        get_node_dns_option(),
4285    );
4286
4287    // Setup log reader - kept alive until end of scope
4288    let log_streams = match stream_logs(cfg, &url) {
4289        Ok(streams) => Some(streams),
4290        Err(e) => {
4291            eprintln!("Warning: Failed to setup program log streaming: {:#}", e);
4292            eprintln!("Program logs will still be visible in the test output.");
4293            None
4294        }
4295    };
4296
4297    // Run the tests.
4298    let test_result = {
4299        let cmd = scripts
4300            .get("test")
4301            .expect("Not able to find script for `test`")
4302            .clone();
4303        let script_args = format!("{cmd} {}", extra_args.join(" "));
4304
4305        std::process::Command::new("bash")
4306            .arg("-c")
4307            .arg(script_args)
4308            .env("ANCHOR_PROVIDER_URL", url)
4309            .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
4310            .env("NODE_OPTIONS", node_options)
4311            .stdout(Stdio::inherit())
4312            .stderr(Stdio::inherit())
4313            .output()
4314            .map_err(anyhow::Error::from)
4315            .context(cmd)
4316    };
4317
4318    // Keep validator running if needed.
4319    if test_result.is_ok() && detach {
4320        println!("Local validator still running. Press Ctrl + C quit.");
4321        std::io::stdin().lock().lines().next().unwrap().unwrap();
4322    }
4323
4324    // Check all errors and shut down.
4325    if let Some(mut child) = validator_handle {
4326        if let Err(err) = child.kill() {
4327            println!("Failed to kill subprocess {}: {}", child.id(), err);
4328        }
4329    }
4330
4331    // Explicitly shutdown log streams - closes WebSocket subscriptions
4332    if let Some(log_streams) = log_streams {
4333        for handle in log_streams {
4334            handle.shutdown();
4335        }
4336    }
4337
4338    // Must exist *after* shutting down the validator and log streams.
4339    match test_result {
4340        Ok(exit) => {
4341            if !exit.status.success() {
4342                std::process::exit(exit.status.code().unwrap());
4343            }
4344        }
4345        Err(err) => {
4346            println!("Failed to run test: {err:#}");
4347            return Err(err);
4348        }
4349    }
4350
4351    Ok(())
4352}
4353
4354// Returns the solana-test-validator flags. This will embed the workspace
4355// programs in the genesis block so we don't have to deploy every time. It also
4356// allows control of other solana-test-validator features.
4357fn validator_flags(
4358    cfg: &WithPath<Config>,
4359    test_validator: &Option<TestValidator>,
4360) -> Result<Vec<String>> {
4361    let programs = cfg.programs.get(&Cluster::Localnet);
4362
4363    let test_upgradeable_program = test_validator
4364        .as_ref()
4365        .map(|test_validator| test_validator.upgradeable)
4366        .unwrap_or(false);
4367
4368    let mut flags = Vec::new();
4369    for mut program in cfg.read_all_programs()? {
4370        let verifiable = false;
4371        let binary_path = program.binary_path(verifiable)?.display().to_string();
4372        // Use the [programs.cluster] override and fallback to the keypair
4373        // files if no override is given.
4374        let address = programs
4375            .and_then(|m| m.get(&program.lib_name))
4376            .map(|deployment| Ok(deployment.address.to_string()))
4377            .unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
4378
4379        if test_upgradeable_program {
4380            flags.push("--upgradeable-program".to_string());
4381            flags.push(address.clone());
4382            flags.push(binary_path);
4383            flags.push(cfg.wallet_kp()?.pubkey().to_string());
4384        } else {
4385            flags.push("--bpf-program".to_string());
4386            flags.push(address.clone());
4387            flags.push(binary_path);
4388        }
4389
4390        if let Some(idl) = program.idl.as_mut() {
4391            // Add program address to the IDL.
4392            idl.address = address;
4393
4394            // Persist it.
4395            let idl_out = target_dir()?
4396                .join("idl")
4397                .join(&idl.metadata.name)
4398                .with_extension("json");
4399            write_idl(idl, OutFile::File(idl_out))?;
4400        }
4401    }
4402
4403    if let Some(test) = test_validator.as_ref() {
4404        if let Some(genesis) = &test.genesis {
4405            for entry in genesis {
4406                let program_path = Path::new(&entry.program);
4407                if !program_path.exists() {
4408                    return Err(anyhow!(
4409                        "Program in genesis configuration does not exist at path: {}",
4410                        program_path.display()
4411                    ));
4412                }
4413                if entry.upgradeable.unwrap_or(false) {
4414                    flags.push("--upgradeable-program".to_string());
4415                    flags.push(entry.address.clone());
4416                    flags.push(entry.program.clone());
4417                    flags.push(cfg.wallet_kp()?.pubkey().to_string());
4418                } else {
4419                    flags.push("--bpf-program".to_string());
4420                    flags.push(entry.address.clone());
4421                    flags.push(entry.program.clone());
4422                }
4423            }
4424        }
4425        if let Some(validator) = &test.validator {
4426            let entries = serde_json::to_value(validator)?;
4427            for (key, value) in entries.as_object().unwrap() {
4428                if key == "ledger" {
4429                    // Ledger flag is a special case as it is passed separately to the rest of
4430                    // these validator flags.
4431                    continue;
4432                };
4433                if key == "account" {
4434                    for entry in value.as_array().unwrap() {
4435                        // Push the account flag for each array entry
4436                        flags.push("--account".to_string());
4437                        flags.push(entry["address"].as_str().unwrap().to_string());
4438                        flags.push(entry["filename"].as_str().unwrap().to_string());
4439                    }
4440                } else if key == "account_dir" {
4441                    for entry in value.as_array().unwrap() {
4442                        flags.push("--account-dir".to_string());
4443                        flags.push(entry["directory"].as_str().unwrap().to_string());
4444                    }
4445                } else if key == "clone" {
4446                    // Client for fetching accounts data
4447                    let client = if let Some(url) = entries["url"].as_str() {
4448                        create_client(url)
4449                    } else {
4450                        return Err(anyhow!(
4451                            "Validator url for Solana's JSON RPC should be provided in order to \
4452                             clone accounts from it"
4453                        ));
4454                    };
4455
4456                    let pubkeys = value
4457                        .as_array()
4458                        .unwrap()
4459                        .iter()
4460                        .map(|entry| {
4461                            let address = entry["address"].as_str().unwrap();
4462                            Pubkey::try_from(address)
4463                                .map_err(|_| anyhow!("Invalid pubkey {}", address))
4464                        })
4465                        .collect::<Result<HashSet<Pubkey>>>()?
4466                        .into_iter()
4467                        .collect::<Vec<_>>();
4468                    let accounts = client.get_multiple_accounts(&pubkeys)?;
4469
4470                    for (pubkey, account) in pubkeys.into_iter().zip(accounts) {
4471                        match account {
4472                            Some(account) => {
4473                                // Use a different flag for program accounts to fix the problem
4474                                // described in https://github.com/anza-xyz/agave/issues/522
4475                                if account.owner == bpf_loader_upgradeable::id()
4476                                    // Only programs are supported with `--clone-upgradeable-program`
4477                                    && matches!(
4478                                        account.deserialize_data::<UpgradeableLoaderState>()?,
4479                                        UpgradeableLoaderState::Program { .. }
4480                                    )
4481                                {
4482                                    flags.push("--clone-upgradeable-program".to_string());
4483                                    flags.push(pubkey.to_string());
4484                                } else {
4485                                    flags.push("--clone".to_string());
4486                                    flags.push(pubkey.to_string());
4487                                }
4488                            }
4489                            _ => return Err(anyhow!("Account {} not found", pubkey)),
4490                        }
4491                    }
4492                } else if key == "deactivate_feature" {
4493                    // Verify that the feature flags are valid pubkeys
4494                    let pubkeys_result: Result<Vec<Pubkey>, _> = value
4495                        .as_array()
4496                        .unwrap()
4497                        .iter()
4498                        .map(|entry| {
4499                            let feature_flag = entry.as_str().unwrap();
4500                            Pubkey::try_from(feature_flag).map_err(|_| {
4501                                anyhow!("Invalid pubkey (feature flag) {}", feature_flag)
4502                            })
4503                        })
4504                        .collect();
4505                    let features = pubkeys_result?;
4506                    for feature in features {
4507                        flags.push("--deactivate-feature".to_string());
4508                        flags.push(feature.to_string());
4509                    }
4510                } else {
4511                    // Remaining validator flags are non-array types
4512                    flags.push(format!("--{}", key.replace('_', "-")));
4513                    if let serde_json::Value::String(v) = value {
4514                        flags.push(v.to_string());
4515                    } else {
4516                        flags.push(value.to_string());
4517                    }
4518                }
4519            }
4520        }
4521    }
4522
4523    Ok(flags)
4524}
4525
4526// Returns Surfpool flags.
4527// This flags will be passed to the Surfpool, it allows to configure the validator.
4528fn surfpool_flags(
4529    cfg: &WithPath<Config>,
4530    surfpool_config: &Option<SurfpoolConfig>,
4531    full_simnet_mode: bool,
4532    skip_deploy: bool,
4533    test_suite_path: Option<&Path>,
4534) -> Result<Vec<String>> {
4535    let programs = cfg.programs.get(&Cluster::Localnet);
4536    let mut flags = Vec::new();
4537
4538    for mut program in cfg.read_all_programs()? {
4539        let address = programs
4540            .and_then(|m| m.get(&program.lib_name))
4541            .map(|deployment| Ok(deployment.address.to_string()))
4542            .unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
4543        if let Some(idl) = program.idl.as_mut() {
4544            // Creating the idl files
4545            idl.address = address;
4546            let idl_out = target_dir()?
4547                .join("idl")
4548                .join(&idl.metadata.name)
4549                .with_extension("json");
4550            write_idl(idl, OutFile::File(idl_out))?;
4551        }
4552    }
4553
4554    if let Some(config) = &surfpool_config {
4555        if let Some(airdrop_addresses) = &config.airdrop_addresses {
4556            for address in airdrop_addresses {
4557                flags.push("--airdrop".to_string());
4558                flags.push(address.to_string());
4559            }
4560        }
4561        if let Some(datasource_rpc_url) = &config.datasource_rpc_url {
4562            flags.push("--rpc-url".to_string());
4563            flags.push(datasource_rpc_url.to_string());
4564        }
4565
4566        let host = &config.host;
4567        flags.push("--host".to_string());
4568        flags.push(host.to_string());
4569
4570        let rpc_port = &config.rpc_port;
4571        flags.push("--port".to_string());
4572        flags.push(rpc_port.to_string());
4573
4574        if let Some(ws_port) = &config.ws_port {
4575            flags.push("--ws-port".to_string());
4576            flags.push(ws_port.to_string());
4577        }
4578
4579        if let Some(manifest_file_path) = &config.manifest_file_path {
4580            flags.push("--manifest-file-path".to_string());
4581            flags.push(manifest_file_path.to_string());
4582        }
4583
4584        if let Some(runbooks) = &config.runbooks {
4585            for runbook in runbooks {
4586                flags.push("--runbook".to_string());
4587                flags.push(runbook.to_string());
4588            }
4589        }
4590
4591        if let Some(slot_time) = &config.slot_time {
4592            flags.push("--slot-time".to_string());
4593            flags.push(slot_time.to_string());
4594        }
4595    }
4596
4597    let online = surfpool_config
4598        .as_ref()
4599        .and_then(|c| c.online)
4600        .unwrap_or(false);
4601    if !online {
4602        flags.push("--offline".to_string());
4603    }
4604
4605    let block_production_mode = surfpool_config
4606        .as_ref()
4607        .and_then(|c| c.block_production_mode.clone())
4608        .unwrap_or("transaction".into());
4609    flags.push("--block-production-mode".to_string());
4610    flags.push(block_production_mode);
4611
4612    flags.push("--log-level".to_string());
4613    flags.push(
4614        surfpool_config
4615            .as_ref()
4616            .and_then(|c| c.log_level.clone())
4617            .unwrap_or("none".into()),
4618    );
4619
4620    if !full_simnet_mode {
4621        flags.push("--no-tui".to_string());
4622        flags.push("--disable-instruction-profiling".to_string());
4623        flags.push("--max-profiles".to_string());
4624        flags.push("1".to_string());
4625        flags.push("--no-studio".to_string());
4626    }
4627
4628    // FIXME: drop this once surfpool's bundled mainnet feature defaults
4629    // catch up. Surfpool advertises "mainnet defaults" but its baked-in
4630    // feature set is stale and missing `deprecate_rent_exemption_threshold`,
4631    // which mainnet has had on for some time. Pinocchio's `Rent::get` only
4632    // reads `lamports_per_byte_year` and treats it as the post-multiplied
4633    // per-byte rate; that's correct under the feature (sysvar stores 6960,
4634    // threshold 1.0) but returns half on stale-default surfpool (sysvar
4635    // stores 3480, threshold 2.0), surfacing as `InsufficientFundsForRent`
4636    // at simulation. Forcing the feature here aligns surfpool with mainnet
4637    // until upstream refreshes its defaults.
4638    flags.push("--feature".to_string());
4639    flags.push("deprecate_rent_exemption_threshold".to_string());
4640
4641    match skip_deploy {
4642        true => flags.push("--no-deploy".to_string()),
4643        false => {
4644            // automatically generate in-memory runbooks
4645            flags.push("--legacy-anchor-compatibility".to_string());
4646            if let Some(test_suite_path) = test_suite_path {
4647                flags.push("--anchor-test-config-path".to_string());
4648                flags.push(test_suite_path.display().to_string());
4649            }
4650        }
4651    }
4652
4653    Ok(flags)
4654}
4655
4656/// Handle for a log streaming thread.
4657///
4658/// Manages a WebSocket subscription and its associated receiver thread.
4659/// Call `shutdown()` to cleanly stop the thread.
4660struct LogStreamHandle {
4661    subscription: PubsubClientSubscription<RpcResponse<RpcLogsResponse>>,
4662}
4663
4664impl LogStreamHandle {
4665    /// Explicitly shutdown the log stream
4666    fn shutdown(self) {
4667        // Send unsubscribe in a background thread to avoid blocking
4668        // PubsubClientSubscription::send_unsubscribe() can block indefinitely if WebSocket is stuck
4669        // The receiver threads will exit when the subscription closes
4670        std::thread::spawn(move || {
4671            let _ = self.subscription.send_unsubscribe();
4672        });
4673    }
4674}
4675
4676/// Spawns a thread to receive logs from a subscription and write them to a file
4677fn spawn_log_receiver_thread<R>(receiver: R, log_file_path: PathBuf)
4678where
4679    R: IntoIterator<Item = RpcResponse<RpcLogsResponse>> + Send + 'static,
4680{
4681    std::thread::spawn(move || {
4682        if let Ok(mut file) = File::create(&log_file_path) {
4683            for response in receiver {
4684                let _ = writeln!(
4685                    file,
4686                    "Transaction executed in slot {}:",
4687                    response.context.slot
4688                );
4689                let _ = writeln!(file, "  Signature: {}", response.value.signature);
4690                let _ = writeln!(
4691                    file,
4692                    "  Status: {}",
4693                    response
4694                        .value
4695                        .err
4696                        .map(|err| err.to_string())
4697                        .unwrap_or_else(|| "Ok".to_string())
4698                );
4699                let _ = writeln!(file, "  Log Messages:");
4700                for log in response.value.logs {
4701                    let _ = writeln!(file, "    {}", log);
4702                }
4703                let _ = writeln!(file); // Empty line between transactions
4704                let _ = file.flush();
4705            }
4706        } else {
4707            eprintln!("Failed to create log file: {:?}", log_file_path);
4708        }
4709    });
4710}
4711
4712fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStreamHandle>> {
4713    // Determine validator type to use appropriate logging
4714    match &config.validator {
4715        Some(ValidatorType::Surfpool) => {
4716            // For Surfpool, we don't need to stream logs via external commands
4717            // Surfpool handles its own logging to .surfpool/logs/ directory
4718            if config
4719                .surfpool_config
4720                .as_ref()
4721                .and_then(|s| {
4722                    s.log_level
4723                        .as_ref()
4724                        .map(|l| l.to_ascii_lowercase().ne("none"))
4725                })
4726                .unwrap_or(false)
4727            {
4728                println!("Surfpool validator logs: .surfpool/logs/ directory");
4729            }
4730            Ok(vec![])
4731        }
4732        Some(ValidatorType::Legacy) | None => stream_solana_logs(config, rpc_url),
4733    }
4734}
4735
4736fn stream_solana_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStreamHandle>> {
4737    let program_logs_dir = Path::new(".anchor").join("program-logs");
4738    if program_logs_dir.exists() {
4739        fs::remove_dir_all(&program_logs_dir)?;
4740    }
4741    fs::create_dir_all(&program_logs_dir)?;
4742
4743    // For solana-test-validator, the WebSocket port is RPC port + WEBSOCKET_PORT_OFFSET
4744    // Extract port from rpc_url and construct WebSocket URL
4745    let ws_url = if rpc_url.contains("127.0.0.1") || rpc_url.contains("localhost") {
4746        // Local validator: increment port by 1 for WebSocket
4747        let rpc_port = rpc_url
4748            .rsplit_once(':')
4749            .and_then(|(_, port)| port.parse::<u16>().ok())
4750            .unwrap_or(DEFAULT_RPC_PORT);
4751
4752        let ws_port = rpc_port + WEBSOCKET_PORT_OFFSET;
4753        let url = format!("ws://127.0.0.1:{}", ws_port);
4754        url
4755    } else {
4756        // Remote cluster: use same URL but replace http(s) with ws(s)
4757        rpc_url
4758            .replace("https://", "wss://")
4759            .replace("http://", "ws://")
4760    };
4761
4762    // Give the WebSocket endpoint a moment to be ready (especially for local validators)
4763    std::thread::sleep(std::time::Duration::from_millis(1500));
4764
4765    let mut handles = vec![];
4766
4767    // Subscribe to logs for all workspace programs
4768    for program in config.read_all_programs()? {
4769        let idl_path = target_dir()?
4770            .join("idl")
4771            .join(&program.lib_name)
4772            .with_extension("json");
4773        let idl = fs::read(&idl_path)?;
4774        let idl = convert_idl(&idl)?;
4775
4776        let log_file_path =
4777            program_logs_dir.join(format!("{}.{}.log", idl.address, program.lib_name));
4778        let program_address = idl.address.clone();
4779
4780        // Subscribe to logs using PubsubClient
4781        let (client, receiver) = match PubsubClient::logs_subscribe(
4782            &ws_url,
4783            RpcTransactionLogsFilter::Mentions(vec![program_address.clone()]),
4784            RpcTransactionLogsConfig {
4785                commitment: Some(CommitmentConfig::confirmed()),
4786            },
4787        ) {
4788            Ok(result) => result,
4789            Err(e) => {
4790                eprintln!(
4791                    "Warning: Failed to subscribe to logs for program {}: {}",
4792                    program.lib_name, e
4793                );
4794                continue;
4795            }
4796        };
4797
4798        // Spawn thread to write logs to file
4799        spawn_log_receiver_thread(receiver, log_file_path);
4800
4801        handles.push(LogStreamHandle {
4802            subscription: client,
4803        });
4804    }
4805
4806    // Also subscribe to logs for genesis programs
4807    if let Some(test) = config.test_validator.as_ref() {
4808        if let Some(genesis) = &test.genesis {
4809            for entry in genesis {
4810                let log_file_path = program_logs_dir.join(&entry.address).with_extension("log");
4811                let address = entry.address.clone();
4812
4813                // Subscribe to logs using PubsubClient
4814                let (client, receiver) = match PubsubClient::logs_subscribe(
4815                    &ws_url,
4816                    RpcTransactionLogsFilter::Mentions(vec![address.clone()]),
4817                    RpcTransactionLogsConfig {
4818                        commitment: Some(CommitmentConfig::confirmed()),
4819                    },
4820                ) {
4821                    Ok(result) => result,
4822                    Err(e) => {
4823                        eprintln!(
4824                            "Warning: Failed to subscribe to logs for genesis program {}: {}",
4825                            &entry.address, e
4826                        );
4827                        continue;
4828                    }
4829                };
4830
4831                // Spawn thread to write logs to file
4832                spawn_log_receiver_thread(receiver, log_file_path);
4833
4834                handles.push(LogStreamHandle {
4835                    subscription: client,
4836                });
4837            }
4838        }
4839    }
4840
4841    Ok(handles)
4842}
4843
4844fn start_surfpool_validator(
4845    flags: Option<Vec<String>>,
4846    surfpool_config: &Option<SurfpoolConfig>,
4847    full_simnet_mode: bool,
4848) -> Result<Child> {
4849    let (host, port) = match surfpool_config {
4850        Some(SurfpoolConfig { host, rpc_port, .. }) => (host.clone(), *rpc_port),
4851        _ => (SURFPOOL_HOST.to_string(), DEFAULT_RPC_PORT),
4852    };
4853    let rpc_url = surfpool_rpc_url(surfpool_config);
4854
4855    // Pre-spawn port-in-use detection. If someone is already bound to
4856    // `host:port` (stale surfpool, earlier `solana-test-validator`, whatever),
4857    // our new `surfpool` process would race for the bind and quietly lose —
4858    // then the startup health loop would succeed against the stranger's RPC
4859    // and `anchor test` would silently run against a foreign validator with
4860    // unknown state. Refuse up front and tell the user how to unblock.
4861    if std::net::TcpStream::connect_timeout(
4862        &format!("{host}:{port}")
4863            .parse()
4864            .map_err(|e| anyhow!("invalid surfpool host:port `{host}:{port}`: {e}"))?,
4865        std::time::Duration::from_millis(200),
4866    )
4867    .is_ok()
4868    {
4869        return Err(anyhow!(
4870            "port {port} on {host} is already in use — another validator is running there. Kill \
4871             it (e.g. `pkill -f surfpool`) or set `[surfpool] rpc_port = N` in Anchor.toml to \
4872             pick a free port."
4873        ));
4874    }
4875
4876    // Keep surfpool's stderr attached to the user's terminal even in non-
4877    // simnet mode so bind failures / panics surface immediately instead of
4878    // getting silently swallowed. Stdout stays nulled — surfpool's INFO-level
4879    // logs are noisy and not useful during a test run.
4880    let test_validator_stdout = match full_simnet_mode {
4881        true => Stdio::inherit(),
4882        false => Stdio::null(),
4883    };
4884
4885    let mut validator_handle = std::process::Command::new("surfpool")
4886        .arg("start")
4887        .args(flags.unwrap_or_default())
4888        .stdout(test_validator_stdout)
4889        .stderr(Stdio::inherit())
4890        .spawn()
4891        .map_err(|e| anyhow!("Failed to spawn `surfpool`: {e}"))?;
4892
4893    let client = create_client(rpc_url.clone());
4894
4895    let mut count = 0;
4896
4897    let ms_wait = surfpool_config
4898        .as_ref()
4899        .map(|surfpool| surfpool.startup_wait)
4900        .unwrap_or(STARTUP_WAIT);
4901
4902    while count < ms_wait {
4903        // If surfpool died during startup (most common cause: port bind
4904        // failure), bail out instead of letting the health poll fall through
4905        // to whatever foreign process happens to be answering on the port.
4906        if let Ok(Some(status)) = validator_handle.try_wait() {
4907            return Err(anyhow!(
4908                "`surfpool` exited during startup with {status} — see the stderr output above. \
4909                 Common causes: port {port} in use, missing deploy artifacts in `target/deploy/`, \
4910                 invalid Anchor.toml config."
4911            ));
4912        }
4913        let r = client.get_latest_blockhash();
4914        if r.is_ok() {
4915            break;
4916        }
4917        std::thread::sleep(std::time::Duration::from_millis(100));
4918        count += 100;
4919    }
4920
4921    if count >= ms_wait {
4922        eprintln!(
4923            "Unable to get latest blockhash. Surfpool validator does not look started. Check \
4924             .surfpool/logs/ directory for errors. Consider increasing [surfpool.startup_wait] in \
4925             Anchor.toml."
4926        );
4927        validator_handle.kill()?;
4928        std::process::exit(1);
4929    }
4930
4931    loop {
4932        let resp = client
4933            .send::<RpcResponse<SurfnetInfoResponse>>(
4934                RpcRequest::Custom {
4935                    method: "surfnet_getSurfnetInfo",
4936                },
4937                serde_json::Value::Null,
4938            )?
4939            .value;
4940
4941        // break out if all runbooks are completed
4942        if resp
4943            .runbook_executions
4944            .iter()
4945            .all(|ex| ex.completed_at.is_some())
4946        {
4947            break;
4948        }
4949        std::thread::sleep(std::time::Duration::from_millis(500));
4950    }
4951    Ok(validator_handle)
4952}
4953
4954fn start_solana_test_validator(
4955    cfg: &Config,
4956    test_validator: &Option<TestValidator>,
4957    flags: Option<Vec<String>>,
4958    test_log_stdout: bool,
4959) -> Result<Child> {
4960    let (test_ledger_directory, test_ledger_log_filename) =
4961        test_validator_file_paths(test_validator)?;
4962
4963    // Start a validator for testing.
4964    let (test_validator_stdout, test_validator_stderr) = match test_log_stdout {
4965        true => {
4966            let test_validator_stdout_file =
4967                File::create(&test_ledger_log_filename).with_context(|| {
4968                    format!(
4969                        "Failed to create validator log file {}",
4970                        test_ledger_log_filename.display()
4971                    )
4972                })?;
4973            let test_validator_sterr_file = test_validator_stdout_file.try_clone()?;
4974            (
4975                Stdio::from(test_validator_stdout_file),
4976                Stdio::from(test_validator_sterr_file),
4977            )
4978        }
4979        false => (Stdio::inherit(), Stdio::inherit()),
4980    };
4981
4982    let rpc_url = test_validator_rpc_url(test_validator);
4983
4984    let rpc_port = cfg
4985        .test_validator
4986        .as_ref()
4987        .and_then(|test| test.validator.as_ref().map(|v| v.rpc_port))
4988        .unwrap_or(DEFAULT_RPC_PORT);
4989    if !portpicker::is_free(rpc_port) {
4990        return Err(anyhow!(
4991            "Your configured rpc port: {rpc_port} is already in use"
4992        ));
4993    }
4994    let faucet_port = cfg
4995        .test_validator
4996        .as_ref()
4997        .and_then(|test| test.validator.as_ref().and_then(|v| v.faucet_port))
4998        .unwrap_or(DEFAULT_FAUCET_PORT);
4999    if !portpicker::is_free(faucet_port) {
5000        return Err(anyhow!(
5001            "Your configured faucet port: {faucet_port} is already in use"
5002        ));
5003    }
5004
5005    let mut validator_handle = std::process::Command::new("solana-test-validator")
5006        .arg("--ledger")
5007        .arg(test_ledger_directory)
5008        .arg("--mint")
5009        .arg(cfg.wallet_kp()?.pubkey().to_string())
5010        .args(flags.unwrap_or_default())
5011        .stdout(test_validator_stdout)
5012        .stderr(test_validator_stderr)
5013        .spawn()
5014        .map_err(|e| anyhow!("Failed to spawn `solana-test-validator`: {e}"))?;
5015
5016    // Wait for the validator to be ready.
5017    let client = create_client(rpc_url);
5018    let mut count = 0;
5019    let ms_wait = test_validator
5020        .as_ref()
5021        .map(|test| test.startup_wait)
5022        .unwrap_or(STARTUP_WAIT);
5023    while count < ms_wait {
5024        let r = client.get_latest_blockhash();
5025        if r.is_ok() {
5026            break;
5027        }
5028        std::thread::sleep(std::time::Duration::from_millis(100));
5029        count += 100;
5030    }
5031    if count >= ms_wait {
5032        eprintln!(
5033            "Unable to get latest blockhash. Test validator does not look started. Check \
5034             {test_ledger_log_filename:?} for errors. Consider increasing [test.startup_wait] in \
5035             Anchor.toml."
5036        );
5037        validator_handle.kill()?;
5038        std::process::exit(1);
5039    }
5040    Ok(validator_handle)
5041}
5042
5043// Return the URL that solana-test-validator should be running on given the
5044// configuration
5045fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
5046    match test_validator {
5047        Some(TestValidator {
5048            validator: Some(validator),
5049            ..
5050        }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
5051        _ => "http://127.0.0.1:8899".to_string(),
5052    }
5053}
5054
5055// Returns the URL that surfpool should be running for the given configuration
5056fn surfpool_rpc_url(surfpool_config: &Option<SurfpoolConfig>) -> String {
5057    match surfpool_config {
5058        Some(SurfpoolConfig { host, rpc_port, .. }) => format!("http://{}:{}", host, rpc_port),
5059        _ => format!("http://{}:{}", SURFPOOL_HOST, DEFAULT_RPC_PORT),
5060    }
5061}
5062
5063// Setup and return paths to the solana-test-validator ledger directory and log
5064// files given the configuration
5065fn test_validator_file_paths(test_validator: &Option<TestValidator>) -> Result<(PathBuf, PathBuf)> {
5066    let ledger_path = match test_validator {
5067        Some(TestValidator {
5068            validator: Some(validator),
5069            ..
5070        }) => PathBuf::from(&validator.ledger),
5071        _ => get_default_ledger_path(),
5072    };
5073
5074    if !ledger_path.is_relative() {
5075        // Prevent absolute paths to avoid someone using / or similar, as the
5076        // directory gets removed
5077        eprintln!("Ledger directory {ledger_path:?} must be relative");
5078        std::process::exit(1);
5079    }
5080    if ledger_path.exists() {
5081        fs::remove_dir_all(&ledger_path).with_context(|| {
5082            format!(
5083                "Failed to remove ledger directory {}",
5084                ledger_path.display()
5085            )
5086        })?;
5087    }
5088
5089    fs::create_dir_all(&ledger_path).with_context(|| {
5090        format!(
5091            "Failed to create ledger directory {}",
5092            ledger_path.display()
5093        )
5094    })?;
5095
5096    let log_path = ledger_path.join("test-ledger-log.txt");
5097    Ok((ledger_path, log_path))
5098}
5099
5100fn cluster_url(
5101    cfg: &Config,
5102    test_validator: &Option<TestValidator>,
5103    surfpool_config: &Option<SurfpoolConfig>,
5104) -> String {
5105    let is_localnet = cfg.provider.cluster == Cluster::Localnet;
5106    match is_localnet {
5107        // Cluster is Localnet, determine which validator to use
5108        true => match &cfg.validator {
5109            Some(ValidatorType::Surfpool) => surfpool_rpc_url(surfpool_config),
5110            Some(ValidatorType::Legacy) | None => test_validator_rpc_url(test_validator),
5111        },
5112        false => cfg.provider.cluster.url().to_string(),
5113    }
5114}
5115
5116fn clean(cfg_override: &ConfigOverride) -> Result<()> {
5117    // Get workspace root - either from Anchor.toml or use current directory
5118    let workspace_root = if let Ok(Some(cfg)) = Config::discover(cfg_override) {
5119        cfg.path()
5120            .parent()
5121            .expect("Invalid Anchor.toml")
5122            .to_path_buf()
5123    } else {
5124        // No Anchor.toml - use current directory for Cargo workspace
5125        std::env::current_dir()?
5126    };
5127
5128    let dot_anchor_dir = workspace_root.join(".anchor");
5129    let target_dir = crate::target_dir()?;
5130    let deploy_dir = target_dir.join("deploy");
5131
5132    if dot_anchor_dir.exists() {
5133        fs::remove_dir_all(&dot_anchor_dir)
5134            .map_err(|e| anyhow!("Could not remove directory {:?}: {}", dot_anchor_dir, e))?;
5135    }
5136
5137    if target_dir.exists() {
5138        for entry in fs::read_dir(target_dir)? {
5139            let path = entry?.path();
5140            if path.is_dir() && path != deploy_dir {
5141                fs::remove_dir_all(&path)
5142                    .map_err(|e| anyhow!("Could not remove directory {}: {}", path.display(), e))?;
5143            } else if path.is_file() {
5144                fs::remove_file(&path)
5145                    .map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
5146            }
5147        }
5148    } else {
5149        println!("skipping target directory: not found")
5150    }
5151
5152    if deploy_dir.exists() {
5153        for file in fs::read_dir(deploy_dir)? {
5154            let path = file?.path();
5155            if path.extension() != Some(&OsString::from("json")) {
5156                fs::remove_file(&path)
5157                    .map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
5158            }
5159        }
5160    } else {
5161        println!("skipping deploy directory: not found")
5162    }
5163
5164    Ok(())
5165}
5166
5167fn deploy(
5168    cfg_override: &ConfigOverride,
5169    program_name: Option<String>,
5170    program_keypair: Option<PathBuf>,
5171    verifiable: bool,
5172    no_idl: bool,
5173    solana_args: Vec<String>,
5174) -> Result<()> {
5175    // Execute the code within the workspace
5176    with_workspace(cfg_override, |cfg| -> Result<()> {
5177        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
5178        let keypair = cfg.provider.wallet.to_string();
5179
5180        // Augment the given solana args with recommended defaults.
5181        let client = create_client(&url);
5182        let solana_args = add_recommended_deployment_solana_args(&client, solana_args)?;
5183
5184        cfg.run_hooks(HookType::PreDeploy)?;
5185        // Deploy the programs.
5186        println!("Deploying cluster: {url}");
5187        println!("Upgrade authority: {keypair}");
5188
5189        for program in cfg.get_programs(program_name)? {
5190            let binary_path = program.binary_path(verifiable)?;
5191
5192            println!("Deploying program {:?}...", program.lib_name);
5193            println!("Program path: {}...", binary_path.display());
5194
5195            let program_keypair_filepath = match program_keypair.as_ref() {
5196                Some(path) => path.clone(),
5197                None => program.keypair_file()?.path().clone(),
5198            };
5199
5200            // Deploy using our native implementation
5201            program::program_deploy(
5202                cfg_override,
5203                Some(strip_workspace_prefix(binary_path)),
5204                None, // program_name - not needed since we have filepath
5205                Some(strip_workspace_prefix(program_keypair_filepath)),
5206                None, // upgrade_authority - uses wallet from config
5207                None, // program_id - derived from program_keypair
5208                None, // buffer
5209                None, // max_len
5210                no_idl,
5211                false, // make_final
5212                solana_args.clone(),
5213            )?;
5214        }
5215
5216        println!("Deploy success");
5217        cfg.run_hooks(HookType::PostDeploy)?;
5218
5219        Ok(())
5220    })?
5221}
5222
5223fn upgrade(
5224    cfg_override: &ConfigOverride,
5225    program_id: Pubkey,
5226    program_filepath: PathBuf,
5227    max_retries: u32,
5228    solana_args: Vec<String>,
5229) -> Result<()> {
5230    // Use our native upgrade implementation
5231    program::program_upgrade(
5232        cfg_override,
5233        program_id,
5234        Some(program_filepath),
5235        None, // program_name - not needed since we have filepath
5236        None, // buffer
5237        None, // upgrade_authority - uses wallet from config
5238        max_retries,
5239        solana_args,
5240    )
5241}
5242
5243fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
5244    with_workspace(cfg_override, |cfg| -> Result<()> {
5245        println!("Running migration deploy script");
5246
5247        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
5248        let cur_dir = std::env::current_dir()?;
5249        let migrations_dir = cur_dir.join("migrations");
5250        let deploy_ts = Path::new("deploy.ts");
5251
5252        let use_ts = Path::new("tsconfig.json").exists() && migrations_dir.join(deploy_ts).exists();
5253
5254        if !Path::new(".anchor").exists() {
5255            fs::create_dir(".anchor")?;
5256        }
5257        std::env::set_current_dir(".anchor")?;
5258
5259        let exit = if use_ts {
5260            let module_path = migrations_dir.join(deploy_ts);
5261            let deploy_script_host_str =
5262                rust_template::deploy_ts_script_host(&url, &module_path.display().to_string());
5263            fs::write(deploy_ts, deploy_script_host_str)?;
5264
5265            let pkg_manager_cmd =
5266                resolve_package_manager(cfg.toolchain.package_manager.clone())?.to_string();
5267
5268            std::process::Command::new(pkg_manager_cmd)
5269                .args([
5270                    "run",
5271                    "ts-node",
5272                    &fs::canonicalize(deploy_ts)?.to_string_lossy(),
5273                ])
5274                .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
5275                .stdout(Stdio::inherit())
5276                .stderr(Stdio::inherit())
5277                .output()?
5278        } else {
5279            let deploy_js = deploy_ts.with_extension("js");
5280            let module_path = migrations_dir.join(&deploy_js);
5281            let deploy_script_host_str =
5282                rust_template::deploy_js_script_host(&url, &module_path.display().to_string());
5283            fs::write(&deploy_js, deploy_script_host_str)?;
5284
5285            std::process::Command::new("node")
5286                .arg(&deploy_js)
5287                .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
5288                .stdout(Stdio::inherit())
5289                .stderr(Stdio::inherit())
5290                .output()?
5291        };
5292
5293        if !exit.status.success() {
5294            eprintln!("Deploy failed.");
5295            std::process::exit(exit.status.code().unwrap());
5296        }
5297
5298        println!("Deploy complete.");
5299        Ok(())
5300    })?
5301}
5302
5303fn set_workspace_dir_or_exit() {
5304    // First try to find Anchor workspace
5305    let d = match Config::discover(&ConfigOverride::default()) {
5306        Err(err) => {
5307            println!("Workspace configuration error: {err}");
5308            std::process::exit(1);
5309        }
5310        Ok(d) => d,
5311    };
5312
5313    match d {
5314        None => {
5315            // No Anchor.toml found - check for Cargo workspace with Solana programs
5316            let current_dir = match std::env::current_dir() {
5317                Ok(dir) => dir,
5318                Err(_) => {
5319                    println!("Unable to determine current directory");
5320                    std::process::exit(1);
5321                }
5322            };
5323
5324            let cargo_toml_path = current_dir.join("Cargo.toml");
5325            if !cargo_toml_path.exists() {
5326                println!(
5327                    "Not in a Solana workspace. This command requires either Anchor.toml or a \
5328                     Cargo workspace with Solana programs."
5329                );
5330                std::process::exit(1);
5331            }
5332
5333            // Check if this is a workspace and has Solana programs
5334            match program::discover_solana_programs(None) {
5335                Ok(programs) if !programs.is_empty() => {
5336                    // Found Solana programs in Cargo workspace - stay in current directory
5337                    // (already in the right place)
5338                }
5339                _ => {
5340                    println!(
5341                        "Not in a Solana workspace. This command requires either Anchor.toml or a \
5342                         Cargo workspace with Solana programs."
5343                    );
5344                    std::process::exit(1);
5345                }
5346            }
5347        }
5348        Some(cfg) => {
5349            // Found Anchor.toml - change to workspace root
5350            match cfg.path().parent() {
5351                None => {
5352                    println!("Unable to make new program");
5353                }
5354                Some(parent) => {
5355                    if std::env::set_current_dir(parent).is_err() {
5356                        println!(
5357                            "Not in a Solana workspace. This command requires either Anchor.toml \
5358                             or a Cargo workspace with Solana programs."
5359                        );
5360                        std::process::exit(1);
5361                    }
5362                }
5363            };
5364        }
5365    }
5366}
5367
5368fn airdrop(cfg_override: &ConfigOverride, amount: f64, pubkey: Option<Pubkey>) -> Result<()> {
5369    // Get cluster URL and wallet path
5370    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
5371
5372    // Create RPC client
5373    let client = RpcClient::new(cluster_url);
5374
5375    // Determine recipient
5376    let recipient_pubkey = if let Some(pubkey) = pubkey {
5377        pubkey
5378    } else {
5379        // Load keypair from wallet path and get pubkey
5380        let keypair = Keypair::read_from_file(&wallet_path)
5381            .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
5382        keypair.pubkey()
5383    };
5384
5385    // Convert SOL to lamports
5386    let lamports = (amount * 1_000_000_000.0) as u64;
5387
5388    // Request airdrop
5389    println!("Requesting airdrop of {} SOL...", amount);
5390    let signature = client
5391        .request_airdrop(&recipient_pubkey, lamports)
5392        .map_err(|e| anyhow!("Airdrop request failed: {}", e))?;
5393
5394    println!("Signature: {}", signature);
5395    println!("Waiting for confirmation...");
5396
5397    // Wait for confirmation
5398    client
5399        .confirm_transaction(&signature)
5400        .map_err(|e| anyhow!("Transaction confirmation failed: {}", e))?;
5401
5402    // Get and display the new balance
5403    let balance = client.get_balance(&recipient_pubkey)?;
5404    println!("{}", format_sol(balance));
5405
5406    Ok(())
5407}
5408
5409fn cluster(_cmd: ClusterCommand) -> Result<()> {
5410    println!("Cluster Endpoints:\n");
5411    println!("* Mainnet - https://api.mainnet-beta.solana.com");
5412    println!("* Devnet  - https://api.devnet.solana.com");
5413    println!("* Testnet - https://api.testnet.solana.com");
5414    Ok(())
5415}
5416
5417fn config_cmd(cfg_override: &ConfigOverride, cmd: ConfigCommand) -> Result<()> {
5418    match cmd {
5419        ConfigCommand::Get => config_get(cfg_override),
5420        ConfigCommand::Set { url, keypair } => config_set(cfg_override, url, keypair),
5421    }
5422}
5423
5424fn config_get(cfg_override: &ConfigOverride) -> Result<()> {
5425    with_workspace(cfg_override, |cfg| -> Result<()> {
5426        println!("Anchor Configuration:");
5427        println!();
5428        println!("Cluster: {}", cfg.provider.cluster.url());
5429        println!("Wallet:  {}", cfg.provider.wallet);
5430        Ok(())
5431    })?
5432}
5433
5434fn config_set(
5435    cfg_override: &ConfigOverride,
5436    url: Option<String>,
5437    keypair: Option<PathBuf>,
5438) -> Result<()> {
5439    // Find the Anchor.toml file
5440    let anchor_toml_path = match Config::discover(cfg_override)? {
5441        Some(cfg) => cfg.path().parent().unwrap().join("Anchor.toml"),
5442        None => bail!("Not in an Anchor workspace"),
5443    };
5444
5445    // Read the current Anchor.toml
5446    let mut toml_content =
5447        fs::read_to_string(&anchor_toml_path).context("Failed to read Anchor.toml")?;
5448    let mut toml_doc: toml::Value =
5449        toml::from_str(&toml_content).context("Failed to parse Anchor.toml")?;
5450
5451    let mut updated = false;
5452
5453    // Update cluster URL if provided
5454    if let Some(cluster_url) = url {
5455        let expanded_url = match cluster_url.as_str() {
5456            "m" => "https://api.mainnet-beta.solana.com".to_string(),
5457            "d" => "https://api.devnet.solana.com".to_string(),
5458            "t" => "https://api.testnet.solana.com".to_string(),
5459            "l" => "http://127.0.0.1:8899".to_string(),
5460            _ => cluster_url,
5461        };
5462
5463        if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) {
5464            provider.insert(
5465                "cluster".to_string(),
5466                toml::Value::String(expanded_url.clone()),
5467            );
5468            println!("Updated cluster to: {}", expanded_url);
5469            updated = true;
5470        }
5471    }
5472
5473    // Update wallet path if provided
5474    if let Some(keypair_path) = keypair {
5475        let expanded_path = shellexpand::tilde(&keypair_path.to_string_lossy()).to_string();
5476
5477        // Check if the wallet file exists
5478        if !Path::new(&expanded_path).exists() {
5479            eprintln!("Warning: Wallet file does not exist: {}", expanded_path);
5480        }
5481
5482        if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) {
5483            provider.insert(
5484                "wallet".to_string(),
5485                toml::Value::String(expanded_path.clone()),
5486            );
5487            println!("Updated wallet to: {}", expanded_path);
5488            updated = true;
5489        }
5490    }
5491
5492    if updated {
5493        // Write the updated config back to Anchor.toml
5494        toml_content =
5495            toml::to_string_pretty(&toml_doc).context("Failed to serialize Anchor.toml")?;
5496        fs::write(&anchor_toml_path, toml_content).context("Failed to write Anchor.toml")?;
5497        println!("\nConfiguration updated successfully!");
5498    } else {
5499        println!("No changes made. Use --url or --keypair to update settings.");
5500    }
5501
5502    Ok(())
5503}
5504
5505fn shell(cfg_override: &ConfigOverride) -> Result<()> {
5506    with_workspace(cfg_override, |cfg| -> Result<()> {
5507        let programs = {
5508            // Create idl map from all workspace programs.
5509            let mut idls: HashMap<String, Idl> = cfg
5510                .read_all_programs()?
5511                .iter()
5512                .filter(|program| program.idl.is_some())
5513                .map(|program| {
5514                    (
5515                        program.idl.as_ref().unwrap().metadata.name.clone(),
5516                        program.idl.clone().unwrap(),
5517                    )
5518                })
5519                .collect();
5520            // Insert all manually specified idls into the idl map.
5521            if let Some(programs) = cfg.programs.get(&cfg.provider.cluster) {
5522                let _ = programs
5523                    .iter()
5524                    .map(|(name, pd)| {
5525                        if let Some(idl_fp) = &pd.idl {
5526                            let file_str =
5527                                fs::read_to_string(idl_fp).expect("Unable to read IDL file");
5528                            let idl = serde_json::from_str(&file_str).expect("Idl not readable");
5529                            idls.insert(name.clone(), idl);
5530                        }
5531                    })
5532                    .collect::<Vec<_>>();
5533            }
5534
5535            // Finalize program list with all programs with IDLs.
5536            match cfg.programs.get(&cfg.provider.cluster) {
5537                None => Vec::new(),
5538                Some(programs) => programs
5539                    .iter()
5540                    .filter_map(|(name, program_deployment)| {
5541                        Some(ProgramWorkspace {
5542                            name: name.to_string(),
5543                            program_id: program_deployment.address,
5544                            idl: match idls.get(name) {
5545                                None => return None,
5546                                Some(idl) => idl.clone(),
5547                            },
5548                        })
5549                    })
5550                    .collect::<Vec<ProgramWorkspace>>(),
5551            }
5552        };
5553        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
5554        let js_code = rust_template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?;
5555        let mut child = std::process::Command::new("node")
5556            .args(["-e", &js_code, "-i", "--experimental-repl-await"])
5557            .stdout(Stdio::inherit())
5558            .stderr(Stdio::inherit())
5559            .spawn()
5560            .map_err(|e| anyhow::format_err!("{}", e))?;
5561
5562        if !child.wait()?.success() {
5563            println!("Error running node shell");
5564            return Ok(());
5565        }
5566        Ok(())
5567    })?
5568}
5569
5570fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec<String>) -> Result<()> {
5571    with_workspace(cfg_override, |cfg| -> Result<()> {
5572        let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
5573        let script = cfg
5574            .scripts
5575            .get(&script)
5576            .ok_or_else(|| anyhow!("Unable to find script"))?;
5577        let script_with_args = format!("{script} {}", script_args.join(" "));
5578        let exit = std::process::Command::new("bash")
5579            .arg("-c")
5580            .arg(&script_with_args)
5581            .env("ANCHOR_PROVIDER_URL", url)
5582            .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
5583            .stdout(Stdio::inherit())
5584            .stderr(Stdio::inherit())
5585            .output()
5586            .unwrap();
5587        if !exit.status.success() {
5588            std::process::exit(exit.status.code().unwrap_or(1));
5589        }
5590        Ok(())
5591    })?
5592}
5593
5594fn keys(cfg_override: &ConfigOverride, cmd: KeysCommand) -> Result<()> {
5595    match cmd {
5596        KeysCommand::List => keys_list(cfg_override),
5597        KeysCommand::Sync { program_name } => keys_sync(cfg_override, program_name),
5598    }
5599}
5600
5601fn keys_list(cfg_override: &ConfigOverride) -> Result<()> {
5602    with_workspace(cfg_override, |cfg| -> Result<()> {
5603        for program in cfg.read_all_programs()? {
5604            let pubkey = program.pubkey()?;
5605            println!("{}: {}", program.lib_name, pubkey);
5606        }
5607        Ok(())
5608    })?
5609}
5610
5611/// Sync program `declare_id!` pubkeys with the pubkey from `target/deploy/<KEYPAIR>.json`.
5612fn keys_sync(cfg_override: &ConfigOverride, program_name: Option<String>) -> Result<()> {
5613    with_workspace(cfg_override, |cfg| -> Result<()> {
5614        let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
5615            .multi_line(true)
5616            .build()
5617            .unwrap();
5618
5619        let cfg_cluster = cfg.provider.cluster.to_owned();
5620        println!("Syncing program ids for the configured cluster ({cfg_cluster})\n");
5621
5622        let mut changed_src = false;
5623        for program in cfg.get_programs(program_name)? {
5624            // Get the pubkey from the keypair file
5625            let actual_program_id = program.pubkey()?.to_string();
5626
5627            // Handle declaration in program files
5628            let src_path = program.path.join("src");
5629            let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
5630
5631            for path in files_to_check {
5632                let mut content = match fs::read_to_string(&path) {
5633                    Ok(content) => content,
5634                    Err(_) => continue,
5635                };
5636
5637                let incorrect_program_id = declare_id_regex
5638                    .captures(&content)
5639                    .and_then(|captures| captures.get(3))
5640                    .filter(|program_id_match| program_id_match.as_str() != actual_program_id);
5641                if let Some(program_id_match) = incorrect_program_id {
5642                    println!("Found incorrect program id declaration in {path:?}");
5643
5644                    // Update the program id
5645                    content.replace_range(program_id_match.range(), &actual_program_id);
5646                    fs::write(&path, content)?;
5647
5648                    changed_src = true;
5649                    println!("Updated to {actual_program_id}\n");
5650                    break;
5651                }
5652            }
5653
5654            // Handle declaration in Anchor.toml
5655            'outer: for (cluster, programs) in &mut cfg.programs {
5656                // Only change if the configured cluster matches the program's cluster
5657                if cluster != &cfg_cluster {
5658                    continue;
5659                }
5660
5661                for (name, deployment) in programs {
5662                    // Skip other programs
5663                    if name != &program.lib_name {
5664                        continue;
5665                    }
5666
5667                    if deployment.address.to_string() != actual_program_id {
5668                        println!(
5669                            "Found incorrect program id declaration in Anchor.toml for the \
5670                             program `{name}`"
5671                        );
5672
5673                        // Update the program id
5674                        deployment.address = Pubkey::try_from(actual_program_id.as_str()).unwrap();
5675                        fs::write(cfg.path(), cfg.to_string())?;
5676
5677                        println!("Updated to {actual_program_id}\n");
5678                        break 'outer;
5679                    }
5680                }
5681            }
5682        }
5683
5684        println!("All program id declarations are synced.");
5685        if changed_src {
5686            println!("Please rebuild the program to update the generated artifacts.")
5687        }
5688
5689        Ok(())
5690    })?
5691}
5692
5693/// Check if there's a mismatch between the program keypair and the `declare_id!` in the source code.
5694/// Returns an error if a mismatch is detected, prompting the user to run `anchor keys sync`.
5695fn check_program_id_mismatch(cfg: &WithPath<Config>, program_name: Option<String>) -> Result<()> {
5696    let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
5697        .multi_line(true)
5698        .build()
5699        .unwrap();
5700
5701    for program in cfg.get_programs(program_name)? {
5702        // Get the pubkey from the keypair file
5703        let actual_program_id = program.pubkey()?.to_string();
5704
5705        // Check declaration in program files
5706        let src_path = program.path.join("src");
5707        let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
5708
5709        for path in files_to_check {
5710            let content = match fs::read_to_string(&path) {
5711                Ok(content) => content,
5712                Err(_) => continue,
5713            };
5714
5715            let incorrect_program_id = declare_id_regex
5716                .captures(&content)
5717                .and_then(|captures| captures.get(3))
5718                .filter(|program_id_match| program_id_match.as_str() != actual_program_id);
5719
5720            if let Some(program_id_match) = incorrect_program_id {
5721                let declared_id = program_id_match.as_str();
5722                return Err(anyhow!(
5723                    "Program ID mismatch detected for program '{}':\n  Keypair file has: {}\n  \
5724                     Source code has:  {}\n\nPlease run 'anchor keys sync' to update the program \
5725                     ID in your source code or use the '--ignore-keys' flag to skip this check.",
5726                    program.lib_name,
5727                    actual_program_id,
5728                    declared_id
5729                ));
5730            }
5731        }
5732    }
5733
5734    Ok(())
5735}
5736
5737#[allow(clippy::too_many_arguments)]
5738fn localnet(
5739    cfg_override: &ConfigOverride,
5740    skip_build: bool,
5741    skip_deploy: bool,
5742    skip_lint: bool,
5743    ignore_keys: bool,
5744    validator_type: ValidatorType,
5745    env_vars: Vec<String>,
5746    cargo_args: Vec<String>,
5747) -> Result<()> {
5748    with_workspace(cfg_override, |cfg| -> Result<()> {
5749        // Build if needed.
5750        if !skip_build {
5751            build(
5752                cfg_override,
5753                false,
5754                None,
5755                None,
5756                false,
5757                skip_lint,
5758                ignore_keys,
5759                None,
5760                None,
5761                None,
5762                BootstrapMode::None,
5763                None,
5764                None,
5765                env_vars,
5766                cargo_args,
5767                false,
5768            )?;
5769        }
5770
5771        let validator_handle: Option<Child> = match validator_type {
5772            ValidatorType::Surfpool => {
5773                let full_simnet_mode = true;
5774                let flags = Some(surfpool_flags(
5775                    cfg,
5776                    &cfg.surfpool_config,
5777                    full_simnet_mode,
5778                    skip_deploy,
5779                    None,
5780                )?);
5781                Some(start_surfpool_validator(
5782                    flags,
5783                    &cfg.surfpool_config,
5784                    full_simnet_mode,
5785                )?)
5786            }
5787            ValidatorType::Legacy => {
5788                let flags = match skip_deploy {
5789                    true => None,
5790                    false => Some(validator_flags(cfg, &cfg.test_validator)?),
5791                };
5792                Some(start_solana_test_validator(
5793                    cfg,
5794                    &cfg.test_validator,
5795                    flags,
5796                    false,
5797                )?)
5798            }
5799        };
5800
5801        // Setup log reader.
5802        let url = test_validator_rpc_url(&cfg.test_validator);
5803        let log_streams = match stream_logs(cfg, &url) {
5804            Ok(streams) => {
5805                println!(
5806                    "Log streams set up successfully ({} streams)",
5807                    streams.len()
5808                );
5809                Some(streams)
5810            }
5811            Err(e) => {
5812                eprintln!("Warning: Failed to setup program log streaming: {:#}", e);
5813                eprintln!("  Program logs will still be visible in the validator output.");
5814                None
5815            }
5816        };
5817
5818        std::io::stdin().lock().lines().next().unwrap().unwrap();
5819
5820        // Check all errors and shut down.
5821        if let Some(mut handle) = validator_handle {
5822            if let Err(err) = handle.kill() {
5823                println!("Failed to kill subprocess {}: {}", handle.id(), err);
5824            }
5825        }
5826
5827        // Explicitly shutdown log streams - closes WebSocket subscriptions
5828        if let Some(log_streams) = log_streams {
5829            for handle in log_streams {
5830                handle.shutdown();
5831            }
5832        }
5833
5834        Ok(())
5835    })?
5836}
5837
5838/// Return the cargo build artifacts directory. The successful result is
5839/// cached.
5840pub fn target_dir() -> Result<&'static Path> {
5841    static TARGET_DIR: OnceLock<PathBuf> = OnceLock::new();
5842    if let Some(path) = TARGET_DIR.get() {
5843        return Ok(path.as_path());
5844    }
5845    let path = target_dir_no_cache()?;
5846    let _ = TARGET_DIR.set(path);
5847    Ok(TARGET_DIR.get().expect("just set").as_path())
5848}
5849
5850/// Return the cargo build artifacts directory.
5851fn target_dir_no_cache() -> Result<PathBuf> {
5852    // `cargo metadata` produces a JSON blob from which we extract the
5853    // `target_directory` field.
5854    let output = std::process::Command::new("cargo")
5855        .args(["metadata", "--no-deps", "--format-version=1"])
5856        .output()
5857        .context("Failed to execute 'cargo metadata'")?;
5858
5859    if !output.status.success() {
5860        let stderr_msg = String::from_utf8_lossy(&output.stderr);
5861        bail!("'cargo metadata' failed with: {stderr_msg}");
5862    }
5863
5864    #[derive(Deserialize)]
5865    struct CargoMetadata {
5866        target_directory: PathBuf,
5867    }
5868
5869    let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
5870        .context("Failed to parse 'cargo metadata' output")?;
5871
5872    Ok(metadata.target_directory)
5873}
5874
5875// with_workspace ensures the current working directory is always the top level
5876// workspace directory, i.e., where the `Anchor.toml` file is located, before
5877// and after the closure invocation.
5878//
5879// The closure passed into this function must never change the working directory
5880// to be outside the workspace. Doing so will have undefined behavior.
5881fn with_workspace<R>(
5882    cfg_override: &ConfigOverride,
5883    f: impl FnOnce(&mut WithPath<Config>) -> R,
5884) -> Result<R> {
5885    set_workspace_dir_or_exit();
5886
5887    let mut cfg = Config::discover(cfg_override)
5888        .map_err(|e| anyhow!("Workspace configuration error: {}", e))?
5889        .ok_or_else(|| anyhow!("This command requires an Anchor workspace."))?;
5890
5891    let r = f(&mut cfg);
5892
5893    set_workspace_dir_or_exit();
5894
5895    Ok(r)
5896}
5897
5898fn is_hidden(entry: &walkdir::DirEntry) -> bool {
5899    entry
5900        .file_name()
5901        .to_str()
5902        .map(|s| s == "." || s.starts_with('.') || s == "target")
5903        .unwrap_or(false)
5904}
5905
5906/// Build the WebSocket URL for `anchor logs` from the cluster URL.
5907///
5908/// - Non-local clusters (devnet, mainnet, custom hosts): swap scheme only
5909///   (`http(s)://…` → `ws(s)://…`).
5910/// - Local clusters (`127.0.0.1` / `localhost`): pick the WS port from config
5911///   when `[test.surfpool] ws_port` is set explicitly; otherwise default to
5912///   the URL's RPC port + 1 (the solana-test-validator convention, which
5913///   surfpool also follows when `ws_port` is unset).
5914///
5915/// Returns a best-effort string — never errors. Malformed URLs fall back to
5916/// `DEFAULT_RPC_PORT + 1` for the local path.
5917fn logs_websocket_url(cfg_override: &ConfigOverride, cluster_url: &str) -> String {
5918    let ws_scheme_url = cluster_url
5919        .replace("https://", "wss://")
5920        .replace("http://", "ws://");
5921
5922    let is_local = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
5923    if !is_local {
5924        return ws_scheme_url;
5925    }
5926
5927    let default_ws_port = extract_url_port(cluster_url)
5928        .map(|p| p.saturating_add(1))
5929        .unwrap_or(DEFAULT_RPC_PORT + 1);
5930    let ws_port = Config::discover(cfg_override)
5931        .ok()
5932        .flatten()
5933        .and_then(|cfg| cfg.surfpool_config.as_ref().and_then(|s| s.ws_port))
5934        .unwrap_or(default_ws_port);
5935
5936    replace_url_port(&ws_scheme_url, ws_port)
5937}
5938
5939/// Extract the explicit port from `scheme://host:port[/...]`. Returns `None`
5940/// when no port is present or it isn't a valid `u16`.
5941fn extract_url_port(url: &str) -> Option<u16> {
5942    let (_, after_scheme) = url.split_once("://")?;
5943    let host_port_end = after_scheme.find('/').unwrap_or(after_scheme.len());
5944    let (_, port_str) = after_scheme[..host_port_end].rsplit_once(':')?;
5945    port_str.parse().ok()
5946}
5947
5948/// Replace (or insert) the port in `scheme://host[:port][/path]`. Preserves
5949/// the path/query tail.
5950fn replace_url_port(url: &str, new_port: u16) -> String {
5951    let Some((scheme, rest)) = url.split_once("://") else {
5952        return url.to_string();
5953    };
5954    let (host_port_part, tail) = match rest.find('/') {
5955        Some(i) => (&rest[..i], &rest[i..]),
5956        None => (rest, ""),
5957    };
5958    let host = host_port_part
5959        .rsplit_once(':')
5960        .map(|(h, _)| h)
5961        .unwrap_or(host_port_part);
5962    format!("{scheme}://{host}:{new_port}{tail}")
5963}
5964
5965fn get_node_version() -> Result<Version> {
5966    let node_version = std::process::Command::new("node")
5967        .arg("--version")
5968        .stderr(Stdio::inherit())
5969        .output()
5970        .map_err(|e| anyhow::format_err!("node failed: {}", e))?;
5971    parse_node_version(std::str::from_utf8(&node_version.stdout)?)
5972}
5973
5974/// Parse the stdout of `node --version` into a `Version`. Tolerates both the
5975/// canonical `v20.10.0\n` shape and a bare `20.10.0` — the latter shows up in
5976/// corporate wrappers / volta shims that strip the `v` prefix. Malformed
5977/// output still errors.
5978fn parse_node_version(output: &str) -> Result<Version> {
5979    let trimmed = output.trim();
5980    let without_v = trimmed.strip_prefix('v').unwrap_or(trimmed);
5981    Version::parse(without_v).map_err(Into::into)
5982}
5983
5984fn add_recommended_deployment_solana_args(
5985    client: &RpcClient,
5986    args: Vec<String>,
5987) -> Result<Vec<String>> {
5988    let mut augmented_args = args.clone();
5989
5990    // If no priority fee is provided, calculate a recommended fee based on recent txs.
5991    if !args.contains(&"--with-compute-unit-price".to_string()) {
5992        let priority_fee = get_recommended_micro_lamport_fee(client);
5993        augmented_args.push("--with-compute-unit-price".to_string());
5994        augmented_args.push(priority_fee.to_string());
5995    }
5996
5997    const DEFAULT_MAX_SIGN_ATTEMPTS: u8 = 30;
5998    if !args.contains(&"--max-sign-attempts".to_string()) {
5999        augmented_args.push("--max-sign-attempts".to_string());
6000        augmented_args.push(DEFAULT_MAX_SIGN_ATTEMPTS.to_string());
6001    }
6002
6003    // If no buffer keypair is provided, create a temporary one to reuse across deployments.
6004    // This is particularly useful for upgrading larger programs, which suffer from an increased
6005    // likelihood of some write transactions failing during any single deployment.
6006    if !args.contains(&"--buffer".to_owned()) {
6007        let tmp_keypair_path = std::env::temp_dir().join("anchor-upgrade-buffer.json");
6008        if !tmp_keypair_path.exists() {
6009            if let Err(err) = Keypair::new().write_to_file(&tmp_keypair_path) {
6010                return Err(anyhow!(
6011                    "Error creating keypair for buffer account, {:?}",
6012                    err
6013                ));
6014            }
6015        }
6016
6017        augmented_args.push("--buffer".to_owned());
6018        augmented_args.push(tmp_keypair_path.to_string_lossy().to_string());
6019    }
6020
6021    Ok(augmented_args)
6022}
6023
6024/// Returns the `NODE_OPTIONS` flag that tells Node to prefer IPv4 in DNS
6025/// resolution (needed to avoid a localhost connect-timeout bug on Node 16+).
6026///
6027/// Returns `""` when `node` is missing or its `--version` output is unparsable.
6028/// This lets `anchor test` run on Rust-only test templates (litesvm / mollusk
6029/// / rust) without requiring `node` on `PATH` — the env var still gets set,
6030/// but `cargo test` ignores it. For JS/TS projects that actually invoke node,
6031/// any real node problem will surface cleanly at the point the test script
6032/// runs.
6033fn get_node_dns_option() -> &'static str {
6034    let Ok(version) = get_node_version() else {
6035        return "";
6036    };
6037    let req = VersionReq::parse(">=16.4.0").unwrap();
6038    if req.matches(&version) {
6039        "--dns-result-order=ipv4first"
6040    } else {
6041        ""
6042    }
6043}
6044
6045// Remove the current workspace directory if it prefixes a string.
6046// This is used as a workaround for the Solana CLI using the uriparse crate to
6047// parse args but not handling percent encoding/decoding when using the path as
6048// a local filesystem path. Removing the workspace prefix handles most/all cases
6049// of spaces in keypair/binary paths, but this should be fixed in the Solana CLI
6050// and removed here.
6051fn strip_workspace_prefix(absolute_path: PathBuf) -> PathBuf {
6052    let workspace_prefix = std::env::current_dir().unwrap();
6053    absolute_path
6054        .strip_prefix(&workspace_prefix)
6055        .unwrap_or(&absolute_path)
6056        .into()
6057}
6058
6059/// Create a new [`RpcClient`] with `confirmed` commitment level instead of the default(finalized).
6060fn create_client<U: ToString>(url: U) -> RpcClient {
6061    RpcClient::new_with_commitment(url, CommitmentConfig::confirmed())
6062}
6063
6064fn address(cfg_override: &ConfigOverride) -> Result<()> {
6065    let (_cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
6066
6067    // Load keypair and get pubkey
6068    let keypair = Keypair::read_from_file(&wallet_path)
6069        .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
6070
6071    // Print the public key
6072    println!("{}", keypair.pubkey());
6073
6074    Ok(())
6075}
6076
6077fn balance(cfg_override: &ConfigOverride, pubkey: Option<Pubkey>, lamports: bool) -> Result<()> {
6078    let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
6079
6080    // Create RPC client
6081    let client = RpcClient::new(cluster_url);
6082
6083    // Determine which account to check
6084    let account_pubkey = if let Some(pubkey) = pubkey {
6085        pubkey
6086    } else {
6087        // Load keypair from wallet path and get pubkey
6088        let keypair = Keypair::read_from_file(&wallet_path)
6089            .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
6090        keypair.pubkey()
6091    };
6092
6093    // Get balance
6094    let balance = client.get_balance(&account_pubkey)?;
6095
6096    // Format and display output
6097    if lamports {
6098        println!("{}", balance);
6099    } else {
6100        println!("{}", format_sol(balance));
6101    }
6102
6103    Ok(())
6104}
6105
6106fn epoch(cfg_override: &ConfigOverride) -> Result<()> {
6107    let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
6108
6109    // Create RPC client
6110    let client = RpcClient::new(cluster_url);
6111
6112    // Get epoch info
6113    let epoch_info = client.get_epoch_info()?;
6114
6115    // Print just the epoch number
6116    println!("{}", epoch_info.epoch);
6117
6118    Ok(())
6119}
6120
6121fn epoch_info(cfg_override: &ConfigOverride) -> Result<()> {
6122    let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
6123
6124    // Create RPC client
6125    let client = RpcClient::new(cluster_url);
6126
6127    // Get epoch info
6128    let epoch_info = client.get_epoch_info()?;
6129
6130    // Calculate epoch slot range
6131    let first_slot_in_epoch = epoch_info.absolute_slot - epoch_info.slot_index;
6132    let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch;
6133
6134    // Calculate completion stats
6135    let epoch_completed_percent =
6136        epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100.0;
6137    let remaining_slots = epoch_info.slots_in_epoch - epoch_info.slot_index;
6138
6139    // Display epoch information (matching Solana CLI format)
6140    println!("Block height: {}", epoch_info.block_height);
6141    println!("Slot: {}", epoch_info.absolute_slot);
6142    println!("Epoch: {}", epoch_info.epoch);
6143
6144    if let Some(tx_count) = epoch_info.transaction_count {
6145        println!("Transaction Count: {}", tx_count);
6146    }
6147
6148    println!(
6149        "Epoch Slot Range: [{}..{})",
6150        first_slot_in_epoch, last_slot_in_epoch
6151    );
6152    println!("Epoch Completed Percent: {:>3.3}%", epoch_completed_percent);
6153    println!(
6154        "Epoch Completed Slots: {}/{} ({} remaining)",
6155        epoch_info.slot_index, epoch_info.slots_in_epoch, remaining_slots
6156    );
6157
6158    // Try to calculate epoch completed time
6159    // Get average slot time from performance samples (aggregate up to 60 samples)
6160    if let Ok(samples) = client.get_recent_performance_samples(Some(60)) {
6161        // Aggregate all samples to calculate average slot time
6162        let (total_slots, total_secs) =
6163            samples.iter().fold((0u64, 0u64), |(slots, secs), sample| {
6164                (
6165                    slots.saturating_add(sample.num_slots),
6166                    secs.saturating_add(sample.sample_period_secs as u64),
6167                )
6168            });
6169
6170        if let Some(avg_slot_time_ms) = (total_secs * 1000).checked_div(total_slots) {
6171            // Calculate time_remaining using average slot time (always estimated)
6172            let remaining_secs = (remaining_slots * avg_slot_time_ms) / 1000;
6173
6174            // Calculate time_elapsed - try actual block times first, then estimate
6175            // Get the first actual block in the epoch and adjust for slot differences
6176            let start_block_time = client
6177                .get_blocks_with_limit(first_slot_in_epoch, 1)
6178                .ok()
6179                .and_then(|slots| slots.first().cloned())
6180                .and_then(|first_actual_block| {
6181                    client.get_block_time(first_actual_block).ok().map(|time| {
6182                        // Adjust backwards if first actual block is after expected start
6183                        let slot_diff = first_actual_block.saturating_sub(first_slot_in_epoch);
6184                        let time_adjustment = (slot_diff * avg_slot_time_ms / 1000) as i64;
6185                        time.saturating_sub(time_adjustment)
6186                    })
6187                });
6188
6189            let current_block_time = client.get_block_time(epoch_info.absolute_slot).ok();
6190
6191            let (elapsed_secs, is_estimated) = if let (Some(start_time), Some(current_time)) =
6192                (start_block_time, current_block_time)
6193            {
6194                // Use actual block times for elapsed
6195                ((current_time - start_time) as u64, false)
6196            } else {
6197                // Estimate elapsed using average slot time
6198                ((epoch_info.slot_index * avg_slot_time_ms) / 1000, true)
6199            };
6200
6201            // Total time = elapsed + remaining
6202            let total_secs = elapsed_secs + remaining_secs;
6203
6204            let estimated_marker = if is_estimated { "*" } else { "" };
6205            println!(
6206                "Epoch Completed Time: {}{}/{} ({} remaining)",
6207                format_duration_secs(elapsed_secs),
6208                estimated_marker,
6209                format_duration_secs(total_secs),
6210                format_duration_secs(remaining_secs)
6211            );
6212        }
6213    }
6214
6215    Ok(())
6216}
6217
6218/// Format seconds into human-readable duration (e.g., "1day 5h 49m 8s")
6219fn format_duration_secs(total_seconds: u64) -> String {
6220    let seconds = total_seconds % 60;
6221    let total_minutes = total_seconds / 60;
6222    let minutes = total_minutes % 60;
6223    let total_hours = total_minutes / 60;
6224    let hours = total_hours % 24;
6225    let days = total_hours / 24;
6226
6227    let mut parts = Vec::new();
6228    if days > 0 {
6229        parts.push(format!("{}day", days));
6230    }
6231    if hours > 0 {
6232        parts.push(format!("{}h", hours));
6233    }
6234    if minutes > 0 {
6235        parts.push(format!("{}m", minutes));
6236    }
6237    if seconds > 0 || parts.is_empty() {
6238        parts.push(format!("{}s", seconds));
6239    }
6240
6241    parts.join(" ")
6242}
6243
6244fn logs_subscribe(
6245    cfg_override: &ConfigOverride,
6246    include_votes: bool,
6247    address: Option<Vec<Pubkey>>,
6248) -> Result<()> {
6249    let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
6250    let ws_url = logs_websocket_url(cfg_override, &cluster_url);
6251
6252    println!("Connecting to {}", ws_url);
6253
6254    let filter = match (include_votes, address) {
6255        (true, Some(address)) => {
6256            RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect())
6257        }
6258        (true, None) => RpcTransactionLogsFilter::AllWithVotes,
6259        (false, Some(address)) => {
6260            RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect())
6261        }
6262        (false, None) => RpcTransactionLogsFilter::All,
6263    };
6264
6265    let (_client, receiver) = PubsubClient::logs_subscribe(
6266        &ws_url,
6267        filter,
6268        RpcTransactionLogsConfig {
6269            commitment: cfg_override.commitment.map(|c| CommitmentConfig {
6270                commitment: c.into(),
6271            }),
6272        },
6273    )?;
6274
6275    loop {
6276        match receiver.recv() {
6277            Ok(logs) => {
6278                println!("Transaction executed in slot {}:", logs.context.slot);
6279                println!("  Signature: {}", logs.value.signature);
6280                println!(
6281                    "  Status: {}",
6282                    logs.value
6283                        .err
6284                        .map(|err| err.to_string())
6285                        .unwrap_or_else(|| "Ok".to_string())
6286                );
6287                println!("  Log Messages:");
6288                for log in logs.value.logs {
6289                    println!("    {log}");
6290                }
6291            }
6292            Err(err) => {
6293                return Err(anyhow!("Disconnected: {err}"));
6294            }
6295        }
6296    }
6297}
6298
6299#[cfg(test)]
6300mod tests {
6301    use {
6302        super::*,
6303        anchor_lang_idl::types::{
6304            IdlGenericArg, IdlInstructionAccount, IdlInstructionAccountItem, IdlPda, IdlSeed,
6305            IdlSeedAccount, IdlTypeDef, IdlTypeDefGeneric,
6306        },
6307    };
6308
6309    #[test]
6310    #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
6311    fn test_init_reserved_word() {
6312        init(
6313            &ConfigOverride {
6314                cluster: None,
6315                wallet: None,
6316                commitment: None,
6317            },
6318            "await".to_string(),
6319            true,
6320            true,
6321            None,
6322            false,
6323            ProgramTemplate::default(),
6324            TestTemplate::default(),
6325            false,
6326            true,
6327        )
6328        .unwrap();
6329    }
6330
6331    #[test]
6332    #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
6333    fn test_init_reserved_word_from_syn() {
6334        init(
6335            &ConfigOverride {
6336                cluster: None,
6337                wallet: None,
6338                commitment: None,
6339            },
6340            "fn".to_string(),
6341            true,
6342            true,
6343            None,
6344            false,
6345            ProgramTemplate::default(),
6346            TestTemplate::default(),
6347            false,
6348            true,
6349        )
6350        .unwrap();
6351    }
6352
6353    #[test]
6354    #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
6355    fn test_init_starting_with_digit() {
6356        init(
6357            &ConfigOverride {
6358                cluster: None,
6359                wallet: None,
6360                commitment: None,
6361            },
6362            "1project".to_string(),
6363            true,
6364            true,
6365            None,
6366            false,
6367            ProgramTemplate::default(),
6368            TestTemplate::default(),
6369            false,
6370            true,
6371        )
6372        .unwrap();
6373    }
6374
6375    #[test]
6376    fn parse_node_version_with_v_prefix() {
6377        let v = parse_node_version("v20.10.0\n").unwrap();
6378        assert_eq!(v.major, 20);
6379        assert_eq!(v.minor, 10);
6380        assert_eq!(v.patch, 0);
6381    }
6382
6383    #[test]
6384    fn parse_node_version_without_v_prefix() {
6385        let v = parse_node_version("20.10.0").unwrap();
6386        assert_eq!(v.major, 20);
6387    }
6388
6389    #[test]
6390    fn parse_node_version_ignores_surrounding_whitespace() {
6391        let v = parse_node_version("  v18.17.1  \n").unwrap();
6392        assert_eq!(v.major, 18);
6393        assert_eq!(v.minor, 17);
6394    }
6395
6396    #[test]
6397    fn parse_node_version_errors_on_garbage() {
6398        assert!(parse_node_version("not a version").is_err());
6399        assert!(parse_node_version("").is_err());
6400    }
6401
6402    #[test]
6403    fn extract_url_port_common_shapes() {
6404        assert_eq!(extract_url_port("http://127.0.0.1:8899"), Some(8899));
6405        assert_eq!(extract_url_port("http://127.0.0.1:8899/"), Some(8899));
6406        assert_eq!(extract_url_port("ws://localhost:8900/path?q=1"), Some(8900));
6407        assert_eq!(
6408            extract_url_port("https://api.mainnet-beta.solana.com"),
6409            None
6410        );
6411        assert_eq!(extract_url_port("http://127.0.0.1"), None);
6412        assert_eq!(extract_url_port("not a url"), None);
6413    }
6414
6415    #[test]
6416    fn replace_url_port_preserves_structure() {
6417        assert_eq!(
6418            replace_url_port("http://127.0.0.1:8899", 9001),
6419            "http://127.0.0.1:9001"
6420        );
6421        assert_eq!(
6422            replace_url_port("ws://127.0.0.1:8899/path?q=1", 9050),
6423            "ws://127.0.0.1:9050/path?q=1"
6424        );
6425        // No original port — the helper still injects one.
6426        assert_eq!(
6427            replace_url_port("http://127.0.0.1", 8900),
6428            "http://127.0.0.1:8900"
6429        );
6430    }
6431
6432    #[test]
6433    fn idl_ts_preserves_literal_values() {
6434        let idl = Idl {
6435            address: "11111111111111111111111111111111".to_string(),
6436            metadata: anchor_lang_idl::types::IdlMetadata {
6437                name: "test_program".to_string(),
6438                version: "0.1.0".to_string(),
6439                spec: "0.1.0".to_string(),
6440                description: None,
6441                repository: None,
6442                dependencies: Vec::new(),
6443                contact: None,
6444                deployments: None,
6445            },
6446            docs: Vec::new(),
6447            instructions: vec![anchor_lang_idl::types::IdlInstruction {
6448                name: "do_thing".to_string(),
6449                docs: Vec::new(),
6450                discriminator: vec![0, 1, 2, 3, 4, 5, 6, 7],
6451                accounts: vec![IdlInstructionAccountItem::Single(IdlInstructionAccount {
6452                    name: "target_account".to_string(),
6453                    docs: Vec::new(),
6454                    writable: false,
6455                    signer: false,
6456                    optional: false,
6457                    address: None,
6458                    pda: Some(IdlPda {
6459                        seeds: vec![IdlSeed::Account(IdlSeedAccount {
6460                            path: "source_account.authority".to_string(),
6461                            account: Some("source_account".to_string()),
6462                        })],
6463                        program: None,
6464                    }),
6465                    relations: vec!["source_account".to_string()],
6466                })],
6467                args: vec![anchor_lang_idl::types::IdlField {
6468                    name: "some_arg".to_string(),
6469                    docs: Vec::new(),
6470                    ty: IdlType::U8,
6471                }],
6472                returns: None,
6473            }],
6474            accounts: vec![anchor_lang_idl::types::IdlAccount {
6475                name: "source_account".to_string(),
6476                discriminator: vec![8, 7, 6, 5, 4, 3, 2, 1],
6477            }],
6478            events: Vec::new(),
6479            errors: vec![anchor_lang_idl::types::IdlErrorCode {
6480                code: 6000,
6481                name: "Unauthorized".to_string(),
6482                msg: Some("Unauthorized".to_string()),
6483            }],
6484            types: vec![IdlTypeDef {
6485                name: "wrapper_type".to_string(),
6486                docs: Vec::new(),
6487                serialization: Default::default(),
6488                repr: None,
6489                generics: vec![IdlTypeDefGeneric::Type {
6490                    name: "item_type".to_string(),
6491                }],
6492                ty: IdlTypeDefTy::Type {
6493                    alias: IdlType::Defined {
6494                        name: "generic_holder".to_string(),
6495                        generics: vec![
6496                            IdlGenericArg::Type {
6497                                ty: IdlType::Generic("item_type".to_string()),
6498                            },
6499                            IdlGenericArg::Const {
6500                                value: "SEED_PREFIX".to_string(),
6501                            },
6502                        ],
6503                    },
6504                },
6505            }],
6506            constants: vec![anchor_lang_idl::types::IdlConst {
6507                name: "seed_prefix".to_string(),
6508                docs: Vec::new(),
6509                ty: IdlType::String,
6510                value: "SEED_PREFIX".to_string(),
6511            }],
6512        };
6513
6514        let ts = idl_ts(&idl).unwrap();
6515
6516        assert!(ts.contains(r#""name": "doThing""#));
6517        assert!(ts.contains(r#""name": "targetAccount""#));
6518        assert!(ts.contains(r#""path": "sourceAccount.authority""#));
6519        assert!(ts.contains(r#""account": "sourceAccount""#));
6520        assert!(ts.contains(r#""sourceAccount""#));
6521        assert!(ts.contains(r#""name": "someArg""#));
6522        assert!(ts.contains(r#""name": "sourceAccount""#));
6523        assert!(ts.contains(r#""name": "unauthorized""#));
6524        assert!(ts.contains(r#""msg": "Unauthorized""#));
6525        assert!(ts.contains(r#""name": "wrapperType""#));
6526        assert!(ts.contains(r#""name": "itemType""#));
6527        assert!(ts.contains(r#""name": "genericHolder""#));
6528        assert!(ts.contains(r#""generic": "itemType""#));
6529        assert!(ts.contains(r#""name": "seedPrefix""#));
6530        assert!(ts.contains(r#""value": "SEED_PREFIX""#));
6531    }
6532}