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
72pub const VERSION: &str = env!("CARGO_PKG_VERSION");
74pub const DOCKER_BUILDER_VERSION: &str = VERSION;
75pub const DEFAULT_RPC_PORT: u16 = 8899;
77const DEFAULT_FAUCET_PORT: u16 = 9900;
78
79pub 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 Init {
105 name: String,
107 #[clap(short, long)]
109 javascript: bool,
110 #[clap(long)]
112 no_install: bool,
113 #[clap(value_enum, long)]
118 package_manager: Option<PackageManager>,
119 #[clap(long)]
121 no_git: bool,
122 #[clap(value_enum, short, long, default_value = "multiple")]
124 template: ProgramTemplate,
125 #[clap(value_enum, long, default_value = "litesvm")]
127 test_template: TestTemplate,
128 #[clap(long, action)]
130 force: bool,
131 #[clap(long)]
133 install_agent_skills: bool,
134 },
135 #[clap(name = "build", alias = "b")]
137 Build {
138 #[clap(long)]
140 skip_lint: bool,
141 #[clap(long)]
143 ignore_keys: bool,
144 #[clap(long)]
146 no_idl: bool,
147 #[clap(short, long)]
149 idl: Option<String>,
150 #[clap(short = 't', long)]
152 idl_ts: Option<String>,
153 #[clap(short, long)]
155 verifiable: bool,
156 #[clap(short, long)]
158 program_name: Option<String>,
159 #[clap(short, long)]
162 solana_version: Option<String>,
163 #[clap(short, long)]
165 docker_image: Option<String>,
166 #[clap(value_enum, short, long, default_value = "none")]
169 bootstrap: BootstrapMode,
170 #[clap(short, long, required = false)]
172 env: Vec<String>,
173 #[clap(required = false, last = true)]
175 cargo_args: Vec<String>,
176 #[clap(long)]
178 no_docs: bool,
179 },
180 Expand {
187 #[clap(short, long)]
189 program_name: Option<String>,
190 #[clap(long)]
192 stdout: bool,
193 #[clap(required = false, last = true)]
195 cargo_args: Vec<String>,
196 },
197 Verify {
201 program_id: Pubkey,
203 #[clap(long, conflicts_with = "current_dir")]
205 repo_url: Option<String>,
206 #[clap(long, requires = "repo_url")]
208 commit_hash: Option<String>,
209 #[clap(long)]
211 current_dir: bool,
212 #[clap(long)]
214 program_name: Option<String>,
215 #[clap(raw = true)]
217 args: Vec<String>,
218 },
219 #[clap(name = "test", alias = "t")]
220 Test {
222 #[clap(short, long)]
224 program_name: Option<String>,
225 #[clap(long)]
228 skip_deploy: bool,
229 #[clap(long)]
232 skip_lint: bool,
233 #[clap(long)]
236 skip_local_validator: bool,
237 #[clap(long)]
240 skip_build: bool,
241 #[clap(long)]
243 no_idl: bool,
244 #[clap(long)]
247 detach: bool,
248 #[clap(long)]
250 run: Vec<String>,
251 #[clap(value_enum, long, default_value = "surfpool")]
253 validator: ValidatorType,
254 #[cfg(not(windows))]
255 #[clap(long)]
261 profile: bool,
262 args: Vec<String>,
263 #[clap(short, long, required = false)]
265 env: Vec<String>,
266 #[clap(required = false, last = true)]
268 cargo_args: Vec<String>,
269 },
270 New {
272 name: String,
274 #[clap(value_enum, short, long, default_value = "multiple")]
276 template: ProgramTemplate,
277 #[clap(long, action)]
279 force: bool,
280 },
281 #[cfg(not(windows))]
282 Debugger {
290 test_name: Option<String>,
294 #[clap(long)]
297 skip_run: bool,
298 #[clap(long)]
301 skip_build: bool,
302 #[clap(long)]
304 skip_lint: bool,
305 #[clap(long)]
311 gdb: bool,
312 #[clap(required = false, last = true)]
314 cargo_args: Vec<String>,
315 },
316 #[cfg(not(windows))]
317 Coverage {
323 #[clap(long)]
326 skip_run: bool,
327 #[clap(long)]
329 skip_build: bool,
330 #[clap(long, default_value = "target/coverage/sbf.lcov")]
332 output: String,
333 #[clap(long, default_value = "target/coverage/traces")]
335 trace_dir: String,
336 #[clap(required = false, last = true)]
338 cargo_args: Vec<String>,
339 },
340 #[cfg(not(windows))]
341 #[clap(name = "coverage-filter-host", hide = true)]
343 CoverageFilterHost {
344 #[clap(long)]
346 sbf_lcov: String,
347 #[clap(long)]
349 host_lcov: String,
350 #[clap(long)]
352 output: String,
353 },
354 Idl {
356 #[clap(subcommand)]
357 subcmd: IdlCommand,
358 },
359 Clean,
361 #[clap(hide = true)]
363 #[deprecated(since = "0.32.0", note = "use `anchor program deploy` instead")]
364 Deploy {
365 #[clap(short, long)]
367 program_name: Option<String>,
368 #[clap(long, requires = "program_name")]
370 program_keypair: Option<PathBuf>,
371 #[clap(short, long)]
373 verifiable: bool,
374 #[clap(long)]
376 no_idl: bool,
377 #[clap(required = false, last = true)]
379 solana_args: Vec<String>,
380 },
381 Migrate,
383 #[clap(hide = true)]
387 #[deprecated(since = "0.32.0", note = "use `anchor program upgrade` instead")]
388 Upgrade {
389 #[clap(short, long)]
391 program_id: Pubkey,
392 program_filepath: PathBuf,
394 #[clap(long, default_value = "0")]
396 max_retries: u32,
397 #[clap(required = false, last = true)]
399 solana_args: Vec<String>,
400 },
401 Airdrop {
403 amount: f64,
405 pubkey: Option<Pubkey>,
407 },
408 Cluster {
410 #[clap(subcommand)]
411 subcmd: ClusterCommand,
412 },
413 Config {
415 #[clap(subcommand)]
416 subcmd: ConfigCommand,
417 },
418 Shell,
421 Run {
423 script: String,
425 #[clap(required = false, last = true)]
427 script_args: Vec<String>,
428 },
429 Keys {
431 #[clap(subcommand)]
432 subcmd: KeysCommand,
433 },
434 Localnet {
436 #[clap(long)]
439 skip_build: bool,
440 #[clap(long)]
443 skip_deploy: bool,
444 #[clap(long)]
447 skip_lint: bool,
448 #[clap(long)]
450 ignore_keys: bool,
451 #[clap(value_enum, long, default_value = "surfpool")]
453 validator: ValidatorType,
454 #[clap(short, long, required = false)]
456 env: Vec<String>,
457 #[clap(required = false, last = true)]
459 cargo_args: Vec<String>,
460 },
461 Account {
463 account_type: String,
465 address: Pubkey,
467 #[clap(long)]
469 idl: Option<PathBuf>,
470 },
471 Completions {
473 #[clap(value_enum)]
474 shell: clap_complete::Shell,
475 },
476 Address,
478 Balance {
480 pubkey: Option<Pubkey>,
482 #[clap(long)]
484 lamports: bool,
485 },
486 Epoch,
488 #[clap(name = "epoch-info")]
490 EpochInfo,
491 Logs {
493 #[clap(long)]
495 include_votes: bool,
496 #[clap(long)]
498 address: Option<Vec<Pubkey>>,
499 },
500 ShowAccount {
502 #[clap(flatten)]
503 cmd: account::ShowAccountCommand,
504 },
505 Keygen {
507 #[clap(subcommand)]
508 subcmd: KeygenCommand,
509 },
510 Program {
512 #[clap(subcommand)]
513 subcmd: ProgramCommand,
514 },
515 Codama {
517 #[clap(subcommand)]
518 subcmd: codama::CodamaCommand,
519 },
520}
521
522#[derive(Debug, Parser, AbsolutePath)]
523pub enum KeygenCommand {
524 New {
526 #[clap(short = 'o', long)]
528 outfile: Option<PathBuf>,
529 #[clap(short, long)]
531 force: bool,
532 #[clap(long)]
534 no_passphrase: bool,
535 #[clap(long)]
537 silent: bool,
538 #[clap(short = 'w', long, default_value = "12")]
540 word_count: usize,
541 },
542 Pubkey {
544 keypair: Option<PathBuf>,
546 },
547 Recover {
549 #[clap(short = 'o', long)]
551 outfile: Option<PathBuf>,
552 #[clap(short, long)]
554 force: bool,
555 #[clap(long)]
557 skip_seed_phrase_validation: bool,
558 #[clap(long)]
560 no_passphrase: bool,
561 },
562 Verify {
564 pubkey: Pubkey,
566 keypair: Option<PathBuf>,
568 },
569}
570
571#[derive(Debug, Parser, AbsolutePath)]
572pub enum KeysCommand {
573 List,
575 Sync {
577 #[clap(short, long)]
579 program_name: Option<String>,
580 },
581}
582
583#[derive(Debug, Parser, AbsolutePath)]
584pub enum ProgramCommand {
585 Deploy {
587 program_filepath: Option<PathBuf>,
590 #[clap(short, long)]
592 program_name: Option<String>,
593 #[clap(long)]
595 program_keypair: Option<PathBuf>,
596 #[clap(long)]
598 upgrade_authority: Option<String>,
599 #[clap(long)]
601 program_id: Option<Pubkey>,
602 #[clap(long)]
604 buffer: Option<Pubkey>,
605 #[clap(long)]
607 max_len: Option<usize>,
608 #[clap(long)]
610 no_idl: bool,
611 #[clap(long = "final")]
613 make_final: bool,
614 #[clap(required = false, last = true)]
616 solana_args: Vec<String>,
617 },
618 WriteBuffer {
620 program_filepath: Option<PathBuf>,
623 #[clap(short, long)]
625 program_name: Option<String>,
626 #[clap(long)]
628 buffer: Option<String>,
629 #[clap(long)]
631 buffer_authority: Option<String>,
632 #[clap(long)]
634 max_len: Option<usize>,
635 },
636 SetBufferAuthority {
638 buffer: Pubkey,
640 new_buffer_authority: Pubkey,
642 },
643 SetUpgradeAuthority {
645 program_id: Pubkey,
647 #[clap(long)]
649 new_upgrade_authority: Option<Pubkey>,
650 #[clap(long)]
653 new_upgrade_authority_signer: Option<String>,
654 #[clap(long)]
657 skip_new_upgrade_authority_signer_check: bool,
658 #[clap(long = "final")]
660 make_final: bool,
661 #[clap(long)]
663 upgrade_authority: Option<String>,
664 },
665 Show {
667 account: Pubkey,
669 #[clap(long)]
671 get_programs: bool,
672 #[clap(long)]
674 get_buffers: bool,
675 #[clap(long)]
677 all: bool,
678 },
679 Upgrade {
681 program_id: Pubkey,
683 #[clap(long)]
685 program_filepath: Option<PathBuf>,
686 #[clap(short, long)]
688 program_name: Option<String>,
689 #[clap(long)]
691 buffer: Option<Pubkey>,
692 #[clap(long)]
694 upgrade_authority: Option<String>,
695 #[clap(long, default_value = "0")]
697 max_retries: u32,
698 #[clap(required = false, last = true)]
700 solana_args: Vec<String>,
701 },
702 Dump {
704 account: Pubkey,
706 output_file: String,
708 },
709 Close {
711 account: Option<Pubkey>,
714 #[clap(short, long)]
716 program_name: Option<String>,
717 #[clap(long)]
719 authority: Option<String>,
720 #[clap(long)]
722 recipient: Option<Pubkey>,
723 #[clap(long)]
725 bypass_warning: bool,
726 },
727 Extend {
729 program_id: Option<Pubkey>,
732 #[clap(short, long)]
734 program_name: Option<String>,
735 additional_bytes: usize,
737 },
738}
739
740#[derive(Debug, Parser, AbsolutePath)]
741pub enum IdlCommand {
742 Init {
744 program_id: Option<Pubkey>,
747 #[clap(short, long)]
748 filepath: PathBuf,
749 #[clap(long)]
750 priority_fee: Option<u64>,
751 #[clap(long)]
753 non_canonical: bool,
754 #[clap(long)]
756 #[cfg(feature = "idl-localnet-testing")]
757 allow_localnet: bool,
758 },
759 Upgrade {
762 program_id: Option<Pubkey>,
765 #[clap(short, long)]
766 filepath: PathBuf,
767 #[clap(long)]
768 priority_fee: Option<u64>,
769 #[clap(long)]
771 #[cfg(feature = "idl-localnet-testing")]
772 allow_localnet: bool,
773 },
774 #[clap(alias = "b")]
776 Build {
777 #[clap(short, long)]
779 program_name: Option<String>,
780 #[clap(short, long)]
782 out: Option<String>,
783 #[clap(short = 't', long)]
785 out_ts: Option<String>,
786 #[clap(long)]
788 no_docs: bool,
789 #[clap(long)]
791 skip_lint: bool,
792 #[clap(required = false, last = true)]
794 cargo_args: Vec<String>,
795 },
796 Fetch {
798 program_id: Pubkey,
799 #[clap(short, long)]
801 out: Option<String>,
802 #[clap(long)]
804 non_canonical: bool,
805 },
806 Convert {
808 path: PathBuf,
810 #[clap(short, long)]
812 out: Option<PathBuf>,
813 #[clap(short, long)]
816 program_id: Option<Pubkey>,
817 },
818 Type {
820 path: PathBuf,
822 #[clap(short, long)]
824 out: Option<PathBuf>,
825 },
826 Close {
828 program_id: Pubkey,
830 #[clap(long, default_value = "idl")]
832 seed: String,
833 #[clap(long)]
835 priority_fee: Option<u64>,
836 },
837 CreateBuffer {
839 #[clap(short, long)]
841 filepath: PathBuf,
842 #[clap(long)]
844 priority_fee: Option<u64>,
845 },
846 SetBufferAuthority {
848 buffer: Pubkey,
850 #[clap(short, long)]
852 new_authority: Pubkey,
853 #[clap(long)]
855 priority_fee: Option<u64>,
856 },
857 WriteBuffer {
859 program_id: Pubkey,
861 #[clap(short, long)]
863 buffer: Pubkey,
864 #[clap(long, default_value = "idl")]
866 seed: String,
867 #[clap(long)]
869 close_buffer: bool,
870 #[clap(long)]
872 priority_fee: Option<u64>,
873 },
874}
875
876#[derive(Debug, Parser, AbsolutePath)]
877pub enum ClusterCommand {
878 List,
880}
881
882#[derive(Debug, Parser, AbsolutePath)]
883pub enum ConfigCommand {
884 Get,
886 Set {
888 #[clap(short = 'u', long = "url")]
890 url: Option<String>,
891 #[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
902fn format_sol(lamports: u64) -> String {
904 let sol = lamports as f64 / 1_000_000_000.0;
905 let formatted = format!("{:.8}", sol);
906
907 let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
909 format!("{} SOL", trimmed)
910}
911
912fn get_cluster_and_wallet(cfg_override: &ConfigOverride) -> Result<(String, String)> {
914 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 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 (
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 (
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 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
968pub fn get_recommended_micro_lamport_fee(client: &RpcClient) -> u64 {
970 let mut fees = match client.get_recent_prioritization_fees(&[]) {
971 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 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
993pub 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
1023type RestoreToolchainCallbacks = Vec<Box<dyn FnOnce() -> Result<()>>>;
1025
1026fn 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 != ¤t_version {
1062 fn override_solana_version(version: String) -> Result<bool> {
1066 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 if get_current_version(cmd_name).is_err() {
1077 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 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 if let Some(anchor_version) = &cfg.toolchain.anchor_version {
1149 const ANCHOR_BINARY_PREFIX: &str = "anchor-";
1151
1152 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 != ¤t_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
1200fn 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
1217fn 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
1228fn 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, 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 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 let extra_keywords = ["async", "await", "try"];
1529 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 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 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 fs::write(".gitignore", rust_template::git_ignore())?;
1570
1571 fs::write(".prettierignore", rust_template::prettier_ignore())?;
1573
1574 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 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 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 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 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 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 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
1709const PACKAGE_MANAGER_WATERFALL: &[PackageManager] = &[
1715 PackageManager::PNPM,
1716 PackageManager::Yarn,
1717 PackageManager::NPM,
1718];
1719
1720fn 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
1741fn 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
1803fn 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 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
1850pub type Files = Vec<(PathBuf, String)>;
1852
1853pub 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
1889pub 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 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 None => expand_all(&workspace_cfg, expansions_path, stdout, cargo_args),
1943 Some(cargo) if cargo.path().parent() == workspace_cfg.path().parent() => {
1945 expand_all(&workspace_cfg, expansions_path, stdout, cargo_args)
1946 }
1947 Some(cargo) => expand_program(
1949 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>, stderr: Option<File>, env_vars: Vec<String>,
2043 cargo_args: Vec<String>,
2044 no_docs: bool,
2045) -> Result<()> {
2046 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 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_anchor_version(&cfg).ok();
2062 check_deps(&cfg).ok();
2063
2064 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 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 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 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 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
2158fn 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>, stderr: Option<File>, 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#[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#[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 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 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 println!("Extracting the IDL");
2304 let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
2305 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 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 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 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 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 docker_cleanup(container_name, target_dir.as_path())?;
2408
2409 result
2411}
2412
2413fn docker_prep(container_name: &str, build_config: &BuildConfig) -> Result<()> {
2414 match build_config.bootstrap {
2417 BootstrapMode::Debian => {
2418 docker_exec(container_name, &["apt", "update"])?;
2420 docker_exec(
2421 container_name,
2422 &["apt", "install", "-y", "curl", "build-essential"],
2423 )?;
2424
2425 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 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 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 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 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 println!("Cleaning up the docker target directory");
2546 docker_exec(container_name, &["rm", "-rf", target_dir.to_str().unwrap()])?;
2547
2548 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 if !no_idl {
2591 check_idl_build_feature()?;
2592 }
2593
2594 if let Err(e) = cargo_build_sbf(None, &cargo_args) {
2598 eprintln!("{e}");
2599 std::process::exit(1);
2600 }
2601
2602 if !no_idl {
2604 let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
2605
2606 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 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_idl(&idl, OutFile::File(out))?;
2623 fs::write(&ts_out, idl_ts(&idl)?)?;
2625
2626 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
2642pub const BUILD_SUBCOMMAND: &[&str] = &["build-sbf", "--tools-version", "v1.52"];
2649
2650pub 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 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 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
2922fn 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 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
3016fn 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 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('.') .and_then(|(x, y)| y.find('.').map_or_else(|| Some((x, y)), |_| None)) .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
3343fn 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
3424fn 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 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 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#[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 cfg.validator = Some(validator_type);
3566
3567 let skip_local_validator =
3571 skip_local_validator || cfg.skip_local_validator.unwrap_or(false);
3572
3573 #[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 std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
3591
3592 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 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 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 *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#[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 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 test(
3838 cfg_override,
3839 None, true, true, skip_build, skip_lint, true, false, Vec::new(), ValidatorType::Surfpool,
3848 true, gdb, Vec::new(), Vec::new(), 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#[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 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 std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
3922
3923 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 if !skip_build {
3941 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 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 std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
4037
4038 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 if trace_path.exists() {
4060 fs::remove_dir_all(&trace_path)?;
4061 }
4062 fs::create_dir_all(&trace_path)?;
4063
4064 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 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#[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#[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#[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 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 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 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 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 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 if let Some(log_streams) = log_streams {
4333 for handle in log_streams {
4334 handle.shutdown();
4335 }
4336 }
4337
4338 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
4354fn 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 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 idl.address = address;
4393
4394 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 continue;
4432 };
4433 if key == "account" {
4434 for entry in value.as_array().unwrap() {
4435 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 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 if account.owner == bpf_loader_upgradeable::id()
4476 && 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 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 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
4526fn 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 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 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 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
4656struct LogStreamHandle {
4661 subscription: PubsubClientSubscription<RpcResponse<RpcLogsResponse>>,
4662}
4663
4664impl LogStreamHandle {
4665 fn shutdown(self) {
4667 std::thread::spawn(move || {
4671 let _ = self.subscription.send_unsubscribe();
4672 });
4673 }
4674}
4675
4676fn 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); 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 match &config.validator {
4715 Some(ValidatorType::Surfpool) => {
4716 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 let ws_url = if rpc_url.contains("127.0.0.1") || rpc_url.contains("localhost") {
4746 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 rpc_url
4758 .replace("https://", "wss://")
4759 .replace("http://", "ws://")
4760 };
4761
4762 std::thread::sleep(std::time::Duration::from_millis(1500));
4764
4765 let mut handles = vec![];
4766
4767 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 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_log_receiver_thread(receiver, log_file_path);
4800
4801 handles.push(LogStreamHandle {
4802 subscription: client,
4803 });
4804 }
4805
4806 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 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_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 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 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 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 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 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 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
5043fn 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
5055fn 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
5063fn 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 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 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 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 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 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 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 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 program::program_deploy(
5202 cfg_override,
5203 Some(strip_workspace_prefix(binary_path)),
5204 None, Some(strip_workspace_prefix(program_keypair_filepath)),
5206 None, None, None, None, no_idl,
5211 false, 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 program::program_upgrade(
5232 cfg_override,
5233 program_id,
5234 Some(program_filepath),
5235 None, None, None, 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 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 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 match program::discover_solana_programs(None) {
5335 Ok(programs) if !programs.is_empty() => {
5336 }
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 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 let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
5371
5372 let client = RpcClient::new(cluster_url);
5374
5375 let recipient_pubkey = if let Some(pubkey) = pubkey {
5377 pubkey
5378 } else {
5379 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 let lamports = (amount * 1_000_000_000.0) as u64;
5387
5388 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 client
5399 .confirm_transaction(&signature)
5400 .map_err(|e| anyhow!("Transaction confirmation failed: {}", e))?;
5401
5402 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 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 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 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 if let Some(keypair_path) = keypair {
5475 let expanded_path = shellexpand::tilde(&keypair_path.to_string_lossy()).to_string();
5476
5477 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 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 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 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 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
5611fn 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 let actual_program_id = program.pubkey()?.to_string();
5626
5627 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 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 'outer: for (cluster, programs) in &mut cfg.programs {
5656 if cluster != &cfg_cluster {
5658 continue;
5659 }
5660
5661 for (name, deployment) in programs {
5662 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 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
5693fn 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 let actual_program_id = program.pubkey()?.to_string();
5704
5705 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 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 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 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 if let Some(log_streams) = log_streams {
5829 for handle in log_streams {
5830 handle.shutdown();
5831 }
5832 }
5833
5834 Ok(())
5835 })?
5836}
5837
5838pub 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
5850fn target_dir_no_cache() -> Result<PathBuf> {
5852 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
5875fn 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
5906fn 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
5939fn 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
5948fn 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
5974fn 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 !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 !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
6024fn 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
6045fn 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
6059fn 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 let keypair = Keypair::read_from_file(&wallet_path)
6069 .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
6070
6071 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 let client = RpcClient::new(cluster_url);
6082
6083 let account_pubkey = if let Some(pubkey) = pubkey {
6085 pubkey
6086 } else {
6087 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 let balance = client.get_balance(&account_pubkey)?;
6095
6096 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 let client = RpcClient::new(cluster_url);
6111
6112 let epoch_info = client.get_epoch_info()?;
6114
6115 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 let client = RpcClient::new(cluster_url);
6126
6127 let epoch_info = client.get_epoch_info()?;
6129
6130 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 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 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 if let Ok(samples) = client.get_recent_performance_samples(Some(60)) {
6161 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 let remaining_secs = (remaining_slots * avg_slot_time_ms) / 1000;
6173
6174 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 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 ((current_time - start_time) as u64, false)
6196 } else {
6197 ((epoch_info.slot_index * avg_slot_time_ms) / 1000, true)
6199 };
6200
6201 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
6218fn 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 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}