1use crate::config::{
2 get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, Manifest,
3 PackageManager, ProgramArch, ProgramDeployment, ProgramWorkspace, ScriptsConfig, TestValidator,
4 WithPath, SHUTDOWN_WAIT, STARTUP_WAIT,
5};
6use anchor_client::Cluster;
7use anchor_lang::idl::{IdlAccount, IdlInstruction, ERASED_AUTHORITY};
8use anchor_lang::prelude::UpgradeableLoaderState;
9use anchor_lang::solana_program::bpf_loader_upgradeable;
10use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize, Discriminator};
11use anchor_lang_idl::convert::convert_idl;
12use anchor_lang_idl::types::{Idl, IdlArrayLen, IdlDefinedFields, IdlType, IdlTypeDefTy};
13use anyhow::{anyhow, bail, Context, Result};
14use checks::{check_anchor_version, check_deps, check_idl_build_feature, check_overflow};
15use clap::{CommandFactory, Parser};
16use dirs::home_dir;
17use flate2::read::ZlibDecoder;
18use flate2::write::ZlibEncoder;
19use flate2::Compression;
20use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
21use regex::{Regex, RegexBuilder};
22use rust_template::{ProgramTemplate, TestTemplate};
23use semver::{Version, VersionReq};
24use serde_json::{json, Map, Value as JsonValue};
25use solana_rpc_client::rpc_client::RpcClient;
26use solana_sdk::commitment_config::CommitmentConfig;
27use solana_sdk::compute_budget::ComputeBudgetInstruction;
28use solana_sdk::instruction::{AccountMeta, Instruction};
29use solana_sdk::pubkey::Pubkey;
30use solana_sdk::signature::Keypair;
31use solana_sdk::signature::Signer;
32use solana_sdk::signer::EncodableKey;
33use solana_sdk::transaction::Transaction;
34use std::collections::BTreeMap;
35use std::collections::HashMap;
36use std::collections::HashSet;
37use std::ffi::OsString;
38use std::fs::{self, File};
39use std::io::prelude::*;
40use std::path::{Path, PathBuf};
41use std::process::{Child, Stdio};
42use std::str::FromStr;
43use std::string::ToString;
44use std::sync::LazyLock;
45
46mod checks;
47pub mod config;
48pub mod rust_template;
49
50pub const VERSION: &str = env!("CARGO_PKG_VERSION");
52pub const DOCKER_BUILDER_VERSION: &str = VERSION;
53
54pub const DEFAULT_RPC_PORT: u16 = 8899;
56
57pub static AVM_HOME: LazyLock<PathBuf> = LazyLock::new(|| {
58 if let Ok(avm_home) = std::env::var("AVM_HOME") {
59 PathBuf::from(avm_home)
60 } else {
61 let mut user_home = dirs::home_dir().expect("Could not find home directory");
62 user_home.push(".avm");
63 user_home
64 }
65});
66
67#[derive(Debug, Parser)]
68#[clap(version = VERSION)]
69pub struct Opts {
70 #[clap(flatten)]
71 pub cfg_override: ConfigOverride,
72 #[clap(subcommand)]
73 pub command: Command,
74}
75
76#[derive(Debug, Parser)]
77pub enum Command {
78 Init {
80 name: String,
82 #[clap(short, long)]
84 javascript: bool,
85 #[clap(long)]
87 no_install: bool,
88 #[clap(value_enum, long, default_value = "yarn")]
90 package_manager: PackageManager,
91 #[clap(long)]
93 no_git: bool,
94 #[clap(value_enum, short, long, default_value = "single")]
96 template: ProgramTemplate,
97 #[clap(value_enum, long, default_value = "mocha")]
99 test_template: TestTemplate,
100 #[clap(long, action)]
102 force: bool,
103 },
104 #[clap(name = "build", alias = "b")]
106 Build {
107 #[clap(long)]
109 skip_lint: bool,
110 #[clap(long)]
112 no_idl: bool,
113 #[clap(short, long)]
115 idl: Option<String>,
116 #[clap(short = 't', long)]
118 idl_ts: Option<String>,
119 #[clap(short, long)]
121 verifiable: bool,
122 #[clap(short, long)]
124 program_name: Option<String>,
125 #[clap(short, long)]
128 solana_version: Option<String>,
129 #[clap(short, long)]
131 docker_image: Option<String>,
132 #[clap(value_enum, short, long, default_value = "none")]
135 bootstrap: BootstrapMode,
136 #[clap(short, long, required = false)]
138 env: Vec<String>,
139 #[clap(required = false, last = true)]
141 cargo_args: Vec<String>,
142 #[clap(long)]
144 no_docs: bool,
145 #[clap(value_enum, long, default_value = "sbf")]
147 arch: ProgramArch,
148 },
149 Expand {
156 #[clap(short, long)]
158 program_name: Option<String>,
159 #[clap(required = false, last = true)]
161 cargo_args: Vec<String>,
162 },
163 Verify {
167 program_id: Pubkey,
169 #[clap(long, conflicts_with = "current_dir")]
171 repo_url: Option<String>,
172 #[clap(long, requires = "repo_url")]
174 commit_hash: Option<String>,
175 #[clap(long)]
177 current_dir: bool,
178 #[clap(long)]
180 program_name: Option<String>,
181 #[clap(raw = true)]
183 args: Vec<String>,
184 },
185 #[clap(name = "test", alias = "t")]
186 Test {
188 #[clap(short, long)]
190 program_name: Option<String>,
191 #[clap(long)]
194 skip_deploy: bool,
195 #[clap(long)]
198 skip_lint: bool,
199 #[clap(long)]
202 skip_local_validator: bool,
203 #[clap(long)]
206 skip_build: bool,
207 #[clap(long)]
209 no_idl: bool,
210 #[clap(value_enum, long, default_value = "sbf")]
212 arch: ProgramArch,
213 #[clap(long)]
216 detach: bool,
217 #[clap(long)]
219 run: Vec<String>,
220 args: Vec<String>,
221 #[clap(short, long, required = false)]
223 env: Vec<String>,
224 #[clap(required = false, last = true)]
226 cargo_args: Vec<String>,
227 },
228 New {
230 name: String,
232 #[clap(value_enum, short, long, default_value = "single")]
234 template: ProgramTemplate,
235 #[clap(long, action)]
237 force: bool,
238 },
239 Idl {
241 #[clap(subcommand)]
242 subcmd: IdlCommand,
243 },
244 Clean,
246 Deploy {
248 #[clap(short, long)]
250 program_name: Option<String>,
251 #[clap(long, requires = "program_name")]
253 program_keypair: Option<String>,
254 #[clap(short, long)]
256 verifiable: bool,
257 #[clap(long)]
259 no_idl: bool,
260 #[clap(required = false, last = true)]
262 solana_args: Vec<String>,
263 },
264 Migrate,
266 Upgrade {
270 #[clap(short, long)]
272 program_id: Pubkey,
273 program_filepath: String,
275 #[clap(long, default_value = "0")]
277 max_retries: u32,
278 #[clap(required = false, last = true)]
280 solana_args: Vec<String>,
281 },
282 #[cfg(feature = "dev")]
283 Airdrop {
285 #[clap(short, long)]
286 url: Option<String>,
287 },
288 Cluster {
290 #[clap(subcommand)]
291 subcmd: ClusterCommand,
292 },
293 Shell,
296 Run {
298 script: String,
300 #[clap(required = false, last = true)]
302 script_args: Vec<String>,
303 },
304 Login {
306 token: String,
308 },
309 Keys {
311 #[clap(subcommand)]
312 subcmd: KeysCommand,
313 },
314 Localnet {
316 #[clap(long)]
319 skip_build: bool,
320 #[clap(long)]
323 skip_deploy: bool,
324 #[clap(long)]
327 skip_lint: bool,
328 #[clap(value_enum, long, default_value = "sbf")]
330 arch: ProgramArch,
331 #[clap(short, long, required = false)]
333 env: Vec<String>,
334 #[clap(required = false, last = true)]
336 cargo_args: Vec<String>,
337 },
338 Account {
340 account_type: String,
342 address: Pubkey,
344 #[clap(long)]
346 idl: Option<String>,
347 },
348 Completions {
350 #[clap(value_enum)]
351 shell: clap_complete::Shell,
352 },
353}
354
355#[derive(Debug, Parser)]
356pub enum KeysCommand {
357 List,
359 Sync {
361 #[clap(short, long)]
363 program_name: Option<String>,
364 },
365}
366
367#[derive(Debug, Parser)]
368pub enum IdlCommand {
369 Init {
371 program_id: Pubkey,
372 #[clap(short, long)]
373 filepath: String,
374 #[clap(long)]
375 priority_fee: Option<u64>,
376 },
377 Close {
378 program_id: Pubkey,
379 #[clap(long)]
381 idl_address: Option<Pubkey>,
382 #[clap(long)]
385 print_only: bool,
386 #[clap(long)]
387 priority_fee: Option<u64>,
388 },
389 WriteBuffer {
392 program_id: Pubkey,
393 #[clap(short, long)]
394 filepath: String,
395 #[clap(long)]
396 priority_fee: Option<u64>,
397 },
398 SetBuffer {
400 program_id: Pubkey,
401 #[clap(short, long)]
403 buffer: Pubkey,
404 #[clap(long)]
407 print_only: bool,
408 #[clap(long)]
409 priority_fee: Option<u64>,
410 },
411 Upgrade {
414 program_id: Pubkey,
415 #[clap(short, long)]
416 filepath: String,
417 #[clap(long)]
418 priority_fee: Option<u64>,
419 },
420 SetAuthority {
422 address: Option<Pubkey>,
425 #[clap(short, long)]
427 program_id: Pubkey,
428 #[clap(short, long)]
430 new_authority: Pubkey,
431 #[clap(long)]
434 print_only: bool,
435 #[clap(long)]
436 priority_fee: Option<u64>,
437 },
438 EraseAuthority {
442 #[clap(short, long)]
443 program_id: Pubkey,
444 #[clap(long)]
445 priority_fee: Option<u64>,
446 },
447 Authority {
449 program_id: Pubkey,
451 },
452 #[clap(alias = "b")]
454 Build {
455 #[clap(short, long)]
457 program_name: Option<String>,
458 #[clap(short, long)]
460 out: Option<String>,
461 #[clap(short = 't', long)]
463 out_ts: Option<String>,
464 #[clap(long)]
466 no_docs: bool,
467 #[clap(long)]
469 skip_lint: bool,
470 #[clap(required = false, last = true)]
472 cargo_args: Vec<String>,
473 },
474 Fetch {
477 address: Pubkey,
478 #[clap(short, long)]
480 out: Option<String>,
481 },
482 Convert {
484 path: String,
486 #[clap(short, long)]
488 out: Option<String>,
489 #[clap(short, long)]
491 program_id: Option<Pubkey>,
492 },
493 Type {
495 path: String,
497 #[clap(short, long)]
499 out: Option<String>,
500 },
501}
502
503#[derive(Debug, Parser)]
504pub enum ClusterCommand {
505 List,
507}
508
509fn get_keypair(path: &str) -> Result<Keypair> {
510 solana_sdk::signature::read_keypair_file(path)
511 .map_err(|_| anyhow!("Unable to read keypair file ({path})"))
512}
513
514pub fn entry(opts: Opts) -> Result<()> {
515 let restore_cbs = override_toolchain(&opts.cfg_override)?;
516 let result = process_command(opts);
517 restore_toolchain(restore_cbs)?;
518
519 result
520}
521
522type RestoreToolchainCallbacks = Vec<Box<dyn FnOnce() -> Result<()>>>;
524
525fn override_toolchain(cfg_override: &ConfigOverride) -> Result<RestoreToolchainCallbacks> {
529 let mut restore_cbs: RestoreToolchainCallbacks = vec![];
530
531 let cfg = Config::discover(cfg_override)?;
532 if let Some(cfg) = cfg {
533 fn parse_version(text: &str) -> Option<String> {
534 Some(
535 Regex::new(r"(\d+\.\d+\.\S+)")
536 .unwrap()
537 .captures_iter(text)
538 .next()?
539 .get(0)?
540 .as_str()
541 .to_string(),
542 )
543 }
544
545 fn get_current_version(cmd_name: &str) -> Result<String> {
546 let output = std::process::Command::new(cmd_name)
547 .arg("--version")
548 .output()?;
549 if !output.status.success() {
550 return Err(anyhow!("Failed to run `{cmd_name} --version`"));
551 }
552
553 let output_version = std::str::from_utf8(&output.stdout)?;
554 parse_version(output_version)
555 .ok_or_else(|| anyhow!("Failed to parse the version of `{cmd_name}`"))
556 }
557
558 if let Some(solana_version) = &cfg.toolchain.solana_version {
559 let current_version = get_current_version("solana")?;
560 if solana_version != ¤t_version {
561 fn override_solana_version(version: String) -> Result<bool> {
565 let (cmd_name, domain) =
568 if Version::parse(&version)? < Version::parse("1.18.19")? {
569 ("solana-install", "solana.com")
570 } else {
571 ("agave-install", "anza.xyz")
572 };
573
574 if get_current_version(cmd_name).is_err() {
576 eprintln!(
583 "Command not installed: `{cmd_name}`. \
584 See https://github.com/anza-xyz/agave/wiki/Agave-Transition, \
585 installing..."
586 );
587 let install_script = std::process::Command::new("curl")
588 .args([
589 "-sSfL",
590 &format!("https://release.{domain}/v{version}/install"),
591 ])
592 .output()?;
593 let is_successful = std::process::Command::new("sh")
594 .args(["-c", std::str::from_utf8(&install_script.stdout)?])
595 .spawn()?
596 .wait_with_output()?
597 .status
598 .success();
599 if !is_successful {
600 return Err(anyhow!("Failed to install `{cmd_name}`"));
601 }
602 }
603
604 let output = std::process::Command::new(cmd_name).arg("list").output()?;
605 if !output.status.success() {
606 return Err(anyhow!("Failed to list installed `solana` versions"));
607 }
608
609 let is_installed = std::str::from_utf8(&output.stdout)?
611 .lines()
612 .filter_map(parse_version)
613 .any(|line_version| line_version == version);
614 let (stderr, stdout) = if is_installed {
615 (Stdio::null(), Stdio::null())
616 } else {
617 (Stdio::inherit(), Stdio::inherit())
618 };
619
620 std::process::Command::new(cmd_name)
621 .arg("init")
622 .arg(&version)
623 .stderr(stderr)
624 .stdout(stdout)
625 .spawn()?
626 .wait()
627 .map(|status| status.success())
628 .map_err(|err| anyhow!("Failed to run `{cmd_name}` command: {err}"))
629 }
630
631 match override_solana_version(solana_version.to_owned())? {
632 true => restore_cbs.push(Box::new(|| {
633 match override_solana_version(current_version)? {
634 true => Ok(()),
635 false => Err(anyhow!("Failed to restore `solana` version")),
636 }
637 })),
638 false => eprintln!(
639 "Failed to override `solana` version to {solana_version}, \
640 using {current_version} instead"
641 ),
642 }
643 }
644 }
645
646 if let Some(anchor_version) = &cfg.toolchain.anchor_version {
648 const ANCHOR_BINARY_PREFIX: &str = "anchor-";
650
651 let current_version = std::env::args()
654 .next()
655 .expect("First arg should exist")
656 .parse::<PathBuf>()?
657 .file_name()
658 .and_then(|name| name.to_str())
659 .expect("File name should be valid Unicode")
660 .split_once(ANCHOR_BINARY_PREFIX)
661 .map(|(_, version)| version)
662 .unwrap_or(VERSION)
663 .to_owned();
664 if anchor_version != ¤t_version {
665 let binary_path = home_dir()
666 .unwrap()
667 .join(".avm")
668 .join("bin")
669 .join(format!("{ANCHOR_BINARY_PREFIX}{anchor_version}"));
670
671 if !binary_path.exists() {
672 eprintln!(
673 "`anchor` {anchor_version} is not installed with `avm`. Installing...\n"
674 );
675
676 if let Err(e) = install_with_avm(anchor_version, false) {
677 eprintln!(
678 "Failed to install `anchor`: {e}, using {current_version} instead"
679 );
680 return Ok(restore_cbs);
681 }
682 }
683
684 let exit_code = std::process::Command::new(binary_path)
685 .args(std::env::args_os().skip(1))
686 .spawn()?
687 .wait()?
688 .code()
689 .unwrap_or(1);
690 restore_toolchain(restore_cbs)?;
691 std::process::exit(exit_code);
692 }
693 }
694 }
695
696 Ok(restore_cbs)
697}
698
699fn install_with_avm(version: &str, verify: bool) -> Result<()> {
702 let mut cmd = std::process::Command::new("avm");
703 cmd.arg("install");
704 cmd.arg(version);
705 cmd.arg("--force");
706 if verify {
707 cmd.arg("--verify");
708 }
709 let status = cmd.status().context("running AVM")?;
710 if !status.success() {
711 bail!("failed to install `anchor` {version} with avm");
712 }
713 Ok(())
714}
715
716fn restore_toolchain(restore_cbs: RestoreToolchainCallbacks) -> Result<()> {
718 for restore_toolchain in restore_cbs {
719 if let Err(e) = restore_toolchain() {
720 eprintln!("Toolchain error: {e}");
721 }
722 }
723
724 Ok(())
725}
726
727fn get_npm_init_license() -> Result<String> {
729 let npm_init_license_output = std::process::Command::new("npm")
730 .arg("config")
731 .arg("get")
732 .arg("init-license")
733 .output()?;
734
735 if !npm_init_license_output.status.success() {
736 return Err(anyhow!("Failed to get npm init license"));
737 }
738
739 let license = String::from_utf8(npm_init_license_output.stdout)?;
740 Ok(license.trim().to_string())
741}
742
743fn process_command(opts: Opts) -> Result<()> {
744 match opts.command {
745 Command::Init {
746 name,
747 javascript,
748 no_install,
749 package_manager,
750 no_git,
751 template,
752 test_template,
753 force,
754 } => init(
755 &opts.cfg_override,
756 name,
757 javascript,
758 no_install,
759 package_manager,
760 no_git,
761 template,
762 test_template,
763 force,
764 ),
765 Command::New {
766 name,
767 template,
768 force,
769 } => new(&opts.cfg_override, name, template, force),
770 Command::Build {
771 no_idl,
772 idl,
773 idl_ts,
774 verifiable,
775 program_name,
776 solana_version,
777 docker_image,
778 bootstrap,
779 cargo_args,
780 env,
781 skip_lint,
782 no_docs,
783 arch,
784 } => build(
785 &opts.cfg_override,
786 no_idl,
787 idl,
788 idl_ts,
789 verifiable,
790 skip_lint,
791 program_name,
792 solana_version,
793 docker_image,
794 bootstrap,
795 None,
796 None,
797 env,
798 cargo_args,
799 no_docs,
800 arch,
801 ),
802 Command::Verify {
803 program_id,
804 repo_url,
805 commit_hash,
806 current_dir,
807 program_name,
808 args,
809 } => verify(
810 program_id,
811 repo_url,
812 commit_hash,
813 current_dir,
814 program_name,
815 args,
816 ),
817 Command::Clean => clean(&opts.cfg_override),
818 Command::Deploy {
819 program_name,
820 program_keypair,
821 verifiable,
822 no_idl,
823 solana_args,
824 } => deploy(
825 &opts.cfg_override,
826 program_name,
827 program_keypair,
828 verifiable,
829 no_idl,
830 solana_args,
831 ),
832 Command::Expand {
833 program_name,
834 cargo_args,
835 } => expand(&opts.cfg_override, program_name, &cargo_args),
836 Command::Upgrade {
837 program_id,
838 program_filepath,
839 max_retries,
840 solana_args,
841 } => upgrade(
842 &opts.cfg_override,
843 program_id,
844 program_filepath,
845 max_retries,
846 solana_args,
847 ),
848 Command::Idl { subcmd } => idl(&opts.cfg_override, subcmd),
849 Command::Migrate => migrate(&opts.cfg_override),
850 Command::Test {
851 program_name,
852 skip_deploy,
853 skip_local_validator,
854 skip_build,
855 no_idl,
856 detach,
857 run,
858 args,
859 env,
860 cargo_args,
861 skip_lint,
862 arch,
863 } => test(
864 &opts.cfg_override,
865 program_name,
866 skip_deploy,
867 skip_local_validator,
868 skip_build,
869 skip_lint,
870 no_idl,
871 detach,
872 run,
873 args,
874 env,
875 cargo_args,
876 arch,
877 ),
878 #[cfg(feature = "dev")]
879 Command::Airdrop { .. } => airdrop(&opts.cfg_override),
880 Command::Cluster { subcmd } => cluster(subcmd),
881 Command::Shell => shell(&opts.cfg_override),
882 Command::Run {
883 script,
884 script_args,
885 } => run(&opts.cfg_override, script, script_args),
886 Command::Login { token } => login(&opts.cfg_override, token),
887 Command::Keys { subcmd } => keys(&opts.cfg_override, subcmd),
888 Command::Localnet {
889 skip_build,
890 skip_deploy,
891 skip_lint,
892 env,
893 cargo_args,
894 arch,
895 } => localnet(
896 &opts.cfg_override,
897 skip_build,
898 skip_deploy,
899 skip_lint,
900 env,
901 cargo_args,
902 arch,
903 ),
904 Command::Account {
905 account_type,
906 address,
907 idl,
908 } => account(&opts.cfg_override, account_type, address, idl),
909 Command::Completions { shell } => {
910 clap_complete::generate(
911 shell,
912 &mut Opts::command(),
913 "anchor",
914 &mut std::io::stdout(),
915 );
916 Ok(())
917 }
918 }
919}
920
921#[allow(clippy::too_many_arguments)]
922fn init(
923 cfg_override: &ConfigOverride,
924 name: String,
925 javascript: bool,
926 no_install: bool,
927 package_manager: PackageManager,
928 no_git: bool,
929 template: ProgramTemplate,
930 test_template: TestTemplate,
931 force: bool,
932) -> Result<()> {
933 if !force && Config::discover(cfg_override)?.is_some() {
934 return Err(anyhow!("Workspace already initialized"));
935 }
936
937 let rust_name = name.to_snake_case();
939 let project_name = if name == rust_name {
940 rust_name.clone()
941 } else {
942 name.to_kebab_case()
943 };
944
945 let extra_keywords = ["async", "await", "try"];
948 if syn::parse_str::<syn::Ident>(&rust_name).is_err()
950 || extra_keywords.contains(&rust_name.as_str())
951 {
952 return Err(anyhow!(
953 "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.",
954 ));
955 }
956
957 if force {
958 fs::create_dir_all(&project_name)?;
959 } else {
960 fs::create_dir(&project_name)?;
961 }
962 std::env::set_current_dir(&project_name)?;
963 fs::create_dir_all("app")?;
964
965 let mut cfg = Config::default();
966
967 let test_script = test_template.get_test_script(javascript, &package_manager);
968 cfg.scripts.insert("test".to_owned(), test_script);
969
970 let package_manager_cmd = package_manager.to_string();
971 cfg.toolchain.package_manager = Some(package_manager);
972
973 let mut localnet = BTreeMap::new();
974 let program_id = rust_template::get_or_create_program_id(&rust_name);
975 localnet.insert(
976 rust_name,
977 ProgramDeployment {
978 address: program_id,
979 path: None,
980 idl: None,
981 },
982 );
983 cfg.programs.insert(Cluster::Localnet, localnet);
984 let toml = cfg.to_string();
985 fs::write("Anchor.toml", toml)?;
986
987 fs::write(".gitignore", rust_template::git_ignore())?;
989
990 fs::write(".prettierignore", rust_template::prettier_ignore())?;
992
993 if force {
995 fs::remove_dir_all(
996 std::env::current_dir()?
997 .join("programs")
998 .join(&project_name),
999 )?;
1000 }
1001
1002 rust_template::create_program(
1004 &project_name,
1005 template,
1006 TestTemplate::Mollusk == test_template,
1007 )?;
1008
1009 let migrations_path = Path::new("migrations");
1011 fs::create_dir_all(migrations_path)?;
1012
1013 let license = get_npm_init_license()?;
1014
1015 let jest = TestTemplate::Jest == test_template;
1016 if javascript {
1017 let mut package_json = File::create("package.json")?;
1019 package_json.write_all(rust_template::package_json(jest, license).as_bytes())?;
1020
1021 let mut deploy = File::create(migrations_path.join("deploy.js"))?;
1022 deploy.write_all(rust_template::deploy_script().as_bytes())?;
1023 } else {
1024 let mut ts_config = File::create("tsconfig.json")?;
1026 ts_config.write_all(rust_template::ts_config(jest).as_bytes())?;
1027
1028 let mut ts_package_json = File::create("package.json")?;
1029 ts_package_json.write_all(rust_template::ts_package_json(jest, license).as_bytes())?;
1030
1031 let mut deploy = File::create(migrations_path.join("deploy.ts"))?;
1032 deploy.write_all(rust_template::ts_deploy_script().as_bytes())?;
1033 }
1034
1035 test_template.create_test_files(&project_name, javascript, &program_id.to_string())?;
1036
1037 if !no_install {
1038 let package_manager_result = install_node_modules(&package_manager_cmd)?;
1039
1040 if !package_manager_result.status.success() && package_manager_cmd != "npm" {
1041 println!("Failed {package_manager_cmd} install will attempt to npm install");
1042 install_node_modules("npm")?;
1043 } else {
1044 eprintln!("Failed to install node modules");
1045 }
1046 }
1047
1048 if !no_git {
1049 let git_result = std::process::Command::new("git")
1050 .arg("init")
1051 .stdout(Stdio::inherit())
1052 .stderr(Stdio::inherit())
1053 .output()
1054 .map_err(|e| anyhow::format_err!("git init failed: {}", e.to_string()))?;
1055 if !git_result.status.success() {
1056 eprintln!("Failed to automatically initialize a new git repository");
1057 }
1058 }
1059
1060 println!("{project_name} initialized");
1061
1062 Ok(())
1063}
1064
1065fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
1066 if cfg!(target_os = "windows") {
1067 std::process::Command::new("cmd")
1068 .arg(format!("/C {cmd} install"))
1069 .stdout(Stdio::inherit())
1070 .stderr(Stdio::inherit())
1071 .output()
1072 .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
1073 } else {
1074 std::process::Command::new(cmd)
1075 .arg("install")
1076 .stdout(Stdio::inherit())
1077 .stderr(Stdio::inherit())
1078 .output()
1079 .map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e.to_string()))
1080 }
1081}
1082
1083fn new(
1085 cfg_override: &ConfigOverride,
1086 name: String,
1087 template: ProgramTemplate,
1088 force: bool,
1089) -> Result<()> {
1090 with_workspace(cfg_override, |cfg| {
1091 match cfg.path().parent() {
1092 None => {
1093 println!("Unable to make new program");
1094 }
1095 Some(parent) => {
1096 std::env::set_current_dir(parent)?;
1097
1098 let cluster = cfg.provider.cluster.clone();
1099 let programs = cfg.programs.entry(cluster).or_default();
1100 if programs.contains_key(&name) {
1101 if !force {
1102 return Err(anyhow!("Program already exists"));
1103 }
1104
1105 fs::remove_dir_all(std::env::current_dir()?.join("programs").join(&name))?;
1107 }
1108
1109 rust_template::create_program(&name, template, false)?;
1110
1111 programs.insert(
1112 name.clone(),
1113 ProgramDeployment {
1114 address: rust_template::get_or_create_program_id(&name),
1115 path: None,
1116 idl: None,
1117 },
1118 );
1119
1120 let toml = cfg.to_string();
1121 fs::write("Anchor.toml", toml)?;
1122
1123 println!("Created new program.");
1124 }
1125 };
1126 Ok(())
1127 })
1128}
1129
1130pub type Files = Vec<(PathBuf, String)>;
1132
1133pub fn create_files(files: &Files) -> Result<()> {
1141 for (path, content) in files {
1142 let path = path
1143 .display()
1144 .to_string()
1145 .replace('/', std::path::MAIN_SEPARATOR_STR);
1146 let path = Path::new(&path);
1147 if path.exists() {
1148 continue;
1149 }
1150
1151 match path.extension() {
1152 Some(_) => {
1153 fs::create_dir_all(path.parent().unwrap())?;
1154 fs::write(path, content)?;
1155 }
1156 None => fs::create_dir_all(path)?,
1157 }
1158 }
1159
1160 Ok(())
1161}
1162
1163pub fn override_or_create_files(files: &Files) -> Result<()> {
1171 for (path, content) in files {
1172 let path = Path::new(path);
1173 if path.exists() {
1174 let mut f = fs::OpenOptions::new()
1175 .write(true)
1176 .truncate(true)
1177 .open(path)?;
1178 f.write_all(content.as_bytes())?;
1179 f.flush()?;
1180 } else {
1181 fs::create_dir_all(path.parent().unwrap())?;
1182 fs::write(path, content)?;
1183 }
1184 }
1185
1186 Ok(())
1187}
1188
1189pub fn expand(
1190 cfg_override: &ConfigOverride,
1191 program_name: Option<String>,
1192 cargo_args: &[String],
1193) -> Result<()> {
1194 if let Some(program_name) = program_name.as_ref() {
1196 cd_member(cfg_override, program_name)?;
1197 }
1198
1199 let workspace_cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
1200 let cfg_parent = workspace_cfg.path().parent().expect("Invalid Anchor.toml");
1201 let cargo = Manifest::discover()?;
1202
1203 let expansions_path = cfg_parent.join(".anchor").join("expanded-macros");
1204 fs::create_dir_all(&expansions_path)?;
1205
1206 match cargo {
1207 None => expand_all(&workspace_cfg, expansions_path, cargo_args),
1209 Some(cargo) if cargo.path().parent() == workspace_cfg.path().parent() => {
1211 expand_all(&workspace_cfg, expansions_path, cargo_args)
1212 }
1213 Some(cargo) => expand_program(
1215 cargo.path().parent().unwrap().to_path_buf(),
1217 expansions_path,
1218 cargo_args,
1219 ),
1220 }
1221}
1222
1223fn expand_all(
1224 workspace_cfg: &WithPath<Config>,
1225 expansions_path: PathBuf,
1226 cargo_args: &[String],
1227) -> Result<()> {
1228 let cur_dir = std::env::current_dir()?;
1229 for p in workspace_cfg.get_rust_program_list()? {
1230 expand_program(p, expansions_path.clone(), cargo_args)?;
1231 }
1232 std::env::set_current_dir(cur_dir)?;
1233 Ok(())
1234}
1235
1236fn expand_program(
1237 program_path: PathBuf,
1238 expansions_path: PathBuf,
1239 cargo_args: &[String],
1240) -> Result<()> {
1241 let cargo = Manifest::from_path(program_path.join("Cargo.toml"))
1242 .map_err(|_| anyhow!("Could not find Cargo.toml for program"))?;
1243
1244 let target_dir_arg = {
1245 let mut target_dir_arg = OsString::from("--target-dir=");
1246 target_dir_arg.push(expansions_path.join("expand-target"));
1247 target_dir_arg
1248 };
1249
1250 let package_name = &cargo
1251 .package
1252 .as_ref()
1253 .ok_or_else(|| anyhow!("Cargo config is missing a package"))?
1254 .name;
1255 let program_expansions_path = expansions_path.join(package_name);
1256 fs::create_dir_all(&program_expansions_path)?;
1257
1258 let exit = std::process::Command::new("cargo")
1259 .arg("expand")
1260 .arg(target_dir_arg)
1261 .arg(format!("--package={package_name}"))
1262 .args(cargo_args)
1263 .stderr(Stdio::inherit())
1264 .output()
1265 .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
1266 if !exit.status.success() {
1267 eprintln!("'anchor expand' failed. Perhaps you have not installed 'cargo-expand'? https://github.com/dtolnay/cargo-expand#installation");
1268 std::process::exit(exit.status.code().unwrap_or(1));
1269 }
1270
1271 let version = cargo.version();
1272 let time = chrono::Utc::now().to_string().replace(' ', "_");
1273 let file_path = program_expansions_path.join(format!("{package_name}-{version}-{time}.rs"));
1274 fs::write(&file_path, &exit.stdout).map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
1275
1276 println!(
1277 "Expanded {} into file {}\n",
1278 package_name,
1279 file_path.to_string_lossy()
1280 );
1281 Ok(())
1282}
1283
1284#[allow(clippy::too_many_arguments)]
1285pub fn build(
1286 cfg_override: &ConfigOverride,
1287 no_idl: bool,
1288 idl: Option<String>,
1289 idl_ts: Option<String>,
1290 verifiable: bool,
1291 skip_lint: bool,
1292 program_name: Option<String>,
1293 solana_version: Option<String>,
1294 docker_image: Option<String>,
1295 bootstrap: BootstrapMode,
1296 stdout: Option<File>, stderr: Option<File>, env_vars: Vec<String>,
1299 cargo_args: Vec<String>,
1300 no_docs: bool,
1301 arch: ProgramArch,
1302) -> Result<()> {
1303 if let Some(program_name) = program_name.as_ref() {
1305 cd_member(cfg_override, program_name)?;
1306 }
1307 let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
1308 let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
1309
1310 let workspace_cargo_toml_path = cfg_parent.join("Cargo.toml");
1312 if workspace_cargo_toml_path.exists() {
1313 check_overflow(workspace_cargo_toml_path)?;
1314 }
1315
1316 check_anchor_version(&cfg).ok();
1318 check_deps(&cfg).ok();
1319
1320 let idl_out = match idl {
1321 Some(idl) => Some(PathBuf::from(idl)),
1322 None => Some(cfg_parent.join("target").join("idl")),
1323 };
1324 fs::create_dir_all(idl_out.as_ref().unwrap())?;
1325
1326 let idl_ts_out = match idl_ts {
1327 Some(idl_ts) => Some(PathBuf::from(idl_ts)),
1328 None => Some(cfg_parent.join("target").join("types")),
1329 };
1330 fs::create_dir_all(idl_ts_out.as_ref().unwrap())?;
1331
1332 if !cfg.workspace.types.is_empty() {
1333 fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
1334 };
1335
1336 let cargo = Manifest::discover()?;
1337 let build_config = BuildConfig {
1338 verifiable,
1339 solana_version: solana_version.or_else(|| cfg.toolchain.solana_version.clone()),
1340 docker_image: docker_image.unwrap_or_else(|| cfg.docker()),
1341 bootstrap,
1342 };
1343 match cargo {
1344 None => build_all(
1346 &cfg,
1347 cfg.path(),
1348 no_idl,
1349 idl_out,
1350 idl_ts_out,
1351 &build_config,
1352 stdout,
1353 stderr,
1354 env_vars,
1355 cargo_args,
1356 skip_lint,
1357 no_docs,
1358 arch,
1359 )?,
1360 Some(cargo) if cargo.path().parent() == cfg.path().parent() => build_all(
1362 &cfg,
1363 cfg.path(),
1364 no_idl,
1365 idl_out,
1366 idl_ts_out,
1367 &build_config,
1368 stdout,
1369 stderr,
1370 env_vars,
1371 cargo_args,
1372 skip_lint,
1373 no_docs,
1374 arch,
1375 )?,
1376 Some(cargo) => build_rust_cwd(
1378 &cfg,
1379 cargo.path().to_path_buf(),
1380 no_idl,
1381 idl_out,
1382 idl_ts_out,
1383 &build_config,
1384 stdout,
1385 stderr,
1386 env_vars,
1387 cargo_args,
1388 skip_lint,
1389 no_docs,
1390 &arch,
1391 )?,
1392 }
1393
1394 set_workspace_dir_or_exit();
1395
1396 Ok(())
1397}
1398
1399#[allow(clippy::too_many_arguments)]
1400fn build_all(
1401 cfg: &WithPath<Config>,
1402 cfg_path: &Path,
1403 no_idl: bool,
1404 idl_out: Option<PathBuf>,
1405 idl_ts_out: Option<PathBuf>,
1406 build_config: &BuildConfig,
1407 stdout: Option<File>, stderr: Option<File>, env_vars: Vec<String>,
1410 cargo_args: Vec<String>,
1411 skip_lint: bool,
1412 no_docs: bool,
1413 arch: ProgramArch,
1414) -> Result<()> {
1415 let cur_dir = std::env::current_dir()?;
1416 let r = match cfg_path.parent() {
1417 None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())),
1418 Some(_parent) => {
1419 for p in cfg.get_rust_program_list()? {
1420 build_rust_cwd(
1421 cfg,
1422 p.join("Cargo.toml"),
1423 no_idl,
1424 idl_out.clone(),
1425 idl_ts_out.clone(),
1426 build_config,
1427 stdout.as_ref().map(|f| f.try_clone()).transpose()?,
1428 stderr.as_ref().map(|f| f.try_clone()).transpose()?,
1429 env_vars.clone(),
1430 cargo_args.clone(),
1431 skip_lint,
1432 no_docs,
1433 &arch,
1434 )?;
1435 }
1436 Ok(())
1437 }
1438 };
1439 std::env::set_current_dir(cur_dir)?;
1440 r
1441}
1442
1443#[allow(clippy::too_many_arguments)]
1445fn build_rust_cwd(
1446 cfg: &WithPath<Config>,
1447 cargo_toml: PathBuf,
1448 no_idl: bool,
1449 idl_out: Option<PathBuf>,
1450 idl_ts_out: Option<PathBuf>,
1451 build_config: &BuildConfig,
1452 stdout: Option<File>,
1453 stderr: Option<File>,
1454 env_vars: Vec<String>,
1455 cargo_args: Vec<String>,
1456 skip_lint: bool,
1457 no_docs: bool,
1458 arch: &ProgramArch,
1459) -> Result<()> {
1460 match cargo_toml.parent() {
1461 None => return Err(anyhow!("Unable to find parent")),
1462 Some(p) => std::env::set_current_dir(p)?,
1463 };
1464 match build_config.verifiable {
1465 false => _build_rust_cwd(
1466 cfg, no_idl, idl_out, idl_ts_out, skip_lint, no_docs, arch, cargo_args,
1467 ),
1468 true => build_cwd_verifiable(
1469 cfg,
1470 cargo_toml,
1471 build_config,
1472 stdout,
1473 stderr,
1474 skip_lint,
1475 env_vars,
1476 cargo_args,
1477 no_docs,
1478 arch,
1479 ),
1480 }
1481}
1482
1483#[allow(clippy::too_many_arguments)]
1486fn build_cwd_verifiable(
1487 cfg: &WithPath<Config>,
1488 cargo_toml: PathBuf,
1489 build_config: &BuildConfig,
1490 stdout: Option<File>,
1491 stderr: Option<File>,
1492 skip_lint: bool,
1493 env_vars: Vec<String>,
1494 cargo_args: Vec<String>,
1495 no_docs: bool,
1496 arch: &ProgramArch,
1497) -> Result<()> {
1498 let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
1500 let target_dir = workspace_dir.join("target");
1501 fs::create_dir_all(target_dir.join("verifiable"))?;
1502 fs::create_dir_all(target_dir.join("idl"))?;
1503 fs::create_dir_all(target_dir.join("types"))?;
1504 if !&cfg.workspace.types.is_empty() {
1505 fs::create_dir_all(workspace_dir.join(&cfg.workspace.types))?;
1506 }
1507
1508 let container_name = "anchor-program";
1509
1510 let result = docker_build(
1512 cfg,
1513 container_name,
1514 cargo_toml,
1515 build_config,
1516 stdout,
1517 stderr,
1518 env_vars,
1519 cargo_args.clone(),
1520 arch,
1521 );
1522
1523 match &result {
1524 Err(e) => {
1525 eprintln!("Error during Docker build: {e:?}");
1526 }
1527 Ok(_) => {
1528 println!("Extracting the IDL");
1530 let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
1531 println!("Writing the IDL file");
1533 let out_file = workspace_dir
1534 .join("target")
1535 .join("idl")
1536 .join(&idl.metadata.name)
1537 .with_extension("json");
1538 write_idl(&idl, OutFile::File(out_file))?;
1539
1540 println!("Writing the .ts file");
1542 let ts_file = workspace_dir
1543 .join("target")
1544 .join("types")
1545 .join(&idl.metadata.name)
1546 .with_extension("ts");
1547 fs::write(&ts_file, idl_ts(&idl)?)?;
1548
1549 if !&cfg.workspace.types.is_empty() {
1551 fs::copy(
1552 ts_file,
1553 workspace_dir
1554 .join(&cfg.workspace.types)
1555 .join(idl.metadata.name)
1556 .with_extension("ts"),
1557 )?;
1558 }
1559
1560 println!("Build success");
1561 }
1562 }
1563
1564 result
1565}
1566
1567#[allow(clippy::too_many_arguments)]
1568fn docker_build(
1569 cfg: &WithPath<Config>,
1570 container_name: &str,
1571 cargo_toml: PathBuf,
1572 build_config: &BuildConfig,
1573 stdout: Option<File>,
1574 stderr: Option<File>,
1575 env_vars: Vec<String>,
1576 cargo_args: Vec<String>,
1577 arch: &ProgramArch,
1578) -> Result<()> {
1579 let binary_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
1580
1581 let workdir = Path::new("/workdir");
1583 let volume_mount = format!(
1584 "{}:{}",
1585 cfg.path().parent().unwrap().canonicalize()?.display(),
1586 workdir.to_str().unwrap(),
1587 );
1588 println!("Using image {:?}", build_config.docker_image);
1589
1590 let target_dir = workdir.join("docker-target");
1592 println!("Run docker image");
1593 let exit = std::process::Command::new("docker")
1594 .args([
1595 "run",
1596 "-it",
1597 "-d",
1598 "--name",
1599 container_name,
1600 "--env",
1601 &format!(
1602 "CARGO_TARGET_DIR={}",
1603 target_dir.as_path().to_str().unwrap()
1604 ),
1605 "-v",
1606 &volume_mount,
1607 "-w",
1608 workdir.to_str().unwrap(),
1609 &build_config.docker_image,
1610 "bash",
1611 ])
1612 .stdout(Stdio::inherit())
1613 .stderr(Stdio::inherit())
1614 .output()
1615 .map_err(|e| anyhow::format_err!("Docker build failed: {}", e.to_string()))?;
1616 if !exit.status.success() {
1617 return Err(anyhow!("Failed to build program"));
1618 }
1619
1620 let result = docker_prep(container_name, build_config).and_then(|_| {
1621 let cfg_parent = cfg.path().parent().unwrap();
1622 docker_build_bpf(
1623 container_name,
1624 cargo_toml.as_path(),
1625 cfg_parent,
1626 target_dir.as_path(),
1627 binary_name,
1628 stdout,
1629 stderr,
1630 env_vars,
1631 cargo_args,
1632 arch,
1633 )
1634 });
1635
1636 docker_cleanup(container_name, target_dir.as_path())?;
1638
1639 result
1641}
1642
1643fn docker_prep(container_name: &str, build_config: &BuildConfig) -> Result<()> {
1644 match build_config.bootstrap {
1647 BootstrapMode::Debian => {
1648 docker_exec(container_name, &["apt", "update"])?;
1650 docker_exec(
1651 container_name,
1652 &["apt", "install", "-y", "curl", "build-essential"],
1653 )?;
1654
1655 docker_exec(
1657 container_name,
1658 &["curl", "https://sh.rustup.rs", "-sfo", "rustup.sh"],
1659 )?;
1660 docker_exec(container_name, &["sh", "rustup.sh", "-y"])?;
1661 docker_exec(container_name, &["rm", "-f", "rustup.sh"])?;
1662 }
1663 BootstrapMode::None => {}
1664 }
1665
1666 if let Some(solana_version) = &build_config.solana_version {
1667 println!("Using solana version: {solana_version}");
1668
1669 docker_exec(
1671 container_name,
1672 &[
1673 "curl",
1674 "-sSfL",
1675 &format!("https://release.anza.xyz/v{solana_version}/install",),
1676 "-o",
1677 "solana_installer.sh",
1678 ],
1679 )?;
1680 docker_exec(container_name, &["sh", "solana_installer.sh"])?;
1681 docker_exec(container_name, &["rm", "-f", "solana_installer.sh"])?;
1682 }
1683 Ok(())
1684}
1685
1686#[allow(clippy::too_many_arguments)]
1687fn docker_build_bpf(
1688 container_name: &str,
1689 cargo_toml: &Path,
1690 cfg_parent: &Path,
1691 target_dir: &Path,
1692 binary_name: String,
1693 stdout: Option<File>,
1694 stderr: Option<File>,
1695 env_vars: Vec<String>,
1696 cargo_args: Vec<String>,
1697 arch: &ProgramArch,
1698) -> Result<()> {
1699 let manifest_path =
1700 pathdiff::diff_paths(cargo_toml.canonicalize()?, cfg_parent.canonicalize()?)
1701 .ok_or_else(|| anyhow!("Unable to diff paths"))?;
1702 println!(
1703 "Building {} manifest: {:?}",
1704 binary_name,
1705 manifest_path.display()
1706 );
1707
1708 let subcommand = arch.build_subcommand();
1709
1710 let exit = std::process::Command::new("docker")
1712 .args([
1713 "exec",
1714 "--env",
1715 "PATH=/root/.local/share/solana/install/active_release/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
1716 ])
1717 .args(env_vars
1718 .iter()
1719 .map(|x| ["--env", x.as_str()])
1720 .collect::<Vec<[&str; 2]>>()
1721 .concat())
1722 .args([
1723 container_name,
1724 "cargo",
1725 subcommand,
1726 "--manifest-path",
1727 &manifest_path.display().to_string(),
1728 ])
1729 .args(cargo_args)
1730 .stdout(match stdout {
1731 None => Stdio::inherit(),
1732 Some(f) => f.into(),
1733 })
1734 .stderr(match stderr {
1735 None => Stdio::inherit(),
1736 Some(f) => f.into(),
1737 })
1738 .output()
1739 .map_err(|e| anyhow::format_err!("Docker build failed: {}", e.to_string()))?;
1740 if !exit.status.success() {
1741 return Err(anyhow!("Failed to build program"));
1742 }
1743
1744 println!("Copying out the build artifacts");
1746 let out_file = cfg_parent
1747 .canonicalize()?
1748 .join(
1749 Path::new("target")
1750 .join("verifiable")
1751 .join(&binary_name)
1752 .with_extension("so"),
1753 )
1754 .display()
1755 .to_string();
1756
1757 let mut bin_path = target_dir.join("deploy");
1760 bin_path.push(format!("{binary_name}.so"));
1761 let bin_artifact = format!(
1762 "{}:{}",
1763 container_name,
1764 bin_path.as_path().to_str().unwrap()
1765 );
1766 let exit = std::process::Command::new("docker")
1767 .args(["cp", &bin_artifact, &out_file])
1768 .stdout(Stdio::inherit())
1769 .stderr(Stdio::inherit())
1770 .output()
1771 .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
1772 if !exit.status.success() {
1773 Err(anyhow!(
1774 "Failed to copy binary out of docker. Is the target directory set correctly?"
1775 ))
1776 } else {
1777 Ok(())
1778 }
1779}
1780
1781fn docker_cleanup(container_name: &str, target_dir: &Path) -> Result<()> {
1782 println!("Cleaning up the docker target directory");
1784 docker_exec(container_name, &["rm", "-rf", target_dir.to_str().unwrap()])?;
1785
1786 println!("Removing the docker container");
1788 let exit = std::process::Command::new("docker")
1789 .args(["rm", "-f", container_name])
1790 .stdout(Stdio::inherit())
1791 .stderr(Stdio::inherit())
1792 .output()
1793 .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
1794 if !exit.status.success() {
1795 println!("Unable to remove the docker container");
1796 std::process::exit(exit.status.code().unwrap_or(1));
1797 }
1798 Ok(())
1799}
1800
1801fn docker_exec(container_name: &str, args: &[&str]) -> Result<()> {
1802 let exit = std::process::Command::new("docker")
1803 .args([&["exec", container_name], args].concat())
1804 .stdout(Stdio::inherit())
1805 .stderr(Stdio::inherit())
1806 .output()
1807 .map_err(|e| anyhow!("Failed to run command \"{:?}\": {:?}", args, e))?;
1808 if !exit.status.success() {
1809 Err(anyhow!("Failed to run command: {:?}", args))
1810 } else {
1811 Ok(())
1812 }
1813}
1814
1815#[allow(clippy::too_many_arguments)]
1816fn _build_rust_cwd(
1817 cfg: &WithPath<Config>,
1818 no_idl: bool,
1819 idl_out: Option<PathBuf>,
1820 idl_ts_out: Option<PathBuf>,
1821 skip_lint: bool,
1822 no_docs: bool,
1823 arch: &ProgramArch,
1824 cargo_args: Vec<String>,
1825) -> Result<()> {
1826 let exit = std::process::Command::new("cargo")
1827 .arg(arch.build_subcommand())
1828 .args(cargo_args.clone())
1829 .stdout(Stdio::inherit())
1830 .stderr(Stdio::inherit())
1831 .output()
1832 .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
1833 if !exit.status.success() {
1834 std::process::exit(exit.status.code().unwrap_or(1));
1835 }
1836
1837 if !no_idl {
1839 let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
1840
1841 let out = match idl_out {
1843 None => PathBuf::from(".")
1844 .join(&idl.metadata.name)
1845 .with_extension("json"),
1846 Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("json")),
1847 };
1848 let ts_out = match idl_ts_out {
1850 None => PathBuf::from(".")
1851 .join(&idl.metadata.name)
1852 .with_extension("ts"),
1853 Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("ts")),
1854 };
1855
1856 write_idl(&idl, OutFile::File(out))?;
1858 fs::write(&ts_out, idl_ts(&idl)?)?;
1860
1861 let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
1863 if !&cfg.workspace.types.is_empty() {
1864 fs::copy(
1865 &ts_out,
1866 cfg_parent
1867 .join(&cfg.workspace.types)
1868 .join(&idl.metadata.name)
1869 .with_extension("ts"),
1870 )?;
1871 }
1872 }
1873
1874 Ok(())
1875}
1876
1877pub fn verify(
1878 program_id: Pubkey,
1879 repo_url: Option<String>,
1880 commit_hash: Option<String>,
1881 current_dir: bool,
1882 program_name: Option<String>,
1883 args: Vec<String>,
1884) -> Result<()> {
1885 let mut command_args = Vec::new();
1886
1887 match (current_dir, repo_url) {
1888 (true, _) => {
1889 let current_path = std::env::current_dir()?
1890 .to_str()
1891 .ok_or_else(|| anyhow!("Invalid current directory path"))?
1892 .to_owned();
1893 command_args.push(current_path);
1894 command_args.push("--current-dir".into());
1895 }
1896 (false, Some(url)) => {
1897 command_args.push(url);
1898 }
1899 (false, None) => {
1900 return Err(anyhow!(
1901 "You must provide either --repo-url or --current-dir"
1902 ));
1903 }
1904 }
1905
1906 if let Some(commit) = commit_hash {
1907 command_args.push("--commit-hash".into());
1908 command_args.push(commit);
1909 }
1910
1911 if let Some(name) = program_name {
1912 command_args.push("--library-name".into());
1913 command_args.push(name);
1914 }
1915
1916 command_args.push("--program-id".into());
1917 command_args.push(program_id.to_string());
1918
1919 command_args.extend(args);
1920
1921 println!("Verifying program {program_id}");
1922 let verify_path = AVM_HOME.join("bin").join("solana-verify");
1923 if !verify_path.exists() {
1924 install_with_avm(env!("CARGO_PKG_VERSION"), true)
1925 .context("installing Anchor with solana-verify")?;
1926 }
1927
1928 let status = std::process::Command::new(verify_path)
1929 .arg("verify-from-repo")
1930 .args(&command_args)
1931 .stdout(std::process::Stdio::inherit())
1932 .stderr(std::process::Stdio::inherit())
1933 .status()
1934 .with_context(|| "Failed to run `solana-verify`")?;
1935
1936 if !status.success() {
1937 return Err(anyhow!("Failed to verify program"));
1938 }
1939
1940 Ok(())
1941}
1942
1943fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> {
1944 let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
1946
1947 for program in cfg.read_all_programs()? {
1948 let cargo_toml = program.path.join("Cargo.toml");
1949 if !cargo_toml.exists() {
1950 return Err(anyhow!(
1951 "Did not find Cargo.toml at the path: {}",
1952 program.path.display()
1953 ));
1954 }
1955
1956 let manifest = Manifest::from_path(&cargo_toml)?;
1957 let pkg_name = manifest.package().name();
1958 let lib_name = manifest.lib_name()?;
1959 if program_name == pkg_name || program_name == lib_name {
1960 std::env::set_current_dir(&program.path)?;
1961 return Ok(());
1962 }
1963 }
1964
1965 Err(anyhow!("{} is not part of the workspace", program_name,))
1966}
1967
1968fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
1969 match subcmd {
1970 IdlCommand::Init {
1971 program_id,
1972 filepath,
1973 priority_fee,
1974 } => idl_init(cfg_override, program_id, filepath, priority_fee),
1975 IdlCommand::Close {
1976 program_id,
1977 idl_address,
1978 print_only,
1979 priority_fee,
1980 } => {
1981 let closed_address = idl_close(
1982 cfg_override,
1983 program_id,
1984 idl_address,
1985 print_only,
1986 priority_fee,
1987 )?;
1988 if !print_only {
1989 println!("Idl account closed: {closed_address}");
1990 }
1991 Ok(())
1992 }
1993 IdlCommand::WriteBuffer {
1994 program_id,
1995 filepath,
1996 priority_fee,
1997 } => {
1998 let idl_buffer = idl_write_buffer(cfg_override, program_id, filepath, priority_fee)?;
1999 println!("Idl buffer created: {idl_buffer}");
2000 Ok(())
2001 }
2002 IdlCommand::SetBuffer {
2003 program_id,
2004 buffer,
2005 print_only,
2006 priority_fee,
2007 } => idl_set_buffer(cfg_override, program_id, buffer, print_only, priority_fee).map(|_| ()),
2008 IdlCommand::Upgrade {
2009 program_id,
2010 filepath,
2011 priority_fee,
2012 } => idl_upgrade(cfg_override, program_id, filepath, priority_fee),
2013 IdlCommand::SetAuthority {
2014 program_id,
2015 address,
2016 new_authority,
2017 print_only,
2018 priority_fee,
2019 } => idl_set_authority(
2020 cfg_override,
2021 program_id,
2022 address,
2023 new_authority,
2024 print_only,
2025 priority_fee,
2026 ),
2027 IdlCommand::EraseAuthority {
2028 program_id,
2029 priority_fee,
2030 } => idl_erase_authority(cfg_override, program_id, priority_fee),
2031 IdlCommand::Authority { program_id } => idl_authority(cfg_override, program_id),
2032 IdlCommand::Build {
2033 program_name,
2034 out,
2035 out_ts,
2036 no_docs,
2037 skip_lint,
2038 cargo_args,
2039 } => idl_build(
2040 cfg_override,
2041 program_name,
2042 out,
2043 out_ts,
2044 no_docs,
2045 skip_lint,
2046 cargo_args,
2047 ),
2048 IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out),
2049 IdlCommand::Convert {
2050 path,
2051 out,
2052 program_id,
2053 } => idl_convert(path, out, program_id),
2054 IdlCommand::Type { path, out } => idl_type(path, out),
2055 }
2056}
2057
2058fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<serde_json::Value> {
2062 let url = match Config::discover(cfg_override)? {
2063 Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
2064 None => {
2065 if let Some(cluster) = cfg_override.cluster.as_ref() {
2069 cluster.url().to_string()
2070 } else {
2071 config::get_solana_cfg_url()?
2072 }
2073 }
2074 };
2075
2076 let client = create_client(url);
2077
2078 let mut account = client.get_account(&idl_addr)?;
2079 if account.executable {
2080 let idl_addr = IdlAccount::address(&idl_addr);
2081 account = client.get_account(&idl_addr)?;
2082 }
2083
2084 let mut d: &[u8] = &account.data[IdlAccount::DISCRIMINATOR.len()..];
2086 let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?;
2087
2088 let compressed_len: usize = idl_account.data_len.try_into().unwrap();
2089 let compressed_bytes = &account.data[44..44 + compressed_len];
2090 let mut z = ZlibDecoder::new(compressed_bytes);
2091 let mut s = Vec::new();
2092 z.read_to_end(&mut s)?;
2093 serde_json::from_slice(&s[..]).map_err(Into::into)
2094}
2095
2096fn get_idl_account(client: &RpcClient, idl_address: &Pubkey) -> Result<IdlAccount> {
2097 let account = client.get_account(idl_address)?;
2098 let mut data: &[u8] = &account.data;
2099 AccountDeserialize::try_deserialize(&mut data).map_err(|e| anyhow!("{:?}", e))
2100}
2101
2102fn idl_init(
2103 cfg_override: &ConfigOverride,
2104 program_id: Pubkey,
2105 idl_filepath: String,
2106 priority_fee: Option<u64>,
2107) -> Result<()> {
2108 with_workspace(cfg_override, |cfg| {
2109 let keypair = cfg.provider.wallet.to_string();
2110
2111 let idl = fs::read(idl_filepath)?;
2112 let idl = convert_idl(&idl)?;
2113
2114 let idl_address = create_idl_account(cfg, &keypair, &program_id, &idl, priority_fee)?;
2115
2116 println!("Idl account created: {idl_address:?}");
2117 Ok(())
2118 })
2119}
2120
2121fn idl_close(
2122 cfg_override: &ConfigOverride,
2123 program_id: Pubkey,
2124 idl_address: Option<Pubkey>,
2125 print_only: bool,
2126 priority_fee: Option<u64>,
2127) -> Result<Pubkey> {
2128 with_workspace(cfg_override, |cfg| {
2129 let idl_address = idl_address.unwrap_or_else(|| IdlAccount::address(&program_id));
2130 idl_close_account(cfg, &program_id, idl_address, print_only, priority_fee)?;
2131
2132 Ok(idl_address)
2133 })
2134}
2135
2136fn idl_write_buffer(
2137 cfg_override: &ConfigOverride,
2138 program_id: Pubkey,
2139 idl_filepath: String,
2140 priority_fee: Option<u64>,
2141) -> Result<Pubkey> {
2142 with_workspace(cfg_override, |cfg| {
2143 let keypair = cfg.provider.wallet.to_string();
2144
2145 let idl = fs::read(idl_filepath)?;
2146 let idl = convert_idl(&idl)?;
2147
2148 let idl_buffer = create_idl_buffer(cfg, &keypair, &program_id, &idl, priority_fee)?;
2149 idl_write(cfg, &program_id, &idl, idl_buffer, priority_fee)?;
2150
2151 Ok(idl_buffer)
2152 })
2153}
2154
2155fn idl_set_buffer(
2156 cfg_override: &ConfigOverride,
2157 program_id: Pubkey,
2158 buffer: Pubkey,
2159 print_only: bool,
2160 priority_fee: Option<u64>,
2161) -> Result<Pubkey> {
2162 with_workspace(cfg_override, |cfg| {
2163 let keypair = get_keypair(&cfg.provider.wallet.to_string())?;
2164 let url = cluster_url(cfg, &cfg.test_validator);
2165 let client = create_client(url);
2166
2167 let idl_address = IdlAccount::address(&program_id);
2168 let idl_authority = if print_only {
2169 get_idl_account(&client, &idl_address)?.authority
2170 } else {
2171 keypair.pubkey()
2172 };
2173 let ix = {
2175 let accounts = vec![
2176 AccountMeta::new(buffer, false),
2177 AccountMeta::new(idl_address, false),
2178 AccountMeta::new(idl_authority, true),
2179 ];
2180 let mut data = anchor_lang::idl::IDL_IX_TAG.to_le_bytes().to_vec();
2181 data.append(&mut IdlInstruction::SetBuffer.try_to_vec()?);
2182 Instruction {
2183 program_id,
2184 accounts,
2185 data,
2186 }
2187 };
2188
2189 if print_only {
2190 print_idl_instruction("SetBuffer", &ix, &idl_address)?;
2191 } else {
2192 let instructions = prepend_compute_unit_ix(vec![ix], &client, priority_fee)?;
2194
2195 let mut latest_hash = client.get_latest_blockhash()?;
2197 for retries in 0..20 {
2198 if !client.is_blockhash_valid(&latest_hash, client.commitment())? {
2199 latest_hash = client.get_latest_blockhash()?;
2200 }
2201 let tx = Transaction::new_signed_with_payer(
2202 &instructions,
2203 Some(&keypair.pubkey()),
2204 &[&keypair],
2205 latest_hash,
2206 );
2207
2208 match client.send_and_confirm_transaction_with_spinner(&tx) {
2209 Ok(_) => break,
2210 Err(e) => {
2211 if retries == 19 {
2212 return Err(anyhow!("Error: {e}. Failed to send transaction."));
2213 }
2214 println!("Error: {e}. Retrying transaction.");
2215 }
2216 }
2217 }
2218 }
2219
2220 Ok(idl_address)
2221 })
2222}
2223
2224fn idl_upgrade(
2225 cfg_override: &ConfigOverride,
2226 program_id: Pubkey,
2227 idl_filepath: String,
2228 priority_fee: Option<u64>,
2229) -> Result<()> {
2230 let buffer_address = idl_write_buffer(cfg_override, program_id, idl_filepath, priority_fee)?;
2231 let idl_address = idl_set_buffer(
2232 cfg_override,
2233 program_id,
2234 buffer_address,
2235 false,
2236 priority_fee,
2237 )?;
2238 idl_close(
2239 cfg_override,
2240 program_id,
2241 Some(buffer_address),
2242 false,
2243 priority_fee,
2244 )?;
2245 println!("Idl account {idl_address} successfully upgraded");
2246 Ok(())
2247}
2248
2249fn idl_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> {
2250 with_workspace(cfg_override, |cfg| {
2251 let url = cluster_url(cfg, &cfg.test_validator);
2252 let client = create_client(url);
2253 let idl_address = {
2254 let account = client.get_account(&program_id)?;
2255 if account.executable {
2256 IdlAccount::address(&program_id)
2257 } else {
2258 program_id
2259 }
2260 };
2261
2262 let idl_account = get_idl_account(&client, &idl_address)?;
2263
2264 println!("{:?}", idl_account.authority);
2265
2266 Ok(())
2267 })
2268}
2269
2270fn idl_set_authority(
2271 cfg_override: &ConfigOverride,
2272 program_id: Pubkey,
2273 address: Option<Pubkey>,
2274 new_authority: Pubkey,
2275 print_only: bool,
2276 priority_fee: Option<u64>,
2277) -> Result<()> {
2278 with_workspace(cfg_override, |cfg| {
2279 let idl_address = match address {
2281 None => IdlAccount::address(&program_id),
2282 Some(addr) => addr,
2283 };
2284 let keypair = get_keypair(&cfg.provider.wallet.to_string())?;
2285 let url = cluster_url(cfg, &cfg.test_validator);
2286 let client = create_client(url);
2287
2288 let idl_authority = if print_only {
2289 get_idl_account(&client, &idl_address)?.authority
2290 } else {
2291 keypair.pubkey()
2292 };
2293
2294 let data =
2296 serialize_idl_ix(anchor_lang::idl::IdlInstruction::SetAuthority { new_authority })?;
2297
2298 let accounts = vec![
2300 AccountMeta::new(idl_address, false),
2301 AccountMeta::new_readonly(idl_authority, true),
2302 ];
2303
2304 let ix = Instruction {
2306 program_id,
2307 accounts,
2308 data,
2309 };
2310
2311 if print_only {
2312 print_idl_instruction("SetAuthority", &ix, &idl_address)?;
2313 } else {
2314 let instructions = prepend_compute_unit_ix(vec![ix], &client, priority_fee)?;
2315
2316 let latest_hash = client.get_latest_blockhash()?;
2318 let tx = Transaction::new_signed_with_payer(
2319 &instructions,
2320 Some(&keypair.pubkey()),
2321 &[&keypair],
2322 latest_hash,
2323 );
2324 client.send_and_confirm_transaction_with_spinner(&tx)?;
2325
2326 println!("Authority update complete.");
2327 }
2328
2329 Ok(())
2330 })
2331}
2332
2333fn idl_erase_authority(
2334 cfg_override: &ConfigOverride,
2335 program_id: Pubkey,
2336 priority_fee: Option<u64>,
2337) -> Result<()> {
2338 println!("Are you sure you want to erase the IDL authority: [y/n]");
2339
2340 let stdin = std::io::stdin();
2341 let mut stdin_lines = stdin.lock().lines();
2342 let input = stdin_lines.next().unwrap().unwrap();
2343 if input != "y" {
2344 println!("Not erasing.");
2345 return Ok(());
2346 }
2347
2348 idl_set_authority(
2349 cfg_override,
2350 program_id,
2351 None,
2352 ERASED_AUTHORITY,
2353 false,
2354 priority_fee,
2355 )?;
2356
2357 Ok(())
2358}
2359
2360fn idl_close_account(
2361 cfg: &Config,
2362 program_id: &Pubkey,
2363 idl_address: Pubkey,
2364 print_only: bool,
2365 priority_fee: Option<u64>,
2366) -> Result<()> {
2367 let keypair = get_keypair(&cfg.provider.wallet.to_string())?;
2368 let url = cluster_url(cfg, &cfg.test_validator);
2369 let client = create_client(url);
2370
2371 let idl_authority = if print_only {
2372 get_idl_account(&client, &idl_address)?.authority
2373 } else {
2374 keypair.pubkey()
2375 };
2376 let accounts = vec![
2378 AccountMeta::new(idl_address, false),
2379 AccountMeta::new_readonly(idl_authority, true),
2380 AccountMeta::new(keypair.pubkey(), false),
2381 ];
2382 let ix = Instruction {
2384 program_id: *program_id,
2385 accounts,
2386 data: { serialize_idl_ix(anchor_lang::idl::IdlInstruction::Close {})? },
2387 };
2388
2389 if print_only {
2390 print_idl_instruction("Close", &ix, &idl_address)?;
2391 } else {
2392 let instructions = prepend_compute_unit_ix(vec![ix], &client, priority_fee)?;
2393
2394 let latest_hash = client.get_latest_blockhash()?;
2396 let tx = Transaction::new_signed_with_payer(
2397 &instructions,
2398 Some(&keypair.pubkey()),
2399 &[&keypair],
2400 latest_hash,
2401 );
2402 client.send_and_confirm_transaction_with_spinner(&tx)?;
2403 }
2404
2405 Ok(())
2406}
2407
2408fn idl_write(
2412 cfg: &Config,
2413 program_id: &Pubkey,
2414 idl: &Idl,
2415 idl_address: Pubkey,
2416 priority_fee: Option<u64>,
2417) -> Result<()> {
2418 let keypair = get_keypair(&cfg.provider.wallet.to_string())?;
2420 let url = cluster_url(cfg, &cfg.test_validator);
2421 let client = create_client(url);
2422
2423 let idl_data = {
2425 let json_bytes = serde_json::to_vec(idl)?;
2426 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
2427 e.write_all(&json_bytes)?;
2428 e.finish()?
2429 };
2430
2431 println!("Idl data length: {:?} bytes", idl_data.len());
2432
2433 const MAX_WRITE_SIZE: usize = 600;
2434 let mut offset = 0;
2435 while offset < idl_data.len() {
2436 println!("Step {offset}/{} ", idl_data.len());
2437 let data = {
2439 let start = offset;
2440 let end = std::cmp::min(offset + MAX_WRITE_SIZE, idl_data.len());
2441 serialize_idl_ix(anchor_lang::idl::IdlInstruction::Write {
2442 data: idl_data[start..end].to_vec(),
2443 })?
2444 };
2445 let accounts = vec![
2447 AccountMeta::new(idl_address, false),
2448 AccountMeta::new_readonly(keypair.pubkey(), true),
2449 ];
2450 let ix = Instruction {
2452 program_id: *program_id,
2453 accounts,
2454 data,
2455 };
2456 let instructions = prepend_compute_unit_ix(vec![ix], &client, priority_fee)?;
2458
2459 let mut latest_hash = client.get_latest_blockhash()?;
2460 for retries in 0..20 {
2461 if !client.is_blockhash_valid(&latest_hash, client.commitment())? {
2462 latest_hash = client.get_latest_blockhash()?;
2463 }
2464 let tx = Transaction::new_signed_with_payer(
2465 &instructions,
2466 Some(&keypair.pubkey()),
2467 &[&keypair],
2468 latest_hash,
2469 );
2470
2471 match client.send_and_confirm_transaction_with_spinner(&tx) {
2472 Ok(_) => break,
2473 Err(e) => {
2474 if retries == 19 {
2475 return Err(anyhow!("Error: {e}. Failed to send transaction."));
2476 }
2477 println!("Error: {e}. Retrying transaction.");
2478 }
2479 }
2480 }
2481
2482 offset += MAX_WRITE_SIZE;
2483 }
2484 Ok(())
2485}
2486
2487fn idl_build(
2488 cfg_override: &ConfigOverride,
2489 program_name: Option<String>,
2490 out: Option<String>,
2491 out_ts: Option<String>,
2492 no_docs: bool,
2493 skip_lint: bool,
2494 cargo_args: Vec<String>,
2495) -> Result<()> {
2496 let cfg = Config::discover(cfg_override)?.expect("Not in workspace");
2497 let current_dir = std::env::current_dir()?;
2498 let program_path = match program_name {
2499 Some(name) => cfg.get_program(&name)?.path,
2500 None => {
2501 let programs = cfg.read_all_programs()?;
2502 if programs.len() == 1 {
2503 programs.into_iter().next().unwrap().path
2504 } else {
2505 programs
2506 .into_iter()
2507 .find(|program| program.path == current_dir)
2508 .ok_or_else(|| anyhow!("Not in a program directory"))?
2509 .path
2510 }
2511 }
2512 };
2513 std::env::set_current_dir(program_path)?;
2514 let idl = generate_idl(&cfg, skip_lint, no_docs, &cargo_args)?;
2515 std::env::set_current_dir(current_dir)?;
2516
2517 let out = match out {
2518 Some(path) => OutFile::File(PathBuf::from(path)),
2519 None => OutFile::Stdout,
2520 };
2521 write_idl(&idl, out)?;
2522
2523 if let Some(path) = out_ts {
2524 fs::write(path, idl_ts(&idl)?)?;
2525 }
2526
2527 Ok(())
2528}
2529
2530fn generate_idl(
2532 cfg: &WithPath<Config>,
2533 skip_lint: bool,
2534 no_docs: bool,
2535 cargo_args: &[String],
2536) -> Result<Idl> {
2537 check_idl_build_feature()?;
2538
2539 anchor_lang_idl::build::IdlBuilder::new()
2540 .resolution(cfg.features.resolution)
2541 .skip_lint(cfg.features.skip_lint || skip_lint)
2542 .no_docs(no_docs)
2543 .cargo_args(cargo_args.into())
2544 .build()
2545}
2546
2547fn idl_fetch(cfg_override: &ConfigOverride, address: Pubkey, out: Option<String>) -> Result<()> {
2548 let idl = fetch_idl(cfg_override, address).map(|idl| serde_json::to_string_pretty(&idl))??;
2549 match out {
2550 Some(out) => fs::write(out, idl)?,
2551 _ => println!("{idl}"),
2552 };
2553
2554 Ok(())
2555}
2556
2557fn idl_convert(path: String, out: Option<String>, program_id: Option<Pubkey>) -> Result<()> {
2558 let idl = fs::read(path)?;
2559
2560 let idl = match program_id {
2562 Some(program_id) => {
2563 let mut idl = serde_json::from_slice::<serde_json::Value>(&idl)?;
2564 idl.as_object_mut()
2565 .ok_or_else(|| anyhow!("IDL must be an object"))?
2566 .insert(
2567 "metadata".into(),
2568 serde_json::json!({ "address": program_id.to_string() }),
2569 );
2570 serde_json::to_vec(&idl)?
2571 }
2572 _ => idl,
2573 };
2574
2575 let idl = convert_idl(&idl)?;
2576 let out = match out {
2577 None => OutFile::Stdout,
2578 Some(out) => OutFile::File(PathBuf::from(out)),
2579 };
2580 write_idl(&idl, out)
2581}
2582
2583fn idl_type(path: String, out: Option<String>) -> Result<()> {
2584 let idl = fs::read(path)?;
2585 let idl = convert_idl(&idl)?;
2586 let types = idl_ts(&idl)?;
2587 match out {
2588 Some(out) => fs::write(out, types)?,
2589 _ => println!("{types}"),
2590 };
2591 Ok(())
2592}
2593
2594fn idl_ts(idl: &Idl) -> Result<String> {
2595 let idl_name = &idl.metadata.name;
2596 let type_name = idl_name.to_pascal_case();
2597 let idl = serde_json::to_string(idl)?;
2598
2599 let camel_idl = Regex::new(r#""\w+":"([\w\d]+)""#)?
2601 .captures_iter(&idl)
2602 .fold(idl.clone(), |acc, cur| {
2603 let name = cur.get(1).unwrap().as_str();
2604
2605 if Pubkey::from_str(name).is_ok() {
2607 return acc;
2608 }
2609
2610 let camel_name = name.to_lower_camel_case();
2611 acc.replace(&format!(r#""{name}""#), &format!(r#""{camel_name}""#))
2612 });
2613
2614 let camel_idl = serde_json::to_string_pretty(&serde_json::from_str::<Idl>(&camel_idl)?)?;
2616
2617 Ok(format!(
2618 r#"/**
2619 * Program IDL in camelCase format in order to be used in JS/TS.
2620 *
2621 * Note that this is only a type helper and is not the actual IDL. The original
2622 * IDL can be found at `target/idl/{idl_name}.json`.
2623 */
2624export type {type_name} = {camel_idl};
2625"#
2626 ))
2627}
2628
2629fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
2630 let idl_json = serde_json::to_string_pretty(idl)?;
2631 match out {
2632 OutFile::Stdout => println!("{idl_json}"),
2633 OutFile::File(out) => fs::write(out, idl_json)?,
2634 };
2635
2636 Ok(())
2637}
2638
2639fn print_idl_instruction(ix_name: &str, ix: &Instruction, idl_address: &Pubkey) -> Result<()> {
2641 use base64::engine::general_purpose::STANDARD;
2642 use base64::Engine;
2643
2644 println!("Print only mode. No execution!");
2645 println!("Instruction: {ix_name}");
2646 println!("IDL address: {idl_address}");
2647 println!("Program: {}", ix.program_id);
2648
2649 let mut serialized_ix = bincode::serialize(ix)?;
2651
2652 let mut remove_extra_vec_bytes = |index: usize| {
2655 serialized_ix.drain((index + 4)..(index + 8));
2656 };
2657
2658 let accounts_index = std::mem::size_of_val(&ix.program_id);
2659 remove_extra_vec_bytes(accounts_index);
2660 let data_index = accounts_index + 4 + std::mem::size_of_val(&*ix.accounts);
2661 remove_extra_vec_bytes(data_index);
2662
2663 println!(
2664 "Base64 encoded instruction: {}",
2665 STANDARD.encode(serialized_ix)
2666 );
2667
2668 Ok(())
2669}
2670
2671fn account(
2672 cfg_override: &ConfigOverride,
2673 account_type: String,
2674 address: Pubkey,
2675 idl_filepath: Option<String>,
2676) -> Result<()> {
2677 let (program_name, account_type_name) = account_type
2678 .split_once('.') .and_then(|(x, y)| y.find('.').map_or_else(|| Some((x, y)), |_| None)) .ok_or_else(|| {
2681 anyhow!(
2682 "Please enter the account struct in the following format: <program_name>.<Account>",
2683 )
2684 })?;
2685
2686 let idl = idl_filepath.map_or_else(
2687 || {
2688 Config::discover(cfg_override)
2689 .expect("Error when detecting workspace.")
2690 .expect("Not in workspace.")
2691 .read_all_programs()
2692 .expect("Workspace must contain atleast one program.")
2693 .into_iter()
2694 .find(|p| p.lib_name == *program_name)
2695 .ok_or_else(|| anyhow!("Program {program_name} not found in workspace."))
2696 .map(|p| p.idl)?
2697 .ok_or_else(|| {
2698 anyhow!(
2699 "IDL not found. Please build the program atleast once to generate the IDL."
2700 )
2701 })
2702 },
2703 |idl_path| {
2704 let idl = fs::read(idl_path)?;
2705 let idl = convert_idl(&idl)?;
2706 if idl.metadata.name != program_name {
2707 return Err(anyhow!("IDL does not match program {program_name}."));
2708 }
2709
2710 Ok(idl)
2711 },
2712 )?;
2713
2714 let cluster = match &cfg_override.cluster {
2715 Some(cluster) => cluster.clone(),
2716 None => Config::discover(cfg_override)?
2717 .map(|cfg| cfg.provider.cluster.clone())
2718 .unwrap_or(Cluster::Localnet),
2719 };
2720
2721 let data = create_client(cluster.url()).get_account_data(&address)?;
2722 let disc_len = idl
2723 .accounts
2724 .iter()
2725 .find(|acc| acc.name == account_type_name)
2726 .map(|acc| acc.discriminator.len())
2727 .ok_or_else(|| anyhow!("Account `{account_type_name}` not found in IDL"))?;
2728 let mut data_view = &data[disc_len..];
2729
2730 let deserialized_json =
2731 deserialize_idl_defined_type_to_json(&idl, account_type_name, &mut data_view)?;
2732
2733 println!(
2734 "{}",
2735 serde_json::to_string_pretty(&deserialized_json).unwrap()
2736 );
2737
2738 Ok(())
2739}
2740
2741fn deserialize_idl_defined_type_to_json(
2743 idl: &Idl,
2744 defined_type_name: &str,
2745 data: &mut &[u8],
2746) -> Result<JsonValue, anyhow::Error> {
2747 let defined_type = &idl
2748 .accounts
2749 .iter()
2750 .find(|acc| acc.name == defined_type_name)
2751 .and_then(|acc| idl.types.iter().find(|ty| ty.name == acc.name))
2752 .or_else(|| idl.types.iter().find(|ty| ty.name == defined_type_name))
2753 .ok_or_else(|| anyhow!("Type `{}` not found in IDL.", defined_type_name))?
2754 .ty;
2755
2756 let mut deserialized_fields = Map::new();
2757
2758 match defined_type {
2759 IdlTypeDefTy::Struct { fields } => {
2760 if let Some(fields) = fields {
2761 match fields {
2762 IdlDefinedFields::Named(fields) => {
2763 for field in fields {
2764 deserialized_fields.insert(
2765 field.name.clone(),
2766 deserialize_idl_type_to_json(&field.ty, data, idl)?,
2767 );
2768 }
2769 }
2770 IdlDefinedFields::Tuple(fields) => {
2771 let mut values = Vec::new();
2772 for field in fields {
2773 values.push(deserialize_idl_type_to_json(field, data, idl)?);
2774 }
2775 deserialized_fields
2776 .insert(defined_type_name.to_owned(), JsonValue::Array(values));
2777 }
2778 }
2779 }
2780 }
2781 IdlTypeDefTy::Enum { variants } => {
2782 let repr = <u8 as AnchorDeserialize>::deserialize(data)?;
2783
2784 let variant = variants
2785 .get(repr as usize)
2786 .ok_or_else(|| anyhow!("Error while deserializing enum variant {repr}"))?;
2787
2788 let mut value = json!({});
2789
2790 if let Some(enum_field) = &variant.fields {
2791 match enum_field {
2792 IdlDefinedFields::Named(fields) => {
2793 let mut values = Map::new();
2794 for field in fields {
2795 values.insert(
2796 field.name.clone(),
2797 deserialize_idl_type_to_json(&field.ty, data, idl)?,
2798 );
2799 }
2800 value = JsonValue::Object(values);
2801 }
2802 IdlDefinedFields::Tuple(fields) => {
2803 let mut values = Vec::new();
2804 for field in fields {
2805 values.push(deserialize_idl_type_to_json(field, data, idl)?);
2806 }
2807 value = JsonValue::Array(values);
2808 }
2809 }
2810 }
2811
2812 deserialized_fields.insert(variant.name.clone(), value);
2813 }
2814 IdlTypeDefTy::Type { alias } => {
2815 return deserialize_idl_type_to_json(alias, data, idl);
2816 }
2817 }
2818
2819 Ok(JsonValue::Object(deserialized_fields))
2820}
2821
2822fn deserialize_idl_type_to_json(
2824 idl_type: &IdlType,
2825 data: &mut &[u8],
2826 parent_idl: &Idl,
2827) -> Result<JsonValue, anyhow::Error> {
2828 if data.is_empty() {
2829 return Err(anyhow::anyhow!("Unable to parse from empty bytes"));
2830 }
2831
2832 Ok(match idl_type {
2833 IdlType::Bool => json!(<bool as AnchorDeserialize>::deserialize(data)?),
2834 IdlType::U8 => {
2835 json!(<u8 as AnchorDeserialize>::deserialize(data)?)
2836 }
2837 IdlType::I8 => {
2838 json!(<i8 as AnchorDeserialize>::deserialize(data)?)
2839 }
2840 IdlType::U16 => {
2841 json!(<u16 as AnchorDeserialize>::deserialize(data)?)
2842 }
2843 IdlType::I16 => {
2844 json!(<i16 as AnchorDeserialize>::deserialize(data)?)
2845 }
2846 IdlType::U32 => {
2847 json!(<u32 as AnchorDeserialize>::deserialize(data)?)
2848 }
2849 IdlType::I32 => {
2850 json!(<i32 as AnchorDeserialize>::deserialize(data)?)
2851 }
2852 IdlType::F32 => json!(<f32 as AnchorDeserialize>::deserialize(data)?),
2853 IdlType::U64 => {
2854 json!(<u64 as AnchorDeserialize>::deserialize(data)?)
2855 }
2856 IdlType::I64 => {
2857 json!(<i64 as AnchorDeserialize>::deserialize(data)?)
2858 }
2859 IdlType::F64 => json!(<f64 as AnchorDeserialize>::deserialize(data)?),
2860 IdlType::U128 => {
2861 json!(<u128 as AnchorDeserialize>::deserialize(data)?.to_string())
2863 }
2864 IdlType::I128 => {
2865 json!(<i128 as AnchorDeserialize>::deserialize(data)?.to_string())
2867 }
2868 IdlType::U256 => todo!("Upon completion of u256 IDL standard"),
2869 IdlType::I256 => todo!("Upon completion of i256 IDL standard"),
2870 IdlType::Bytes => JsonValue::Array(
2871 <Vec<u8> as AnchorDeserialize>::deserialize(data)?
2872 .iter()
2873 .map(|i| json!(*i))
2874 .collect(),
2875 ),
2876 IdlType::String => json!(<String as AnchorDeserialize>::deserialize(data)?),
2877 IdlType::Pubkey => {
2878 json!(<Pubkey as AnchorDeserialize>::deserialize(data)?.to_string())
2879 }
2880 IdlType::Array(ty, size) => match size {
2881 IdlArrayLen::Value(size) => {
2882 let mut array_data: Vec<JsonValue> = Vec::with_capacity(*size);
2883
2884 for _ in 0..*size {
2885 array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
2886 }
2887
2888 JsonValue::Array(array_data)
2889 }
2890 IdlArrayLen::Generic(_) => unimplemented!("Generic array length is not yet supported"),
2892 },
2893 IdlType::Option(ty) => {
2894 let is_present = <u8 as AnchorDeserialize>::deserialize(data)?;
2895
2896 if is_present == 0 {
2897 JsonValue::String("None".to_string())
2898 } else {
2899 deserialize_idl_type_to_json(ty, data, parent_idl)?
2900 }
2901 }
2902 IdlType::Vec(ty) => {
2903 let size: usize = <u32 as AnchorDeserialize>::deserialize(data)?
2904 .try_into()
2905 .unwrap();
2906
2907 let mut vec_data: Vec<JsonValue> = Vec::with_capacity(size);
2908
2909 for _ in 0..size {
2910 vec_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
2911 }
2912
2913 JsonValue::Array(vec_data)
2914 }
2915 IdlType::Defined {
2916 name,
2917 generics: _generics,
2918 } => {
2919 deserialize_idl_defined_type_to_json(parent_idl, name, data)?
2921 }
2922 IdlType::Generic(generic) => json!(generic),
2923 _ => unimplemented!("{idl_type:?}"),
2924 })
2925}
2926
2927enum OutFile {
2928 Stdout,
2929 File(PathBuf),
2930}
2931
2932#[allow(clippy::too_many_arguments)]
2934fn test(
2935 cfg_override: &ConfigOverride,
2936 program_name: Option<String>,
2937 skip_deploy: bool,
2938 skip_local_validator: bool,
2939 skip_build: bool,
2940 skip_lint: bool,
2941 no_idl: bool,
2942 detach: bool,
2943 tests_to_run: Vec<String>,
2944 extra_args: Vec<String>,
2945 env_vars: Vec<String>,
2946 cargo_args: Vec<String>,
2947 arch: ProgramArch,
2948) -> Result<()> {
2949 let test_paths = tests_to_run
2950 .iter()
2951 .map(|path| {
2952 PathBuf::from(path)
2953 .canonicalize()
2954 .map_err(|_| anyhow!("Wrong path {}", path))
2955 })
2956 .collect::<Result<Vec<_>, _>>()?;
2957
2958 with_workspace(cfg_override, |cfg| {
2959 if !skip_build {
2961 build(
2962 cfg_override,
2963 no_idl,
2964 None,
2965 None,
2966 false,
2967 skip_lint,
2968 program_name.clone(),
2969 None,
2970 None,
2971 BootstrapMode::None,
2972 None,
2973 None,
2974 env_vars,
2975 cargo_args,
2976 false,
2977 arch,
2978 )?;
2979 }
2980
2981 let root = cfg.path().parent().unwrap().to_owned();
2982 cfg.add_test_config(root, test_paths)?;
2983
2984 let is_localnet = cfg.provider.cluster == Cluster::Localnet;
2991 if (!is_localnet || skip_local_validator) && !skip_deploy {
2992 deploy(cfg_override, None, None, false, true, vec![])?;
2993 }
2994 let mut is_first_suite = true;
2995 if let Some(test_script) = cfg.scripts.get_mut("test") {
2996 is_first_suite = false;
2997
2998 match program_name {
2999 Some(program_name) => {
3000 if let Some((from, to)) = Regex::new("\\s(tests/\\S+\\.(js|ts))")
3001 .unwrap()
3002 .captures_iter(&test_script.clone())
3003 .last()
3004 .and_then(|c| c.get(1).and_then(|mtch| c.get(2).map(|ext| (mtch, ext))))
3005 .map(|(mtch, ext)| {
3006 (
3007 mtch.as_str(),
3008 format!("tests/{program_name}.{}", ext.as_str()),
3009 )
3010 })
3011 {
3012 println!("\nRunning tests of program `{program_name}`!");
3013 *test_script = test_script.replace(from, &to);
3015 }
3016 }
3017 _ => println!(
3018 "\nFound a 'test' script in the Anchor.toml. Running it as a test suite!"
3019 ),
3020 }
3021
3022 run_test_suite(
3023 cfg,
3024 cfg.path(),
3025 is_localnet,
3026 skip_local_validator,
3027 skip_deploy,
3028 detach,
3029 &cfg.test_validator,
3030 &cfg.scripts,
3031 &extra_args,
3032 )?;
3033 }
3034 if let Some(test_config) = &cfg.test_config {
3035 for test_suite in test_config.iter() {
3036 if !is_first_suite {
3037 std::thread::sleep(std::time::Duration::from_millis(
3038 test_suite
3039 .1
3040 .test
3041 .as_ref()
3042 .map(|val| val.shutdown_wait)
3043 .unwrap_or(SHUTDOWN_WAIT) as u64,
3044 ));
3045 } else {
3046 is_first_suite = false;
3047 }
3048
3049 run_test_suite(
3050 cfg,
3051 test_suite.0,
3052 is_localnet,
3053 skip_local_validator,
3054 skip_deploy,
3055 detach,
3056 &test_suite.1.test,
3057 &test_suite.1.scripts,
3058 &extra_args,
3059 )?;
3060 }
3061 }
3062 Ok(())
3063 })
3064}
3065
3066#[allow(clippy::too_many_arguments)]
3067fn run_test_suite(
3068 cfg: &WithPath<Config>,
3069 test_suite_path: impl AsRef<Path>,
3070 is_localnet: bool,
3071 skip_local_validator: bool,
3072 skip_deploy: bool,
3073 detach: bool,
3074 test_validator: &Option<TestValidator>,
3075 scripts: &ScriptsConfig,
3076 extra_args: &[String],
3077) -> Result<()> {
3078 println!("\nRunning test suite: {:#?}\n", test_suite_path.as_ref());
3079 let mut validator_handle = None;
3081 if is_localnet && (!skip_local_validator) {
3082 let flags = match skip_deploy {
3083 true => None,
3084 false => Some(validator_flags(cfg, test_validator)?),
3085 };
3086 validator_handle = Some(start_test_validator(cfg, test_validator, flags, true)?);
3087 }
3088
3089 let url = cluster_url(cfg, test_validator);
3090
3091 let node_options = format!(
3092 "{} {}",
3093 match std::env::var_os("NODE_OPTIONS") {
3094 Some(value) => value
3095 .into_string()
3096 .map_err(std::env::VarError::NotUnicode)?,
3097 None => "".to_owned(),
3098 },
3099 get_node_dns_option()?,
3100 );
3101
3102 let log_streams = stream_logs(cfg, &url);
3104
3105 let test_result = {
3107 let cmd = scripts
3108 .get("test")
3109 .expect("Not able to find script for `test`")
3110 .clone();
3111 let script_args = format!("{cmd} {}", extra_args.join(" "));
3112 std::process::Command::new("bash")
3113 .arg("-c")
3114 .arg(script_args)
3115 .env("ANCHOR_PROVIDER_URL", url)
3116 .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
3117 .env("NODE_OPTIONS", node_options)
3118 .stdout(Stdio::inherit())
3119 .stderr(Stdio::inherit())
3120 .output()
3121 .map_err(anyhow::Error::from)
3122 .context(cmd)
3123 };
3124
3125 if test_result.is_ok() && detach {
3127 println!("Local validator still running. Press Ctrl + C quit.");
3128 std::io::stdin().lock().lines().next().unwrap().unwrap();
3129 }
3130
3131 if let Some(mut child) = validator_handle {
3133 if let Err(err) = child.kill() {
3134 println!("Failed to kill subprocess {}: {}", child.id(), err);
3135 }
3136 }
3137 for mut child in log_streams? {
3138 if let Err(err) = child.kill() {
3139 println!("Failed to kill subprocess {}: {}", child.id(), err);
3140 }
3141 }
3142
3143 match test_result {
3145 Ok(exit) => {
3146 if !exit.status.success() {
3147 std::process::exit(exit.status.code().unwrap());
3148 }
3149 }
3150 Err(err) => {
3151 println!("Failed to run test: {err:#}");
3152 return Err(err);
3153 }
3154 }
3155
3156 Ok(())
3157}
3158
3159fn validator_flags(
3163 cfg: &WithPath<Config>,
3164 test_validator: &Option<TestValidator>,
3165) -> Result<Vec<String>> {
3166 let programs = cfg.programs.get(&Cluster::Localnet);
3167
3168 let test_upgradeable_program = test_validator
3169 .as_ref()
3170 .map(|test_validator| test_validator.upgradeable)
3171 .unwrap_or(false);
3172
3173 let mut flags = Vec::new();
3174 for mut program in cfg.read_all_programs()? {
3175 let verifiable = false;
3176 let binary_path = program.binary_path(verifiable).display().to_string();
3177
3178 let address = programs
3181 .and_then(|m| m.get(&program.lib_name))
3182 .map(|deployment| Ok(deployment.address.to_string()))
3183 .unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
3184
3185 if test_upgradeable_program {
3186 flags.push("--upgradeable-program".to_string());
3187 flags.push(address.clone());
3188 flags.push(binary_path);
3189 flags.push(cfg.wallet_kp()?.pubkey().to_string());
3190 } else {
3191 flags.push("--bpf-program".to_string());
3192 flags.push(address.clone());
3193 flags.push(binary_path);
3194 }
3195
3196 if let Some(idl) = program.idl.as_mut() {
3197 idl.address = address;
3199
3200 let idl_out = Path::new("target")
3202 .join("idl")
3203 .join(&idl.metadata.name)
3204 .with_extension("json");
3205 write_idl(idl, OutFile::File(idl_out))?;
3206 }
3207 }
3208
3209 if let Some(test) = test_validator.as_ref() {
3210 if let Some(genesis) = &test.genesis {
3211 for entry in genesis {
3212 let program_path = Path::new(&entry.program);
3213 if !program_path.exists() {
3214 return Err(anyhow!(
3215 "Program in genesis configuration does not exist at path: {}",
3216 program_path.display()
3217 ));
3218 }
3219 if entry.upgradeable.unwrap_or(false) {
3220 flags.push("--upgradeable-program".to_string());
3221 flags.push(entry.address.clone());
3222 flags.push(entry.program.clone());
3223 flags.push(cfg.wallet_kp()?.pubkey().to_string());
3224 } else {
3225 flags.push("--bpf-program".to_string());
3226 flags.push(entry.address.clone());
3227 flags.push(entry.program.clone());
3228 }
3229 }
3230 }
3231 if let Some(validator) = &test.validator {
3232 let entries = serde_json::to_value(validator)?;
3233 for (key, value) in entries.as_object().unwrap() {
3234 if key == "ledger" {
3235 continue;
3238 };
3239 if key == "account" {
3240 for entry in value.as_array().unwrap() {
3241 flags.push("--account".to_string());
3243 flags.push(entry["address"].as_str().unwrap().to_string());
3244 flags.push(entry["filename"].as_str().unwrap().to_string());
3245 }
3246 } else if key == "account_dir" {
3247 for entry in value.as_array().unwrap() {
3248 flags.push("--account-dir".to_string());
3249 flags.push(entry["directory"].as_str().unwrap().to_string());
3250 }
3251 } else if key == "clone" {
3252 let client = if let Some(url) = entries["url"].as_str() {
3254 create_client(url)
3255 } else {
3256 return Err(anyhow!(
3257 "Validator url for Solana's JSON RPC should be provided in order to clone accounts from it"
3258 ));
3259 };
3260
3261 let pubkeys = value
3262 .as_array()
3263 .unwrap()
3264 .iter()
3265 .map(|entry| {
3266 let address = entry["address"].as_str().unwrap();
3267 Pubkey::from_str(address)
3268 .map_err(|_| anyhow!("Invalid pubkey {}", address))
3269 })
3270 .collect::<Result<HashSet<Pubkey>>>()?
3271 .into_iter()
3272 .collect::<Vec<_>>();
3273 let accounts = client.get_multiple_accounts(&pubkeys)?;
3274
3275 for (pubkey, account) in pubkeys.into_iter().zip(accounts) {
3276 match account {
3277 Some(account) => {
3278 if account.owner == bpf_loader_upgradeable::id()
3281 && matches!(
3283 account.deserialize_data::<UpgradeableLoaderState>()?,
3284 UpgradeableLoaderState::Program { .. }
3285 )
3286 {
3287 flags.push("--clone-upgradeable-program".to_string());
3288 flags.push(pubkey.to_string());
3289 } else {
3290 flags.push("--clone".to_string());
3291 flags.push(pubkey.to_string());
3292 }
3293 }
3294 _ => return Err(anyhow!("Account {} not found", pubkey)),
3295 }
3296 }
3297 } else if key == "deactivate_feature" {
3298 let pubkeys_result: Result<Vec<Pubkey>, _> = value
3300 .as_array()
3301 .unwrap()
3302 .iter()
3303 .map(|entry| {
3304 let feature_flag = entry.as_str().unwrap();
3305 Pubkey::from_str(feature_flag).map_err(|_| {
3306 anyhow!("Invalid pubkey (feature flag) {}", feature_flag)
3307 })
3308 })
3309 .collect();
3310 let features = pubkeys_result?;
3311 for feature in features {
3312 flags.push("--deactivate-feature".to_string());
3313 flags.push(feature.to_string());
3314 }
3315 } else {
3316 flags.push(format!("--{}", key.replace('_', "-")));
3318 if let serde_json::Value::String(v) = value {
3319 flags.push(v.to_string());
3320 } else {
3321 flags.push(value.to_string());
3322 }
3323 }
3324 }
3325 }
3326 }
3327
3328 Ok(flags)
3329}
3330
3331fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<std::process::Child>> {
3332 let program_logs_dir = Path::new(".anchor").join("program-logs");
3333 if program_logs_dir.exists() {
3334 fs::remove_dir_all(&program_logs_dir)?;
3335 }
3336 fs::create_dir_all(&program_logs_dir)?;
3337
3338 let mut handles = vec![];
3339 for program in config.read_all_programs()? {
3340 let idl_path = Path::new("target")
3341 .join("idl")
3342 .join(&program.lib_name)
3343 .with_extension("json");
3344 let idl = fs::read(idl_path)?;
3345 let idl = convert_idl(&idl)?;
3346
3347 let log_file = File::create(
3348 program_logs_dir.join(format!("{}.{}.log", idl.address, program.lib_name)),
3349 )?;
3350 let stdio = std::process::Stdio::from(log_file);
3351 let child = std::process::Command::new("solana")
3352 .arg("logs")
3353 .arg(idl.address)
3354 .arg("--url")
3355 .arg(rpc_url)
3356 .stdout(stdio)
3357 .spawn()?;
3358 handles.push(child);
3359 }
3360 if let Some(test) = config.test_validator.as_ref() {
3361 if let Some(genesis) = &test.genesis {
3362 for entry in genesis {
3363 let log_file =
3364 File::create(program_logs_dir.join(&entry.address).with_extension("log"))?;
3365 let stdio = std::process::Stdio::from(log_file);
3366 let child = std::process::Command::new("solana")
3367 .arg("logs")
3368 .arg(entry.address.clone())
3369 .arg("--url")
3370 .arg(rpc_url)
3371 .stdout(stdio)
3372 .spawn()?;
3373 handles.push(child);
3374 }
3375 }
3376 }
3377
3378 Ok(handles)
3379}
3380
3381fn start_test_validator(
3382 cfg: &Config,
3383 test_validator: &Option<TestValidator>,
3384 flags: Option<Vec<String>>,
3385 test_log_stdout: bool,
3386) -> Result<Child> {
3387 let (test_ledger_directory, test_ledger_log_filename) =
3388 test_validator_file_paths(test_validator)?;
3389
3390 let (test_validator_stdout, test_validator_stderr) = match test_log_stdout {
3392 true => {
3393 let test_validator_stdout_file = File::create(&test_ledger_log_filename)?;
3394 let test_validator_sterr_file = test_validator_stdout_file.try_clone()?;
3395 (
3396 Stdio::from(test_validator_stdout_file),
3397 Stdio::from(test_validator_sterr_file),
3398 )
3399 }
3400 false => (Stdio::inherit(), Stdio::inherit()),
3401 };
3402
3403 let rpc_url = test_validator_rpc_url(test_validator);
3404
3405 let rpc_port = cfg
3406 .test_validator
3407 .as_ref()
3408 .and_then(|test| test.validator.as_ref().map(|v| v.rpc_port))
3409 .unwrap_or(DEFAULT_RPC_PORT);
3410 if !portpicker::is_free(rpc_port) {
3411 return Err(anyhow!(
3412 "Your configured rpc port: {rpc_port} is already in use"
3413 ));
3414 }
3415 let faucet_port = cfg
3416 .test_validator
3417 .as_ref()
3418 .and_then(|test| test.validator.as_ref().and_then(|v| v.faucet_port))
3419 .unwrap_or(solana_faucet::faucet::FAUCET_PORT);
3420 if !portpicker::is_free(faucet_port) {
3421 return Err(anyhow!(
3422 "Your configured faucet port: {faucet_port} is already in use"
3423 ));
3424 }
3425
3426 let mut validator_handle = std::process::Command::new("solana-test-validator")
3427 .arg("--ledger")
3428 .arg(test_ledger_directory)
3429 .arg("--mint")
3430 .arg(cfg.wallet_kp()?.pubkey().to_string())
3431 .args(flags.unwrap_or_default())
3432 .stdout(test_validator_stdout)
3433 .stderr(test_validator_stderr)
3434 .spawn()
3435 .map_err(|e| anyhow!("Failed to spawn `solana-test-validator`: {e}"))?;
3436
3437 let client = create_client(rpc_url);
3439 let mut count = 0;
3440 let ms_wait = test_validator
3441 .as_ref()
3442 .map(|test| test.startup_wait)
3443 .unwrap_or(STARTUP_WAIT);
3444 while count < ms_wait {
3445 let r = client.get_latest_blockhash();
3446 if r.is_ok() {
3447 break;
3448 }
3449 std::thread::sleep(std::time::Duration::from_millis(100));
3450 count += 100;
3451 }
3452 if count >= ms_wait {
3453 eprintln!(
3454 "Unable to get latest blockhash. Test validator does not look started. \
3455 Check {test_ledger_log_filename:?} for errors. Consider increasing [test.startup_wait] in Anchor.toml."
3456 );
3457 validator_handle.kill()?;
3458 std::process::exit(1);
3459 }
3460 Ok(validator_handle)
3461}
3462
3463fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
3466 match test_validator {
3467 Some(TestValidator {
3468 validator: Some(validator),
3469 ..
3470 }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
3471 _ => "http://127.0.0.1:8899".to_string(),
3472 }
3473}
3474
3475fn test_validator_file_paths(test_validator: &Option<TestValidator>) -> Result<(PathBuf, PathBuf)> {
3478 let ledger_path = match test_validator {
3479 Some(TestValidator {
3480 validator: Some(validator),
3481 ..
3482 }) => PathBuf::from(&validator.ledger),
3483 _ => get_default_ledger_path(),
3484 };
3485
3486 if !ledger_path.is_relative() {
3487 eprintln!("Ledger directory {ledger_path:?} must be relative");
3490 std::process::exit(1);
3491 }
3492 if ledger_path.exists() {
3493 fs::remove_dir_all(&ledger_path)?;
3494 }
3495
3496 fs::create_dir_all(&ledger_path)?;
3497
3498 let log_path = ledger_path.join("test-ledger-log.txt");
3499 Ok((ledger_path, log_path))
3500}
3501
3502fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String {
3503 let is_localnet = cfg.provider.cluster == Cluster::Localnet;
3504 match is_localnet {
3505 true => test_validator_rpc_url(test_validator),
3508 false => cfg.provider.cluster.url().to_string(),
3509 }
3510}
3511
3512fn clean(cfg_override: &ConfigOverride) -> Result<()> {
3513 let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
3514 let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
3515 let dot_anchor_dir = cfg_parent.join(".anchor");
3516 let target_dir = cfg_parent.join("target");
3517 let deploy_dir = target_dir.join("deploy");
3518
3519 if dot_anchor_dir.exists() {
3520 fs::remove_dir_all(&dot_anchor_dir)
3521 .map_err(|e| anyhow!("Could not remove directory {:?}: {}", dot_anchor_dir, e))?;
3522 }
3523
3524 if target_dir.exists() {
3525 for entry in fs::read_dir(target_dir)? {
3526 let path = entry?.path();
3527 if path.is_dir() && path != deploy_dir {
3528 fs::remove_dir_all(&path)
3529 .map_err(|e| anyhow!("Could not remove directory {}: {}", path.display(), e))?;
3530 } else if path.is_file() {
3531 fs::remove_file(&path)
3532 .map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
3533 }
3534 }
3535 } else {
3536 println!("skipping target directory: not found")
3537 }
3538
3539 if deploy_dir.exists() {
3540 for file in fs::read_dir(deploy_dir)? {
3541 let path = file?.path();
3542 if path.extension() != Some(&OsString::from("json")) {
3543 fs::remove_file(&path)
3544 .map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
3545 }
3546 }
3547 } else {
3548 println!("skipping deploy directory: not found")
3549 }
3550
3551 Ok(())
3552}
3553
3554fn deploy(
3555 cfg_override: &ConfigOverride,
3556 program_name: Option<String>,
3557 program_keypair: Option<String>,
3558 verifiable: bool,
3559 no_idl: bool,
3560 solana_args: Vec<String>,
3561) -> Result<()> {
3562 with_workspace(cfg_override, |cfg| {
3564 let url = cluster_url(cfg, &cfg.test_validator);
3565 let keypair = cfg.provider.wallet.to_string();
3566
3567 let client = create_client(&url);
3569 let solana_args = add_recommended_deployment_solana_args(&client, solana_args)?;
3570
3571 println!("Deploying cluster: {url}");
3573 println!("Upgrade authority: {keypair}");
3574
3575 for mut program in cfg.get_programs(program_name)? {
3576 let binary_path = program.binary_path(verifiable).display().to_string();
3577
3578 println!("Deploying program {:?}...", program.lib_name);
3579 println!("Program path: {binary_path}...");
3580
3581 let (program_keypair_filepath, program_id) = match &program_keypair {
3582 Some(path) => (path.clone(), get_keypair(path)?.pubkey()),
3583 None => (
3584 program.keypair_file()?.path().display().to_string(),
3585 program.pubkey()?,
3586 ),
3587 };
3588
3589 let exit = std::process::Command::new("solana")
3591 .arg("program")
3592 .arg("deploy")
3593 .arg("--url")
3594 .arg(&url)
3595 .arg("--keypair")
3596 .arg(&keypair)
3597 .arg("--program-id")
3598 .arg(strip_workspace_prefix(program_keypair_filepath))
3599 .arg(strip_workspace_prefix(binary_path))
3600 .args(&solana_args)
3601 .stdout(Stdio::inherit())
3602 .stderr(Stdio::inherit())
3603 .output()
3604 .expect("Must deploy");
3605
3606 if !exit.status.success() {
3608 println!("There was a problem deploying: {exit:?}.");
3609 std::process::exit(exit.status.code().unwrap_or(1));
3610 }
3611
3612 let idl_filepath = Path::new("target")
3614 .join("idl")
3615 .join(&program.lib_name)
3616 .with_extension("json");
3617
3618 if let Some(idl) = program.idl.as_mut() {
3619 idl.address = program_id.to_string();
3621
3622 write_idl(idl, OutFile::File(idl_filepath.clone()))?;
3624
3625 if !no_idl {
3627 let client = create_client(&url);
3630 let max_retries = 5;
3631 let retry_delay = std::time::Duration::from_millis(500);
3632 let cache_delay = std::time::Duration::from_secs(2);
3633
3634 println!("Waiting for program {} to be confirmed...", program_id);
3635
3636 for attempt in 0..max_retries {
3637 if let Ok(account) = client.get_account(&program_id) {
3638 if account.executable {
3639 println!("Program confirmed on-chain");
3640 std::thread::sleep(cache_delay);
3641 break;
3642 }
3643 }
3644
3645 if attempt == max_retries - 1 {
3646 return Err(anyhow!(
3647 "Timeout waiting for program {} to be confirmed",
3648 program_id
3649 ));
3650 }
3651
3652 std::thread::sleep(retry_delay);
3653 }
3654
3655 let idl_address = IdlAccount::address(&program_id);
3657 let idl_account_exists = client.get_account(&idl_address).is_ok();
3658
3659 if idl_account_exists {
3660 idl_upgrade(
3662 cfg_override,
3663 program_id,
3664 idl_filepath.display().to_string(),
3665 None,
3666 )?;
3667 } else {
3668 idl_init(
3670 cfg_override,
3671 program_id,
3672 idl_filepath.display().to_string(),
3673 None,
3674 )?;
3675 }
3676 }
3677 }
3678 }
3679
3680 println!("Deploy success");
3681
3682 Ok(())
3683 })
3684}
3685
3686fn upgrade(
3687 cfg_override: &ConfigOverride,
3688 program_id: Pubkey,
3689 program_filepath: String,
3690 max_retries: u32,
3691 solana_args: Vec<String>,
3692) -> Result<()> {
3693 let path: PathBuf = program_filepath.parse().unwrap();
3694 let program_filepath = path.canonicalize()?.display().to_string();
3695
3696 with_workspace(cfg_override, |cfg| {
3697 let url = cluster_url(cfg, &cfg.test_validator);
3698 let client = create_client(&url);
3699 let solana_args = add_recommended_deployment_solana_args(&client, solana_args)?;
3700
3701 for retry in 0..(1 + max_retries) {
3702 let exit = std::process::Command::new("solana")
3703 .arg("program")
3704 .arg("deploy")
3705 .arg("--url")
3706 .arg(url.clone())
3707 .arg("--keypair")
3708 .arg(cfg.provider.wallet.to_string())
3709 .arg("--program-id")
3710 .arg(program_id.to_string())
3711 .arg(strip_workspace_prefix(program_filepath.clone()))
3712 .args(&solana_args)
3713 .stdout(Stdio::inherit())
3714 .stderr(Stdio::inherit())
3715 .output()
3716 .expect("Must deploy");
3717 if exit.status.success() {
3718 break;
3719 }
3720
3721 println!("There was a problem deploying: {exit:?}.");
3722 if retry < max_retries {
3723 println!("Retrying {} more time(s)...", max_retries - retry);
3724 } else {
3725 std::process::exit(exit.status.code().unwrap_or(1));
3726 }
3727 }
3728 Ok(())
3729 })
3730}
3731
3732fn create_idl_account(
3733 cfg: &Config,
3734 keypair_path: &str,
3735 program_id: &Pubkey,
3736 idl: &Idl,
3737 priority_fee: Option<u64>,
3738) -> Result<Pubkey> {
3739 let idl_address = IdlAccount::address(program_id);
3741 let keypair = get_keypair(keypair_path)?;
3742 let url = cluster_url(cfg, &cfg.test_validator);
3743 let client = create_client(url);
3744 let idl_data = serialize_idl(idl)?;
3745
3746 {
3748 let pda_max_growth = 60_000;
3749 let idl_header_size = 44;
3750 let idl_data_len = idl_data.len() as u64;
3751 if idl_data_len > pda_max_growth {
3754 return Err(anyhow!(
3755 "Your IDL is over 60kb and this isn't supported right now"
3756 ));
3757 }
3758 let data_len = (idl_data_len * 2).min(pda_max_growth - idl_header_size);
3760
3761 let num_additional_instructions = data_len / 10000;
3762 let mut instructions = Vec::new();
3763 let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Create { data_len })?;
3764 let program_signer = Pubkey::find_program_address(&[], program_id).0;
3765 let accounts = vec![
3766 AccountMeta::new_readonly(keypair.pubkey(), true),
3767 AccountMeta::new(idl_address, false),
3768 AccountMeta::new_readonly(program_signer, false),
3769 AccountMeta::new_readonly(solana_sdk::system_program::ID, false),
3770 AccountMeta::new_readonly(*program_id, false),
3771 ];
3772 instructions.push(Instruction {
3773 program_id: *program_id,
3774 accounts,
3775 data,
3776 });
3777
3778 for _ in 0..num_additional_instructions {
3779 let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Resize { data_len })?;
3780 instructions.push(Instruction {
3781 program_id: *program_id,
3782 accounts: vec![
3783 AccountMeta::new(idl_address, false),
3784 AccountMeta::new_readonly(keypair.pubkey(), true),
3785 AccountMeta::new_readonly(solana_sdk::system_program::ID, false),
3786 ],
3787 data,
3788 });
3789 }
3790 instructions = prepend_compute_unit_ix(instructions, &client, priority_fee)?;
3791
3792 let mut latest_hash = client.get_latest_blockhash()?;
3793 for retries in 0..20 {
3794 if !client.is_blockhash_valid(&latest_hash, client.commitment())? {
3795 latest_hash = client.get_latest_blockhash()?;
3796 }
3797
3798 let tx = Transaction::new_signed_with_payer(
3799 &instructions,
3800 Some(&keypair.pubkey()),
3801 &[&keypair],
3802 latest_hash,
3803 );
3804
3805 match client.send_and_confirm_transaction_with_spinner(&tx) {
3806 Ok(_) => break,
3807 Err(err) => {
3808 if retries == 19 {
3809 return Err(anyhow!("Error creating IDL account: {}", err));
3810 }
3811 println!("Error creating IDL account: {err}. Retrying...");
3812 }
3813 }
3814 }
3815 }
3816
3817 idl_write(
3819 cfg,
3820 program_id,
3821 idl,
3822 IdlAccount::address(program_id),
3823 priority_fee,
3824 )?;
3825
3826 Ok(idl_address)
3827}
3828
3829fn create_idl_buffer(
3830 cfg: &Config,
3831 keypair_path: &str,
3832 program_id: &Pubkey,
3833 idl: &Idl,
3834 priority_fee: Option<u64>,
3835) -> Result<Pubkey> {
3836 let keypair = get_keypair(keypair_path)?;
3837 let url = cluster_url(cfg, &cfg.test_validator);
3838 let client = create_client(url);
3839
3840 let buffer = Keypair::new();
3841
3842 let create_account_ix = {
3844 let space = IdlAccount::DISCRIMINATOR.len() + 32 + 4 + serialize_idl(idl)?.len();
3845 let lamports = client.get_minimum_balance_for_rent_exemption(space)?;
3846 solana_sdk::system_instruction::create_account(
3847 &keypair.pubkey(),
3848 &buffer.pubkey(),
3849 lamports,
3850 space as u64,
3851 program_id,
3852 )
3853 };
3854
3855 let create_buffer_ix = {
3857 let accounts = vec![
3858 AccountMeta::new(buffer.pubkey(), false),
3859 AccountMeta::new_readonly(keypair.pubkey(), true),
3860 ];
3861 let mut data = anchor_lang::idl::IDL_IX_TAG.to_le_bytes().to_vec();
3862 data.append(&mut IdlInstruction::CreateBuffer.try_to_vec()?);
3863 Instruction {
3864 program_id: *program_id,
3865 accounts,
3866 data,
3867 }
3868 };
3869
3870 let instructions = prepend_compute_unit_ix(
3871 vec![create_account_ix, create_buffer_ix],
3872 &client,
3873 priority_fee,
3874 )?;
3875
3876 let mut latest_hash = client.get_latest_blockhash()?;
3877 for retries in 0..20 {
3878 if !client.is_blockhash_valid(&latest_hash, client.commitment())? {
3879 latest_hash = client.get_latest_blockhash()?;
3880 }
3881 let tx = Transaction::new_signed_with_payer(
3882 &instructions,
3883 Some(&keypair.pubkey()),
3884 &[&keypair, &buffer],
3885 latest_hash,
3886 );
3887 match client.send_and_confirm_transaction_with_spinner(&tx) {
3888 Ok(_) => break,
3889 Err(err) => {
3890 if retries == 19 {
3891 return Err(anyhow!("Error creating buffer account: {}", err));
3892 }
3893 println!("Error creating buffer account: {err}. Retrying...");
3894 }
3895 }
3896 }
3897
3898 Ok(buffer.pubkey())
3899}
3900
3901fn serialize_idl(idl: &Idl) -> Result<Vec<u8>> {
3903 let json_bytes = serde_json::to_vec(idl)?;
3904 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
3905 e.write_all(&json_bytes)?;
3906 e.finish().map_err(Into::into)
3907}
3908
3909fn serialize_idl_ix(ix_inner: anchor_lang::idl::IdlInstruction) -> Result<Vec<u8>> {
3910 let mut data = Vec::with_capacity(256);
3911 data.extend_from_slice(&anchor_lang::idl::IDL_IX_TAG.to_le_bytes());
3912 ix_inner.serialize(&mut data)?;
3913 Ok(data)
3914}
3915
3916fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
3917 with_workspace(cfg_override, |cfg| {
3918 println!("Running migration deploy script");
3919
3920 let url = cluster_url(cfg, &cfg.test_validator);
3921 let cur_dir = std::env::current_dir()?;
3922 let migrations_dir = cur_dir.join("migrations");
3923 let deploy_ts = Path::new("deploy.ts");
3924
3925 let use_ts = Path::new("tsconfig.json").exists() && migrations_dir.join(deploy_ts).exists();
3926
3927 if !Path::new(".anchor").exists() {
3928 fs::create_dir(".anchor")?;
3929 }
3930 std::env::set_current_dir(".anchor")?;
3931
3932 let exit = if use_ts {
3933 let module_path = migrations_dir.join(deploy_ts);
3934 let deploy_script_host_str =
3935 rust_template::deploy_ts_script_host(&url, &module_path.display().to_string());
3936 fs::write(deploy_ts, deploy_script_host_str)?;
3937
3938 let pkg_manager_cmd = match &cfg.toolchain.package_manager {
3939 Some(pkg_manager) => pkg_manager.to_string(),
3940 None => PackageManager::default().to_string(),
3941 };
3942
3943 std::process::Command::new(pkg_manager_cmd)
3944 .args([
3945 "run",
3946 "ts-node",
3947 &fs::canonicalize(deploy_ts)?.to_string_lossy(),
3948 ])
3949 .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
3950 .stdout(Stdio::inherit())
3951 .stderr(Stdio::inherit())
3952 .output()?
3953 } else {
3954 let deploy_js = deploy_ts.with_extension("js");
3955 let module_path = migrations_dir.join(&deploy_js);
3956 let deploy_script_host_str =
3957 rust_template::deploy_js_script_host(&url, &module_path.display().to_string());
3958 fs::write(&deploy_js, deploy_script_host_str)?;
3959
3960 std::process::Command::new("node")
3961 .arg(&deploy_js)
3962 .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
3963 .stdout(Stdio::inherit())
3964 .stderr(Stdio::inherit())
3965 .output()?
3966 };
3967
3968 if !exit.status.success() {
3969 eprintln!("Deploy failed.");
3970 std::process::exit(exit.status.code().unwrap());
3971 }
3972
3973 println!("Deploy complete.");
3974 Ok(())
3975 })
3976}
3977
3978fn set_workspace_dir_or_exit() {
3979 let d = match Config::discover(&ConfigOverride::default()) {
3980 Err(err) => {
3981 println!("Workspace configuration error: {err}");
3982 std::process::exit(1);
3983 }
3984 Ok(d) => d,
3985 };
3986 match d {
3987 None => {
3988 println!("Not in anchor workspace.");
3989 std::process::exit(1);
3990 }
3991 Some(cfg) => {
3992 match cfg.path().parent() {
3993 None => {
3994 println!("Unable to make new program");
3995 }
3996 Some(parent) => {
3997 if std::env::set_current_dir(parent).is_err() {
3998 println!("Not in anchor workspace.");
3999 std::process::exit(1);
4000 }
4001 }
4002 };
4003 }
4004 }
4005}
4006
4007#[cfg(feature = "dev")]
4008fn airdrop(cfg_override: &ConfigOverride) -> Result<()> {
4009 let url = cfg_override
4010 .cluster
4011 .as_ref()
4012 .unwrap_or(&Cluster::Devnet)
4013 .url();
4014 loop {
4015 let exit = std::process::Command::new("solana")
4016 .arg("airdrop")
4017 .arg("10")
4018 .arg("--url")
4019 .arg(url)
4020 .stdout(Stdio::inherit())
4021 .stderr(Stdio::inherit())
4022 .output()
4023 .expect("Must airdrop");
4024 if !exit.status.success() {
4025 println!("There was a problem airdropping: {:?}.", exit);
4026 std::process::exit(exit.status.code().unwrap_or(1));
4027 }
4028 std::thread::sleep(std::time::Duration::from_millis(10000));
4029 }
4030}
4031
4032fn cluster(_cmd: ClusterCommand) -> Result<()> {
4033 println!("Cluster Endpoints:\n");
4034 println!("* Mainnet - https://api.mainnet-beta.solana.com");
4035 println!("* Devnet - https://api.devnet.solana.com");
4036 println!("* Testnet - https://api.testnet.solana.com");
4037 Ok(())
4038}
4039
4040fn shell(cfg_override: &ConfigOverride) -> Result<()> {
4041 with_workspace(cfg_override, |cfg| {
4042 let programs = {
4043 let mut idls: HashMap<String, Idl> = cfg
4045 .read_all_programs()?
4046 .iter()
4047 .filter(|program| program.idl.is_some())
4048 .map(|program| {
4049 (
4050 program.idl.as_ref().unwrap().metadata.name.clone(),
4051 program.idl.clone().unwrap(),
4052 )
4053 })
4054 .collect();
4055 if let Some(programs) = cfg.programs.get(&cfg.provider.cluster) {
4057 let _ = programs
4058 .iter()
4059 .map(|(name, pd)| {
4060 if let Some(idl_fp) = &pd.idl {
4061 let file_str =
4062 fs::read_to_string(idl_fp).expect("Unable to read IDL file");
4063 let idl = serde_json::from_str(&file_str).expect("Idl not readable");
4064 idls.insert(name.clone(), idl);
4065 }
4066 })
4067 .collect::<Vec<_>>();
4068 }
4069
4070 match cfg.programs.get(&cfg.provider.cluster) {
4072 None => Vec::new(),
4073 Some(programs) => programs
4074 .iter()
4075 .filter_map(|(name, program_deployment)| {
4076 Some(ProgramWorkspace {
4077 name: name.to_string(),
4078 program_id: program_deployment.address,
4079 idl: match idls.get(name) {
4080 None => return None,
4081 Some(idl) => idl.clone(),
4082 },
4083 })
4084 })
4085 .collect::<Vec<ProgramWorkspace>>(),
4086 }
4087 };
4088 let url = cluster_url(cfg, &cfg.test_validator);
4089 let js_code = rust_template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?;
4090 let mut child = std::process::Command::new("node")
4091 .args(["-e", &js_code, "-i", "--experimental-repl-await"])
4092 .stdout(Stdio::inherit())
4093 .stderr(Stdio::inherit())
4094 .spawn()
4095 .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
4096
4097 if !child.wait()?.success() {
4098 println!("Error running node shell");
4099 return Ok(());
4100 }
4101 Ok(())
4102 })
4103}
4104
4105fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec<String>) -> Result<()> {
4106 with_workspace(cfg_override, |cfg| {
4107 let url = cluster_url(cfg, &cfg.test_validator);
4108 let script = cfg
4109 .scripts
4110 .get(&script)
4111 .ok_or_else(|| anyhow!("Unable to find script"))?;
4112 let script_with_args = format!("{script} {}", script_args.join(" "));
4113 let exit = std::process::Command::new("bash")
4114 .arg("-c")
4115 .arg(&script_with_args)
4116 .env("ANCHOR_PROVIDER_URL", url)
4117 .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
4118 .stdout(Stdio::inherit())
4119 .stderr(Stdio::inherit())
4120 .output()
4121 .unwrap();
4122 if !exit.status.success() {
4123 std::process::exit(exit.status.code().unwrap_or(1));
4124 }
4125 Ok(())
4126 })
4127}
4128
4129fn login(_cfg_override: &ConfigOverride, token: String) -> Result<()> {
4130 let anchor_dir = Path::new(&*shellexpand::tilde("~"))
4131 .join(".config")
4132 .join("anchor");
4133 if !anchor_dir.exists() {
4134 fs::create_dir(&anchor_dir)?;
4135 }
4136
4137 std::env::set_current_dir(&anchor_dir)?;
4138
4139 let mut file = File::create("credentials")?;
4141 file.write_all(rust_template::credentials(&token).as_bytes())?;
4142 Ok(())
4143}
4144
4145fn keys(cfg_override: &ConfigOverride, cmd: KeysCommand) -> Result<()> {
4146 match cmd {
4147 KeysCommand::List => keys_list(cfg_override),
4148 KeysCommand::Sync { program_name } => keys_sync(cfg_override, program_name),
4149 }
4150}
4151
4152fn keys_list(cfg_override: &ConfigOverride) -> Result<()> {
4153 with_workspace(cfg_override, |cfg| {
4154 for program in cfg.read_all_programs()? {
4155 let pubkey = program.pubkey()?;
4156 println!("{}: {}", program.lib_name, pubkey);
4157 }
4158 Ok(())
4159 })
4160}
4161
4162fn keys_sync(cfg_override: &ConfigOverride, program_name: Option<String>) -> Result<()> {
4164 with_workspace(cfg_override, |cfg| {
4165 let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
4166 .multi_line(true)
4167 .build()
4168 .unwrap();
4169
4170 let cfg_cluster = cfg.provider.cluster.to_owned();
4171 println!("Syncing program ids for the configured cluster ({cfg_cluster})\n");
4172
4173 let mut changed_src = false;
4174 for program in cfg.get_programs(program_name)? {
4175 let actual_program_id = program.pubkey()?.to_string();
4177
4178 let src_path = program.path.join("src");
4180 let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
4181
4182 for path in files_to_check {
4183 let mut content = match fs::read_to_string(&path) {
4184 Ok(content) => content,
4185 Err(_) => continue,
4186 };
4187
4188 let incorrect_program_id = declare_id_regex
4189 .captures(&content)
4190 .and_then(|captures| captures.get(3))
4191 .filter(|program_id_match| program_id_match.as_str() != actual_program_id);
4192 if let Some(program_id_match) = incorrect_program_id {
4193 println!("Found incorrect program id declaration in {path:?}");
4194
4195 content.replace_range(program_id_match.range(), &actual_program_id);
4197 fs::write(&path, content)?;
4198
4199 changed_src = true;
4200 println!("Updated to {actual_program_id}\n");
4201 break;
4202 }
4203 }
4204
4205 'outer: for (cluster, programs) in &mut cfg.programs {
4207 if cluster != &cfg_cluster {
4209 continue;
4210 }
4211
4212 for (name, deployment) in programs {
4213 if name != &program.lib_name {
4215 continue;
4216 }
4217
4218 if deployment.address.to_string() != actual_program_id {
4219 println!("Found incorrect program id declaration in Anchor.toml for the program `{name}`");
4220
4221 deployment.address = Pubkey::from_str(&actual_program_id).unwrap();
4223 fs::write(cfg.path(), cfg.to_string())?;
4224
4225 println!("Updated to {actual_program_id}\n");
4226 break 'outer;
4227 }
4228 }
4229 }
4230 }
4231
4232 println!("All program id declarations are synced.");
4233 if changed_src {
4234 println!("Please rebuild the program to update the generated artifacts.")
4235 }
4236
4237 Ok(())
4238 })
4239}
4240
4241fn localnet(
4242 cfg_override: &ConfigOverride,
4243 skip_build: bool,
4244 skip_deploy: bool,
4245 skip_lint: bool,
4246 env_vars: Vec<String>,
4247 cargo_args: Vec<String>,
4248 arch: ProgramArch,
4249) -> Result<()> {
4250 with_workspace(cfg_override, |cfg| {
4251 if !skip_build {
4253 build(
4254 cfg_override,
4255 false,
4256 None,
4257 None,
4258 false,
4259 skip_lint,
4260 None,
4261 None,
4262 None,
4263 BootstrapMode::None,
4264 None,
4265 None,
4266 env_vars,
4267 cargo_args,
4268 false,
4269 arch,
4270 )?;
4271 }
4272
4273 let flags = match skip_deploy {
4274 true => None,
4275 false => Some(validator_flags(cfg, &cfg.test_validator)?),
4276 };
4277
4278 let validator_handle = &mut start_test_validator(cfg, &cfg.test_validator, flags, false)?;
4279
4280 let url = test_validator_rpc_url(&cfg.test_validator);
4282 let log_streams = stream_logs(cfg, &url);
4283
4284 std::io::stdin().lock().lines().next().unwrap().unwrap();
4285
4286 if let Err(err) = validator_handle.kill() {
4288 println!(
4289 "Failed to kill subprocess {}: {}",
4290 validator_handle.id(),
4291 err
4292 );
4293 }
4294
4295 for mut child in log_streams? {
4296 if let Err(err) = child.kill() {
4297 println!("Failed to kill subprocess {}: {}", child.id(), err);
4298 }
4299 }
4300
4301 Ok(())
4302 })
4303}
4304
4305fn with_workspace<R>(
4312 cfg_override: &ConfigOverride,
4313 f: impl FnOnce(&mut WithPath<Config>) -> R,
4314) -> R {
4315 set_workspace_dir_or_exit();
4316
4317 let mut cfg = Config::discover(cfg_override)
4318 .expect("Previously set the workspace dir")
4319 .expect("Anchor.toml must always exist");
4320
4321 let r = f(&mut cfg);
4322
4323 set_workspace_dir_or_exit();
4324
4325 r
4326}
4327
4328fn is_hidden(entry: &walkdir::DirEntry) -> bool {
4329 entry
4330 .file_name()
4331 .to_str()
4332 .map(|s| s == "." || s.starts_with('.') || s == "target")
4333 .unwrap_or(false)
4334}
4335
4336fn get_node_version() -> Result<Version> {
4337 let node_version = std::process::Command::new("node")
4338 .arg("--version")
4339 .stderr(Stdio::inherit())
4340 .output()
4341 .map_err(|e| anyhow::format_err!("node failed: {}", e.to_string()))?;
4342 let output = std::str::from_utf8(&node_version.stdout)?
4343 .strip_prefix('v')
4344 .unwrap()
4345 .trim();
4346 Version::parse(output).map_err(Into::into)
4347}
4348
4349fn add_recommended_deployment_solana_args(
4350 client: &RpcClient,
4351 args: Vec<String>,
4352) -> Result<Vec<String>> {
4353 let mut augmented_args = args.clone();
4354
4355 if !args.contains(&"--with-compute-unit-price".to_string()) {
4357 let priority_fee = get_recommended_micro_lamport_fee(client)?;
4358 augmented_args.push("--with-compute-unit-price".to_string());
4359 augmented_args.push(priority_fee.to_string());
4360 }
4361
4362 const DEFAULT_MAX_SIGN_ATTEMPTS: u8 = 30;
4363 if !args.contains(&"--max-sign-attempts".to_string()) {
4364 augmented_args.push("--max-sign-attempts".to_string());
4365 augmented_args.push(DEFAULT_MAX_SIGN_ATTEMPTS.to_string());
4366 }
4367
4368 if !args.contains(&"--buffer".to_owned()) {
4372 let tmp_keypair_path = std::env::temp_dir().join("anchor-upgrade-buffer.json");
4373 if !tmp_keypair_path.exists() {
4374 if let Err(err) = Keypair::new().write_to_file(&tmp_keypair_path) {
4375 return Err(anyhow!(
4376 "Error creating keypair for buffer account, {:?}",
4377 err
4378 ));
4379 }
4380 }
4381
4382 augmented_args.push("--buffer".to_owned());
4383 augmented_args.push(tmp_keypair_path.to_string_lossy().to_string());
4384 }
4385
4386 Ok(augmented_args)
4387}
4388
4389fn get_recommended_micro_lamport_fee(client: &RpcClient) -> Result<u64> {
4390 let mut fees = client.get_recent_prioritization_fees(&[])?;
4391 if fees.is_empty() {
4392 return Ok(0);
4394 }
4395
4396 fees.sort_unstable_by_key(|fee| fee.prioritization_fee);
4398 let median_index = fees.len() / 2;
4399
4400 let median_priority_fee = if fees.len() % 2 == 0 {
4401 (fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2
4402 } else {
4403 fees[median_index].prioritization_fee
4404 };
4405
4406 Ok(median_priority_fee)
4407}
4408
4409fn prepend_compute_unit_ix(
4412 instructions: Vec<Instruction>,
4413 client: &RpcClient,
4414 priority_fee: Option<u64>,
4415) -> Result<Vec<Instruction>> {
4416 let priority_fee = match priority_fee {
4417 Some(fee) => fee,
4418 None => get_recommended_micro_lamport_fee(client)?,
4419 };
4420
4421 if priority_fee > 0 {
4422 let mut instructions_appended = instructions.clone();
4423 instructions_appended.insert(
4424 0,
4425 ComputeBudgetInstruction::set_compute_unit_price(priority_fee),
4426 );
4427 Ok(instructions_appended)
4428 } else {
4429 Ok(instructions)
4430 }
4431}
4432
4433fn get_node_dns_option() -> Result<&'static str> {
4434 let version = get_node_version()?;
4435 let req = VersionReq::parse(">=16.4.0").unwrap();
4436 let option = match req.matches(&version) {
4437 true => "--dns-result-order=ipv4first",
4438 false => "",
4439 };
4440 Ok(option)
4441}
4442
4443fn strip_workspace_prefix(absolute_path: String) -> String {
4450 let workspace_prefix =
4451 std::env::current_dir().unwrap().display().to_string() + std::path::MAIN_SEPARATOR_STR;
4452 absolute_path
4453 .strip_prefix(&workspace_prefix)
4454 .unwrap_or(&absolute_path)
4455 .into()
4456}
4457
4458fn create_client<U: ToString>(url: U) -> RpcClient {
4460 RpcClient::new_with_commitment(url, CommitmentConfig::confirmed())
4461}
4462
4463#[cfg(test)]
4464mod tests {
4465 use super::*;
4466
4467 #[test]
4468 #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
4469 fn test_init_reserved_word() {
4470 init(
4471 &ConfigOverride {
4472 cluster: None,
4473 wallet: None,
4474 },
4475 "await".to_string(),
4476 true,
4477 true,
4478 PackageManager::default(),
4479 false,
4480 ProgramTemplate::default(),
4481 TestTemplate::default(),
4482 false,
4483 )
4484 .unwrap();
4485 }
4486
4487 #[test]
4488 #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
4489 fn test_init_reserved_word_from_syn() {
4490 init(
4491 &ConfigOverride {
4492 cluster: None,
4493 wallet: None,
4494 },
4495 "fn".to_string(),
4496 true,
4497 true,
4498 PackageManager::default(),
4499 false,
4500 ProgramTemplate::default(),
4501 TestTemplate::default(),
4502 false,
4503 )
4504 .unwrap();
4505 }
4506
4507 #[test]
4508 #[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
4509 fn test_init_starting_with_digit() {
4510 init(
4511 &ConfigOverride {
4512 cluster: None,
4513 wallet: None,
4514 },
4515 "1project".to_string(),
4516 true,
4517 true,
4518 PackageManager::default(),
4519 false,
4520 ProgramTemplate::default(),
4521 TestTemplate::default(),
4522 false,
4523 )
4524 .unwrap();
4525 }
4526}