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