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