use {
crate::config::{
get_default_ledger_path, BootstrapMode, BuildConfig, Config, ConfigOverride, HookType,
Manifest, PackageManager, ProgramDeployment, ProgramWorkspace, ScriptsConfig,
SurfnetInfoResponse, SurfpoolConfig, TestValidator, Validator, ValidatorType, WithPath,
SHUTDOWN_WAIT, STARTUP_WAIT, SURFPOOL_HOST,
},
abs_path::AbsolutePath,
anchor_cli_macros::AbsolutePath,
anchor_client::Cluster,
anchor_lang::{
prelude::UpgradeableLoaderState, solana_program::bpf_loader_upgradeable, AnchorDeserialize,
},
anchor_lang_idl::{
convert::{convert_idl, convert_idl_to_legacy},
types::{Idl, IdlArrayLen, IdlDefinedFields, IdlType, IdlTypeDefTy},
},
anyhow::{anyhow, bail, Context, Result},
base64::{engine::general_purpose::STANDARD, Engine},
cargo_metadata::{DependencyKind, MetadataCommand},
checks::{check_anchor_version, check_deps, check_idl_build_feature, check_overflow},
clap::{CommandFactory, Parser},
dirs::home_dir,
heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase},
regex::{Regex, RegexBuilder},
semver::{Version, VersionReq},
serde::Deserialize,
serde_json::{json, Map, Value as JsonValue},
solana_cli_config::Config as SolanaCliConfig,
solana_commitment_config::CommitmentConfig,
solana_compute_budget_interface::ComputeBudgetInstruction,
solana_instruction::Instruction,
solana_keypair::Keypair,
solana_pubkey::Pubkey,
solana_pubsub_client::pubsub_client::{PubsubClient, PubsubClientSubscription},
solana_rpc_client::rpc_client::RpcClient,
solana_rpc_client_api::{
config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter},
request::RpcRequest,
response::{Response as RpcResponse, RpcLogsResponse},
},
solana_signer::{EncodableKey, Signer},
std::{
collections::{BTreeMap, HashMap, HashSet},
ffi::OsString,
fs::{self, File},
io::prelude::*,
path::{Path, PathBuf},
process::{Child, ExitStatus, Stdio},
string::ToString,
sync::{LazyLock, OnceLock},
},
template::{AnchorVersion, ProgramTemplate, TestTemplate},
};
mod abs_path;
mod account;
mod checks;
pub mod codama;
pub mod config;
#[cfg(not(windows))]
pub mod coverage;
#[cfg(not(windows))]
pub mod debugger;
pub mod fetch;
#[cfg(not(windows))]
mod flamegraph;
mod keygen;
mod legacy_idl;
mod metadata;
#[cfg(not(windows))]
mod profile;
mod program;
pub mod template;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DOCKER_BUILDER_VERSION: &str = VERSION;
pub const DEFAULT_RPC_PORT: u16 = 8899;
const DEFAULT_FAUCET_PORT: u16 = 9900;
pub const WEBSOCKET_PORT_OFFSET: u16 = 1;
pub static AVM_HOME: LazyLock<PathBuf> = LazyLock::new(|| {
if let Ok(avm_home) = std::env::var("AVM_HOME") {
PathBuf::from(avm_home)
} else {
let mut user_home = dirs::home_dir().expect("Could not find home directory");
user_home.push(".avm");
user_home
}
});
pub fn support_version_report() -> String {
let mut lines = vec![format!("anchor-cli {VERSION}")];
lines.push(command_version_line("solana-cli", "solana"));
lines.push(command_version_line("cargo", "cargo"));
lines.push(format!("OS: {}", os_version()));
lines.join("\n") + "\n"
}
fn command_version_line(label: &str, command: &str) -> String {
match command_output(command, &["--version"]) {
Some(version) if version.starts_with(label) => version,
Some(version) => format!("{label} {version}"),
None => format!("{label} unavailable"),
}
}
fn command_output(command: &str, args: &[&str]) -> Option<String> {
let output = std::process::Command::new(command)
.args(args)
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.and_then(|output| output.lines().next().map(str::trim).map(str::to_owned))
.filter(|line| !line.is_empty())
}
fn os_version() -> String {
#[cfg(target_os = "macos")]
if let Some(version) = macos_version() {
return version;
}
#[cfg(target_os = "linux")]
{
if let Some(version) = command_output("lsb_release", &["-ds"]) {
return version.trim_matches('"').to_owned();
}
if let Some(version) = linux_os_release() {
return version;
}
}
#[cfg(target_os = "windows")]
if let Some(version) = command_output("cmd", &["/C", "ver"]) {
return version;
}
std::env::consts::OS.to_owned()
}
#[cfg(target_os = "macos")]
fn macos_version() -> Option<String> {
let name = command_output("sw_vers", &["-productName"])?;
let version = command_output("sw_vers", &["-productVersion"])?;
let build = command_output("sw_vers", &["-buildVersion"])?;
Some(format!("{name} {version} {build}"))
}
#[cfg(target_os = "linux")]
fn linux_os_release() -> Option<String> {
fs::read_to_string("/etc/os-release")
.ok()?
.lines()
.find_map(|line| line.strip_prefix("PRETTY_NAME="))
.map(|value| value.trim_matches('"').to_owned())
}
#[derive(Debug, Parser, AbsolutePath)]
#[clap(version = VERSION)]
pub struct Opts {
#[clap(flatten)]
pub cfg_override: ConfigOverride,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum Command {
Init {
name: String,
#[clap(short, long)]
javascript: bool,
#[clap(long)]
no_install: bool,
#[clap(value_enum, long)]
package_manager: Option<PackageManager>,
#[clap(long)]
no_git: bool,
#[clap(value_enum, short, long, default_value = "multiple")]
template: ProgramTemplate,
#[clap(value_enum, long, default_value = "v1")]
anchor_version: AnchorVersion,
#[clap(value_enum, long, default_value = "litesvm")]
test_template: TestTemplate,
#[clap(long, action)]
force: bool,
#[clap(long)]
install_agent_skills: bool,
},
#[clap(name = "build", alias = "b")]
Build {
#[clap(long)]
skip_lint: bool,
#[clap(long)]
ignore_keys: bool,
#[clap(long)]
no_idl: bool,
#[clap(short, long)]
idl: Option<String>,
#[clap(short = 't', long)]
idl_ts: Option<String>,
#[clap(short, long)]
verifiable: bool,
#[clap(short, long)]
program_name: Option<String>,
#[clap(short, long)]
solana_version: Option<String>,
#[clap(short, long)]
docker_image: Option<String>,
#[clap(value_enum, short, long, default_value = "none")]
bootstrap: BootstrapMode,
#[clap(short, long, required = false)]
env: Vec<String>,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
#[clap(long)]
no_docs: bool,
},
Expand {
#[clap(short, long)]
program_name: Option<String>,
#[clap(long)]
stdout: bool,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
Verify {
program_id: Pubkey,
#[clap(long, conflicts_with = "current_dir")]
repo_url: Option<String>,
#[clap(long, requires = "repo_url")]
commit_hash: Option<String>,
#[clap(long)]
current_dir: bool,
#[clap(long)]
program_name: Option<String>,
#[clap(raw = true)]
args: Vec<String>,
},
#[clap(name = "test", alias = "t")]
Test {
#[clap(short, long)]
program_name: Option<String>,
#[clap(long)]
skip_deploy: bool,
#[clap(long)]
skip_lint: bool,
#[clap(long)]
skip_local_validator: bool,
#[clap(long)]
skip_build: bool,
#[clap(long)]
no_idl: bool,
#[clap(long)]
detach: bool,
#[clap(long)]
run: Vec<String>,
#[clap(long)]
script: Option<String>,
#[clap(value_enum, long, default_value = "surfpool")]
validator: ValidatorType,
#[clap(long)]
profile: bool,
args: Vec<String>,
#[clap(short, long, required = false)]
env: Vec<String>,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
Fuzz(crucible_fuzz_cli::Cli),
New {
name: String,
#[clap(value_enum, short, long, default_value = "multiple")]
template: ProgramTemplate,
#[clap(value_enum, long, default_value = "v1")]
anchor_version: AnchorVersion,
#[clap(long, action)]
force: bool,
},
#[cfg(not(windows))]
Debugger {
test_name: Option<String>,
#[clap(long)]
skip_run: bool,
#[clap(long)]
skip_build: bool,
#[clap(long)]
skip_lint: bool,
#[clap(long)]
gdb: bool,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
#[cfg(not(windows))]
Coverage {
#[clap(long)]
skip_run: bool,
#[clap(long)]
skip_build: bool,
#[clap(long, default_value = "target/coverage/sbf.lcov")]
output: String,
#[clap(long, default_value = "target/coverage/traces")]
trace_dir: String,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
Idl {
#[clap(subcommand)]
subcmd: IdlCommand,
},
Clean,
#[clap(hide = true)]
#[deprecated(since = "0.32.0", note = "use `anchor program deploy` instead")]
Deploy {
#[clap(short, long)]
program_name: Option<String>,
#[clap(long, requires = "program_name")]
program_keypair: Option<PathBuf>,
#[clap(short, long)]
verifiable: bool,
#[clap(long)]
no_idl: bool,
#[clap(required = false, last = true)]
solana_args: Vec<String>,
},
Migrate,
#[clap(hide = true)]
#[deprecated(since = "0.32.0", note = "use `anchor program upgrade` instead")]
Upgrade {
#[clap(short, long)]
program_id: Pubkey,
program_filepath: PathBuf,
#[clap(long, default_value = "0")]
max_retries: u32,
#[clap(required = false, last = true)]
solana_args: Vec<String>,
},
Airdrop {
amount: f64,
pubkey: Option<Pubkey>,
},
Cluster {
#[clap(subcommand)]
subcmd: ClusterCommand,
},
Config {
#[clap(subcommand)]
subcmd: ConfigCommand,
},
Shell,
#[clap(alias = "r")]
Run {
script: String,
#[clap(required = false, last = true)]
script_args: Vec<String>,
},
Keys {
#[clap(subcommand)]
subcmd: KeysCommand,
},
Localnet {
#[clap(long)]
skip_build: bool,
#[clap(long)]
skip_deploy: bool,
#[clap(long)]
skip_lint: bool,
#[clap(long)]
ignore_keys: bool,
#[clap(value_enum, long, default_value = "surfpool")]
validator: ValidatorType,
#[clap(short, long, required = false)]
env: Vec<String>,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
Account {
account_type: String,
address: Pubkey,
#[clap(long)]
idl: Option<PathBuf>,
},
Completions {
#[clap(value_enum)]
shell: clap_complete::Shell,
},
Address,
Balance {
pubkey: Option<Pubkey>,
#[clap(long)]
lamports: bool,
},
Epoch,
#[clap(name = "epoch-info")]
EpochInfo,
Logs {
#[clap(long)]
include_votes: bool,
#[clap(long)]
address: Option<Vec<Pubkey>>,
},
ShowAccount {
#[clap(flatten)]
cmd: account::ShowAccountCommand,
},
Keygen {
#[clap(subcommand)]
subcmd: KeygenCommand,
},
Program {
#[clap(subcommand)]
subcmd: ProgramCommand,
},
Codama {
#[clap(subcommand)]
subcmd: codama::CodamaCommand,
},
LegacyIdl {
#[clap(subcommand)]
subcmd: legacy_idl::LegacyIdlCommand,
},
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum KeygenCommand {
New {
#[clap(short = 'o', long)]
outfile: Option<PathBuf>,
#[clap(short, long)]
force: bool,
#[clap(long)]
no_passphrase: bool,
#[clap(long)]
silent: bool,
#[clap(short = 'w', long, default_value = "12")]
word_count: usize,
},
Pubkey {
keypair: Option<PathBuf>,
},
Recover {
#[clap(short = 'o', long)]
outfile: Option<PathBuf>,
#[clap(short, long)]
force: bool,
#[clap(long)]
skip_seed_phrase_validation: bool,
#[clap(long)]
no_passphrase: bool,
},
Verify {
pubkey: Pubkey,
keypair: Option<PathBuf>,
},
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum KeysCommand {
List,
Sync {
#[clap(short, long)]
program_name: Option<String>,
},
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum ProgramCommand {
Deploy {
program_filepath: Option<PathBuf>,
#[clap(short, long)]
program_name: Option<String>,
#[clap(long)]
program_keypair: Option<PathBuf>,
#[clap(long)]
upgrade_authority: Option<String>,
#[clap(long)]
program_id: Option<Pubkey>,
#[clap(long)]
buffer: Option<Pubkey>,
#[clap(long)]
max_len: Option<usize>,
#[clap(long)]
use_rpc: bool,
#[clap(long)]
no_idl: bool,
#[clap(long = "final")]
make_final: bool,
#[clap(required = false, last = true)]
solana_args: Vec<String>,
},
WriteBuffer {
program_filepath: Option<PathBuf>,
#[clap(short, long)]
program_name: Option<String>,
#[clap(long)]
buffer: Option<String>,
#[clap(long)]
buffer_authority: Option<String>,
#[clap(long)]
max_len: Option<usize>,
},
SetBufferAuthority {
buffer: Pubkey,
new_buffer_authority: Pubkey,
},
SetUpgradeAuthority {
program_id: Pubkey,
#[clap(long)]
new_upgrade_authority: Option<Pubkey>,
#[clap(long)]
new_upgrade_authority_signer: Option<String>,
#[clap(long)]
skip_new_upgrade_authority_signer_check: bool,
#[clap(long = "final")]
make_final: bool,
#[clap(long)]
upgrade_authority: Option<String>,
},
Show {
account: Pubkey,
#[clap(long)]
get_programs: bool,
#[clap(long)]
get_buffers: bool,
#[clap(long)]
all: bool,
},
Upgrade {
program_id: Pubkey,
#[clap(long)]
program_filepath: Option<PathBuf>,
#[clap(short, long)]
program_name: Option<String>,
#[clap(long)]
buffer: Option<Pubkey>,
#[clap(long)]
upgrade_authority: Option<String>,
#[clap(long, default_value = "0")]
max_retries: u32,
#[clap(long)]
use_rpc: bool,
#[clap(required = false, last = true)]
solana_args: Vec<String>,
},
Dump {
account: Pubkey,
output_file: String,
},
Close {
account: Option<Pubkey>,
#[clap(short, long)]
program_name: Option<String>,
#[clap(long)]
authority: Option<String>,
#[clap(long)]
recipient: Option<Pubkey>,
#[clap(long)]
bypass_warning: bool,
},
Extend {
program_id: Option<Pubkey>,
#[clap(short, long)]
program_name: Option<String>,
additional_bytes: usize,
},
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum IdlCommand {
Init {
program_id: Option<Pubkey>,
#[clap(short, long)]
filepath: PathBuf,
#[clap(long)]
priority_fee: Option<u64>,
#[clap(long)]
non_canonical: bool,
#[clap(long)]
#[cfg(feature = "idl-localnet-testing")]
allow_localnet: bool,
},
Upgrade {
program_id: Option<Pubkey>,
#[clap(short, long)]
filepath: PathBuf,
#[clap(long)]
priority_fee: Option<u64>,
#[clap(long)]
#[cfg(feature = "idl-localnet-testing")]
allow_localnet: bool,
},
#[clap(alias = "b")]
Build {
#[clap(short, long)]
program_name: Option<String>,
#[clap(short, long)]
out: Option<String>,
#[clap(short = 't', long)]
out_ts: Option<String>,
#[clap(long)]
no_docs: bool,
#[clap(long)]
skip_lint: bool,
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
Fetch {
program_id: Pubkey,
#[clap(short, long)]
out: Option<String>,
#[clap(long)]
non_canonical: bool,
},
FetchHistorical {
program_id: Pubkey,
#[clap(long)]
authority: Option<Pubkey>,
#[clap(long, conflicts_with_all = ["before", "after"])]
slot: Option<u64>,
#[clap(long)]
before: Option<String>,
#[clap(long)]
after: Option<String>,
#[clap(long)]
out_dir: Option<PathBuf>,
#[clap(long)]
rpc_workers: Option<usize>,
#[clap(long, conflicts_with = "rpc_workers")]
no_parallel: bool,
#[clap(long, default_value_t = 5)]
rpc_max_retries: u32,
#[clap(long, default_value_t = 500)]
rpc_retry_backoff_ms: u64,
#[clap(long, default_value_t = 1000)]
max_signatures: usize,
#[clap(long)]
verbose: bool,
},
Convert {
path: PathBuf,
#[clap(short, long)]
out: Option<PathBuf>,
#[clap(short, long)]
program_id: Option<Pubkey>,
#[clap(long)]
to_legacy: bool,
},
Type {
path: PathBuf,
#[clap(short, long)]
out: Option<PathBuf>,
},
Close {
program_id: Pubkey,
#[clap(long, default_value = "idl")]
seed: String,
#[clap(long)]
priority_fee: Option<u64>,
},
CreateBuffer {
#[clap(short, long)]
filepath: PathBuf,
#[clap(long)]
priority_fee: Option<u64>,
},
SetBufferAuthority {
buffer: Pubkey,
#[clap(short, long)]
new_authority: Pubkey,
#[clap(long)]
priority_fee: Option<u64>,
},
WriteBuffer {
program_id: Pubkey,
#[clap(short, long)]
buffer: Pubkey,
#[clap(long, default_value = "idl")]
seed: String,
#[clap(long)]
close_buffer: bool,
#[clap(long)]
priority_fee: Option<u64>,
},
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum ClusterCommand {
List,
}
#[derive(Debug, Parser, AbsolutePath)]
pub enum ConfigCommand {
Get,
Set {
#[clap(short = 'u', long = "url")]
url: Option<String>,
#[clap(short = 'k', long = "keypair")]
keypair: Option<PathBuf>,
},
}
fn get_keypair(path: &Path) -> Result<Keypair> {
solana_keypair::read_keypair_file(path)
.map_err(|_| anyhow!("Unable to read keypair file ({})", path.display()))
}
fn format_sol(lamports: u64) -> String {
let sol = lamports as f64 / 1_000_000_000.0;
let formatted = format!("{:.8}", sol);
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
format!("{} SOL", trimmed)
}
fn get_cluster_and_wallet(cfg_override: &ConfigOverride) -> Result<(String, String)> {
if let Ok(Some(cfg)) = Config::discover(cfg_override) {
return Ok((
cfg.provider.cluster.url().to_string(),
cfg.provider.wallet.to_string(),
));
}
let (cluster_url, wallet_path) =
if let Some(config_file) = solana_cli_config::CONFIG_FILE.as_ref() {
match SolanaCliConfig::load(config_file) {
Ok(cli_config) => (
cli_config.json_rpc_url.clone(),
cli_config.keypair_path.clone(),
),
Err(_) => {
(
"https://api.mainnet-beta.solana.com".to_string(),
dirs::home_dir()
.map(|home| {
home.join(".config/solana/id.json")
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| "~/.config/solana/id.json".to_string()),
)
}
}
} else {
(
"https://api.mainnet-beta.solana.com".to_string(),
dirs::home_dir()
.map(|home| {
home.join(".config/solana/id.json")
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| "~/.config/solana/id.json".to_string()),
)
};
let final_cluster = if let Some(cluster) = &cfg_override.cluster {
cluster.url().to_string()
} else {
cluster_url
};
Ok((final_cluster, wallet_path))
}
pub fn get_recommended_micro_lamport_fee(
client: &RpcClient,
write_locked_accounts: &[Pubkey],
) -> u64 {
let mut fees = match client.get_recent_prioritization_fees(write_locked_accounts) {
Err(e) => {
eprintln!("Warning: failed to fetch prioritization fees, defaulting to 0: {e}");
return 0;
}
Ok(f) if f.is_empty() => {
return 0;
}
Ok(f) => f,
};
fees.sort_unstable_by_key(|fee| fee.prioritization_fee);
let median_index = fees.len() / 2;
if fees.len() % 2 == 0 {
(fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2
} else {
fees[median_index].prioritization_fee
}
}
pub fn prepend_compute_unit_ix(
instructions: Vec<Instruction>,
client: &RpcClient,
priority_fee: Option<u64>,
write_locked_accounts: &[Pubkey],
) -> Vec<Instruction> {
let priority_fee = priority_fee
.unwrap_or_else(|| get_recommended_micro_lamport_fee(client, write_locked_accounts));
if priority_fee > 0 {
let mut instructions_appended = instructions.clone();
instructions_appended.insert(
0,
ComputeBudgetInstruction::set_compute_unit_price(priority_fee),
);
instructions_appended
} else {
instructions
}
}
pub fn entry(opts: Opts) -> Result<()> {
let opts = opts.absolute();
let restore_cbs = override_toolchain(&opts.cfg_override)?;
let result = process_command(opts);
restore_toolchain(restore_cbs)?;
result
}
type RestoreToolchainCallbacks = Vec<Box<dyn FnOnce() -> Result<()>>>;
fn override_toolchain(cfg_override: &ConfigOverride) -> Result<RestoreToolchainCallbacks> {
let mut restore_cbs: RestoreToolchainCallbacks = vec![];
let cfg = Config::discover(cfg_override)?;
if let Some(cfg) = cfg {
fn parse_version(text: &str) -> Option<String> {
Some(
Regex::new(r"(\d+\.\d+\.\S+)")
.unwrap()
.captures_iter(text)
.next()?
.get(0)?
.as_str()
.to_string(),
)
}
fn get_current_version(cmd_name: &str) -> Result<String> {
let output = std::process::Command::new(cmd_name)
.arg("--version")
.output()?;
if !output.status.success() {
return Err(anyhow!("Failed to run `{cmd_name} --version`"));
}
let output_version = std::str::from_utf8(&output.stdout)?;
parse_version(output_version)
.ok_or_else(|| anyhow!("Failed to parse the version of `{cmd_name}`"))
}
if let Some(solana_version) = &cfg.toolchain.solana_version {
let current_version = get_current_version("solana")?;
if solana_version != ¤t_version {
fn override_solana_version(version: String) -> Result<bool> {
let (cmd_name, domain) =
if Version::parse(&version)? < Version::parse("1.18.19")? {
("solana-install", "anza.xyz")
} else {
("agave-install", "anza.xyz")
};
if get_current_version(cmd_name).is_err() {
eprintln!(
"Command not installed: `{cmd_name}`. \
See https://github.com/anza-xyz/agave/wiki/Agave-Transition, \
installing..."
);
let install_script = std::process::Command::new("curl")
.args([
"-sSfL",
&format!("https://release.{domain}/v{version}/install"),
])
.output()?;
let is_successful = std::process::Command::new("sh")
.args(["-c", std::str::from_utf8(&install_script.stdout)?])
.spawn()?
.wait_with_output()?
.status
.success();
if !is_successful {
return Err(anyhow!("Failed to install `{cmd_name}`"));
}
}
let output = std::process::Command::new(cmd_name).arg("list").output()?;
if !output.status.success() {
return Err(anyhow!("Failed to list installed `solana` versions"));
}
let is_installed = std::str::from_utf8(&output.stdout)?
.lines()
.filter_map(parse_version)
.any(|line_version| line_version == version);
let (stderr, stdout) = if is_installed {
(Stdio::null(), Stdio::null())
} else {
(Stdio::inherit(), Stdio::inherit())
};
std::process::Command::new(cmd_name)
.arg("init")
.arg(&version)
.stderr(stderr)
.stdout(stdout)
.spawn()?
.wait()
.map(|status| status.success())
.map_err(|err| anyhow!("Failed to run `{cmd_name}` command: {err}"))
}
match override_solana_version(solana_version.to_owned())? {
true => restore_cbs.push(Box::new(|| {
match override_solana_version(current_version)? {
true => Ok(()),
false => Err(anyhow!("Failed to restore `solana` version")),
}
})),
false => eprintln!(
"Failed to override `solana` version to {solana_version}, using \
{current_version} instead"
),
}
}
}
if let Some(anchor_version) = &cfg.toolchain.anchor_version {
if std::env::var("AVM_ACTIVE").is_ok() {
return Ok(restore_cbs);
}
const ANCHOR_BINARY_PREFIX: &str = "anchor-";
let current_version = std::env::args()
.next()
.expect("First arg should exist")
.parse::<PathBuf>()?
.file_name()
.and_then(|name| name.to_str())
.expect("File name should be valid Unicode")
.split_once(ANCHOR_BINARY_PREFIX)
.map(|(_, version)| version)
.unwrap_or(VERSION)
.to_owned();
if anchor_version != ¤t_version {
let binary_path = home_dir()
.unwrap()
.join(".avm")
.join("bin")
.join(format!("{ANCHOR_BINARY_PREFIX}{anchor_version}"));
if !binary_path.exists() {
eprintln!(
"`anchor` {anchor_version} is not installed with `avm`. Installing...\n"
);
if let Err(e) = install_with_avm(anchor_version, false) {
eprintln!(
"Failed to install `anchor`: {e}, using {current_version} instead"
);
return Ok(restore_cbs);
}
}
let exit_code = std::process::Command::new(binary_path)
.args(std::env::args_os().skip(1))
.spawn()?
.wait()?
.code()
.unwrap_or(1);
restore_toolchain(restore_cbs)?;
std::process::exit(exit_code);
}
}
}
Ok(restore_cbs)
}
fn install_with_avm(version: &str, verify: bool) -> Result<()> {
let mut cmd = std::process::Command::new("avm");
cmd.arg("install");
cmd.arg(version);
cmd.arg("--force");
if verify {
cmd.arg("--verify");
}
let status = cmd.status().context("running AVM")?;
if !status.success() {
bail!("failed to install `anchor` {version} with avm");
}
Ok(())
}
fn restore_toolchain(restore_cbs: RestoreToolchainCallbacks) -> Result<()> {
for restore_toolchain in restore_cbs {
if let Err(e) = restore_toolchain() {
eprintln!("Toolchain error: {e}");
}
}
Ok(())
}
fn get_npm_init_license() -> Result<String> {
let npm_init_license_output = std::process::Command::new("npm")
.arg("config")
.arg("get")
.arg("init-license")
.output()?;
if !npm_init_license_output.status.success() {
return Err(anyhow!("Failed to get npm init license"));
}
let license = String::from_utf8(npm_init_license_output.stdout)?;
Ok(license.trim().to_string())
}
fn process_command(opts: Opts) -> Result<()> {
match opts.command {
Command::Init {
name,
javascript,
no_install,
package_manager,
no_git,
template,
anchor_version,
test_template,
force,
install_agent_skills,
} => init(
&opts.cfg_override,
name,
javascript,
no_install,
package_manager,
no_git,
template,
anchor_version,
test_template,
force,
install_agent_skills,
),
Command::Fuzz(cli) => crucible_fuzz_cli::run(cli),
Command::New {
name,
template,
anchor_version,
force,
} => new(&opts.cfg_override, name, template, anchor_version, force),
Command::Build {
no_idl,
idl,
idl_ts,
verifiable,
program_name,
solana_version,
docker_image,
bootstrap,
cargo_args,
env,
skip_lint,
ignore_keys,
no_docs,
} => build(
&opts.cfg_override,
no_idl,
idl,
idl_ts,
verifiable,
skip_lint,
ignore_keys,
program_name,
solana_version,
docker_image,
bootstrap,
None,
None,
env,
cargo_args,
no_docs,
),
Command::Verify {
program_id,
repo_url,
commit_hash,
current_dir,
program_name,
args,
} => verify(
program_id,
repo_url,
commit_hash,
current_dir,
program_name,
args,
),
Command::Clean => clean(&opts.cfg_override),
#[allow(deprecated)]
Command::Deploy {
program_name,
program_keypair,
verifiable,
no_idl,
solana_args,
} => {
eprintln!(
"Warning: 'anchor deploy' is deprecated. Use 'anchor program deploy' instead."
);
deploy(
&opts.cfg_override,
program_name,
program_keypair,
verifiable,
no_idl,
solana_args,
)
}
Command::Expand {
program_name,
stdout,
cargo_args,
} => expand(&opts.cfg_override, program_name, stdout, &cargo_args),
#[allow(deprecated)]
Command::Upgrade {
program_id,
program_filepath,
max_retries,
solana_args,
} => {
eprintln!(
"Warning: 'anchor upgrade' is deprecated. Use 'anchor program upgrade' instead."
);
upgrade(
&opts.cfg_override,
program_id,
program_filepath,
max_retries,
solana_args,
)
}
Command::Idl { subcmd } => idl(&opts.cfg_override, subcmd),
Command::LegacyIdl { subcmd } => {
legacy_idl::handle_legacy_idl_command(&opts.cfg_override, subcmd)
}
Command::Migrate => migrate(&opts.cfg_override),
Command::Test {
program_name,
skip_deploy,
skip_local_validator,
skip_build,
no_idl,
detach,
run,
script,
validator,
profile,
args,
env,
cargo_args,
skip_lint,
} => test(
&opts.cfg_override,
program_name,
skip_deploy,
skip_local_validator,
skip_build,
skip_lint,
no_idl,
detach,
run,
script,
validator,
profile,
false,
args,
env,
cargo_args,
),
#[cfg(not(windows))]
Command::Debugger {
test_name,
skip_run,
skip_build,
skip_lint,
gdb,
cargo_args,
} => debugger(
&opts.cfg_override,
test_name,
skip_run,
skip_build,
skip_lint,
gdb,
cargo_args,
),
#[cfg(not(windows))]
Command::Coverage {
skip_run,
skip_build,
output,
trace_dir,
cargo_args,
} => run_coverage(
&opts.cfg_override,
skip_run,
skip_build,
&output,
&trace_dir,
cargo_args,
),
Command::Airdrop { amount, pubkey } => airdrop(&opts.cfg_override, amount, pubkey),
Command::Cluster { subcmd } => cluster(subcmd),
Command::Config { subcmd } => config_cmd(&opts.cfg_override, subcmd),
Command::Shell => shell(&opts.cfg_override),
Command::Run {
script,
script_args,
} => run(&opts.cfg_override, script, script_args),
Command::Keys { subcmd } => keys(&opts.cfg_override, subcmd),
Command::Localnet {
skip_build,
skip_deploy,
skip_lint,
ignore_keys,
validator,
env,
cargo_args,
} => localnet(
&opts.cfg_override,
skip_build,
skip_deploy,
skip_lint,
ignore_keys,
validator,
env,
cargo_args,
),
Command::Account {
account_type,
address,
idl,
} => account(&opts.cfg_override, account_type, address, idl),
Command::Completions { shell } => {
clap_complete::generate(
shell,
&mut Opts::command(),
"anchor",
&mut std::io::stdout(),
);
Ok(())
}
Command::Address => address(&opts.cfg_override),
Command::Balance { pubkey, lamports } => balance(&opts.cfg_override, pubkey, lamports),
Command::Epoch => epoch(&opts.cfg_override),
Command::EpochInfo => epoch_info(&opts.cfg_override),
Command::Logs {
include_votes,
address,
} => logs_subscribe(&opts.cfg_override, include_votes, address),
Command::ShowAccount { cmd } => account::show_account(&opts.cfg_override, cmd),
Command::Keygen { subcmd } => keygen::keygen(&opts.cfg_override, subcmd),
Command::Program { subcmd } => program::program(&opts.cfg_override, subcmd),
Command::Codama { subcmd } => codama::entry(subcmd),
}
}
fn reject_if_inside_cargo_project(start: PathBuf) -> Result<()> {
if let Some(parent) = Manifest::discover_from_path(start)? {
return Err(anyhow!(
"Cannot run `anchor init` inside an existing Cargo project at `{}`.\nTo add a new \
program to the existing project, run `anchor new <name>` from the workspace root. To \
create a fresh Anchor workspace, run `anchor init` outside any Cargo project tree.",
parent.path().display()
));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn init(
cfg_override: &ConfigOverride,
name: String,
javascript: bool,
no_install: bool,
package_manager: Option<PackageManager>,
no_git: bool,
template: ProgramTemplate,
anchor_version: AnchorVersion,
test_template: TestTemplate,
force: bool,
install_agent_skills: bool,
) -> Result<()> {
if !force {
if Config::discover(cfg_override)?.is_some() {
return Err(anyhow!("Workspace already initialized"));
}
reject_if_inside_cargo_project(std::env::current_dir()?)?;
}
let rust_name = name.to_snake_case();
let project_name = if name == rust_name {
rust_name.clone()
} else {
name.to_kebab_case()
};
let extra_keywords = ["async", "await", "try"];
if syn::parse_str::<syn::Ident>(&rust_name).is_err()
|| extra_keywords.contains(&rust_name.as_str())
{
return Err(anyhow!(
"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.",
));
}
if force {
fs::create_dir_all(&project_name)?;
} else {
fs::create_dir(&project_name)?;
}
std::env::set_current_dir(&project_name)?;
fs::create_dir_all("app")?;
let mut cfg = Config::default();
let uses_node = test_template.uses_node();
let package_manager = if uses_node {
Some(resolve_package_manager(package_manager)?)
} else {
None
};
let test_script = test_template.get_test_script(javascript, package_manager.as_ref());
cfg.scripts.insert("test".to_owned(), test_script);
if matches!(test_template, TestTemplate::Litesvm | TestTemplate::Mollusk) {
cfg.skip_local_validator = Some(true);
}
let package_manager_cmd = package_manager.as_ref().map(ToString::to_string);
if uses_node {
cfg.toolchain.package_manager = package_manager.clone();
}
fs::write(".gitignore", template::git_ignore())?;
fs::write(".prettierignore", template::prettier_ignore())?;
if force {
let default_program_dir = std::env::current_dir()?
.join("programs")
.join(&project_name);
if default_program_dir.exists() {
fs::remove_dir_all(default_program_dir)?;
}
}
template::create_program(
&project_name,
template,
Some(&test_template),
anchor_version,
)?;
let program_id = template::get_or_create_program_id(&rust_name, target_dir()?);
let mut localnet = BTreeMap::new();
localnet.insert(
rust_name,
ProgramDeployment {
address: program_id,
path: None,
idl: None,
},
);
cfg.programs.insert(Cluster::Localnet, localnet);
let toml = cfg.to_string();
fs::write("Anchor.toml", toml)?;
if uses_node {
let migrations_path = Path::new("migrations");
fs::create_dir_all(migrations_path)?;
let license = get_npm_init_license()?;
let jest = TestTemplate::Jest == test_template;
if javascript {
let mut package_json = File::create("package.json")?;
package_json
.write_all(template::package_json(jest, license, anchor_version).as_bytes())?;
let mut deploy = File::create(migrations_path.join("deploy.js"))?;
deploy.write_all(template::deploy_script().as_bytes())?;
} else {
let mut ts_config = File::create("tsconfig.json")?;
ts_config.write_all(template::ts_config(jest).as_bytes())?;
let mut ts_package_json = File::create("package.json")?;
ts_package_json
.write_all(template::ts_package_json(jest, license, anchor_version).as_bytes())?;
let mut deploy = File::create(migrations_path.join("deploy.ts"))?;
deploy.write_all(template::ts_deploy_script().as_bytes())?;
}
}
test_template.create_test_files(
&project_name,
javascript,
&program_id.to_string(),
anchor_version,
)?;
if !no_install && uses_node {
let package_manager_cmd =
package_manager_cmd.expect("Node templates resolve a package manager");
let output = install_node_modules(&package_manager_cmd)?;
if !output.status.success() {
return Err(anyhow!(
"`{package_manager_cmd} install` failed (exit code {:?}). Re-run with \
`--no-install` to keep the generated files without installing dependencies.",
output.status.code()
));
}
}
if !no_git {
let git_result = std::process::Command::new("git")
.arg("init")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("git init failed: {}", e))?;
if !git_result.status.success() {
eprintln!("Failed to automatically initialize a new git repository");
}
}
if install_agent_skills {
install_solana_skill();
}
println!("{project_name} initialized");
Ok(())
}
fn install_solana_skill() {
const SKILL_REPO: &str = "https://github.com/solana-foundation/solana-dev-skill";
const SKILL_NAME: &str = "solana-dev";
if home_dir().is_some_and(|home| {
home.join(".agents")
.join("skills")
.join(SKILL_NAME)
.exists()
}) {
return;
}
let project_path = Path::new(".agents").join("skills").join(SKILL_NAME);
if project_path.exists() {
return;
}
println!("Installing Solana dev skill for Agents from {SKILL_REPO}");
let status = std::process::Command::new("npx")
.args([
"--yes",
"skills@1.4.4",
"add",
SKILL_REPO,
"--skill",
"*",
"-y",
])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
match status {
Ok(s) if s.success() => {
println!("Solana dev skill installed successfully");
}
_ => {
eprintln!(
"Warning: Failed to install Solana dev skill. Install manually with:\n npx \
skills add {SKILL_REPO}"
);
}
}
}
const PACKAGE_MANAGER_WATERFALL: &[PackageManager] = &[
PackageManager::PNPM,
PackageManager::Yarn,
PackageManager::NPM,
];
fn package_manager_available(pm: &PackageManager) -> bool {
let cmd = pm.to_string();
let mut command = if cfg!(target_os = "windows") {
let mut command = std::process::Command::new("cmd");
command.arg(format!("/C {cmd} --version"));
command
} else {
let mut command = std::process::Command::new(&cmd);
command.arg("--version");
command
};
command
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn resolve_package_manager(explicit: Option<PackageManager>) -> Result<PackageManager> {
if let Some(pm) = explicit {
if !package_manager_available(&pm) {
return Err(anyhow!(
"`{pm}` was requested but is not on PATH. Install it or pick a different package \
manager with `--package-manager`."
));
}
return Ok(pm);
}
let mut skipped = Vec::new();
for candidate in PACKAGE_MANAGER_WATERFALL {
if package_manager_available(candidate) {
if !skipped.is_empty() {
let missing = skipped
.iter()
.map(|pm: &PackageManager| pm.to_string())
.collect::<Vec<_>>()
.join(", ");
eprintln!("warning: {missing} not found on PATH, using `{candidate}` instead");
}
return Ok(candidate.clone());
}
skipped.push(candidate.clone());
}
Err(anyhow!(
"No supported package manager found on PATH (tried pnpm, yarn, npm). Install one of them, \
or re-run with `--no-install`."
))
}
fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.arg(format!("/C {cmd} install"))
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e))
} else {
std::process::Command::new(cmd)
.arg("install")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{} install failed: {}", cmd, e))
}
}
fn new(
cfg_override: &ConfigOverride,
name: String,
template: ProgramTemplate,
anchor_version: AnchorVersion,
force: bool,
) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
match cfg.path().parent() {
None => {
println!("Unable to make new program");
}
Some(parent) => {
std::env::set_current_dir(parent)?;
let cluster = cfg.provider.cluster.clone();
let programs = cfg.programs.entry(cluster).or_default();
if programs.contains_key(&name) {
if !force {
return Err(anyhow!("Program already exists"));
}
fs::remove_dir_all(std::env::current_dir()?.join("programs").join(&name))?;
}
template::create_program(&name, template, None, anchor_version)?;
programs.insert(
name.clone(),
ProgramDeployment {
address: template::get_or_create_program_id(&name, target_dir()?),
path: None,
idl: None,
},
);
let toml = cfg.to_string();
fs::write("Anchor.toml", toml)?;
println!("Created new program.");
}
};
Ok(())
})?
}
pub type Files = Vec<(PathBuf, String)>;
pub fn create_files(files: &Files) -> Result<()> {
for (path, content) in files {
let path = path
.display()
.to_string()
.replace('/', std::path::MAIN_SEPARATOR_STR);
let path = Path::new(&path);
if path.exists() {
continue;
}
match path.extension() {
Some(_) => {
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, content)?;
}
None => fs::create_dir_all(path)?,
}
}
Ok(())
}
pub fn override_or_create_files(files: &Files) -> Result<()> {
for (path, content) in files {
let path = Path::new(path);
if path.exists() {
let mut f = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(path)?;
f.write_all(content.as_bytes())?;
f.flush()?;
} else {
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, content)?;
}
}
Ok(())
}
pub fn expand(
cfg_override: &ConfigOverride,
program_name: Option<String>,
stdout: bool,
cargo_args: &[String],
) -> Result<()> {
if let Some(program_name) = program_name.as_ref() {
cd_member(cfg_override, program_name)?;
}
let workspace_cfg = Config::discover(cfg_override)?
.ok_or_else(|| anyhow!("The 'anchor expand' command requires an Anchor workspace."))?;
let cfg_parent = workspace_cfg.path().parent().expect("Invalid Anchor.toml");
let cargo = Manifest::discover()?;
let expansions_path = cfg_parent.join(".anchor").join("expanded-macros");
fs::create_dir_all(&expansions_path)?;
match cargo {
None => expand_all(&workspace_cfg, expansions_path, stdout, cargo_args),
Some(cargo) if cargo.path().parent() == workspace_cfg.path().parent() => {
expand_all(&workspace_cfg, expansions_path, stdout, cargo_args)
}
Some(cargo) => expand_program(
cargo.path().parent().unwrap().to_path_buf(),
expansions_path,
stdout,
cargo_args,
),
}
}
fn expand_all(
workspace_cfg: &WithPath<Config>,
expansions_path: PathBuf,
stdout: bool,
cargo_args: &[String],
) -> Result<()> {
let cur_dir = std::env::current_dir()?;
for p in workspace_cfg.get_program_list()? {
expand_program(p, expansions_path.clone(), stdout, cargo_args)?;
}
std::env::set_current_dir(cur_dir)?;
Ok(())
}
fn expand_program(
program_path: PathBuf,
expansions_path: PathBuf,
stdout: bool,
cargo_args: &[String],
) -> Result<()> {
let cargo = Manifest::from_path(program_path.join("Cargo.toml"))
.map_err(|_| anyhow!("Could not find Cargo.toml for program"))?;
let package_name = &cargo
.package
.as_ref()
.ok_or_else(|| anyhow!("Cargo config is missing a package"))?
.name;
let mut cmd = std::process::Command::new("cargo");
cmd.arg("expand")
.arg("--target-dir")
.arg(expansions_path.join("expand-target"))
.arg("--package")
.arg(package_name)
.args(cargo_args);
let handle_err = |err| anyhow!("Failed to run `cargo expand`: {err}");
let exit_on_err = |exit_status: ExitStatus| {
if !exit_status.success() {
eprintln!("'anchor expand' failed. Perhaps you have not installed 'cargo-expand'? https://github.com/dtolnay/cargo-expand#installation");
std::process::exit(exit_status.code().unwrap_or(1));
}
};
if stdout {
let status = cmd.status().map_err(handle_err)?;
exit_on_err(status);
} else {
let output = cmd.stderr(Stdio::inherit()).output().map_err(handle_err)?;
exit_on_err(output.status);
let program_expansions_path = expansions_path.join(package_name);
fs::create_dir_all(&program_expansions_path)?;
let version = cargo.version();
let time = chrono::Utc::now().to_string().replace(' ', "_");
let file_path = program_expansions_path.join(format!("{package_name}-{version}-{time}.rs"));
fs::write(&file_path, &output.stdout)?;
println!(
"Expanded {} into file {}\n",
package_name,
file_path.to_string_lossy()
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn build(
cfg_override: &ConfigOverride,
no_idl: bool,
idl: Option<String>,
idl_ts: Option<String>,
verifiable: bool,
skip_lint: bool,
ignore_keys: bool,
program_name: Option<String>,
solana_version: Option<String>,
docker_image: Option<String>,
bootstrap: BootstrapMode,
stdout: Option<File>, stderr: Option<File>, env_vars: Vec<String>,
cargo_args: Vec<String>,
no_docs: bool,
) -> Result<()> {
if let Some(program_name) = program_name.as_ref() {
cd_member(cfg_override, program_name)?;
}
let cfg = Config::discover(cfg_override)?
.ok_or_else(|| anyhow!("The 'anchor build' command requires an Anchor workspace."))?;
let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
let workspace_cargo_toml_path = cfg_parent.join("Cargo.toml");
if workspace_cargo_toml_path.exists() {
check_overflow(workspace_cargo_toml_path)?;
}
check_anchor_version(&cfg).ok();
check_deps(&cfg).ok();
if !ignore_keys {
if let ProgramIdComparison::Mismatch {
lib_name,
actual_id,
declared_id,
} = check_program_id_mismatch(&cfg, program_name.clone())?
{
eprintln!(
"Program ID mismatch detected for program '{lib_name}':\n Keypair file has: \
{actual_id}\n Source code has: {declared_id}\n\nPlease run 'anchor keys sync' \
to update the program ID in your source code or use the '--ignore-keys' flag to \
skip this check.",
);
}
}
let idl_out = match idl {
Some(idl) => Some(PathBuf::from(idl)),
None => Some(target_dir()?.join("idl")),
};
fs::create_dir_all(idl_out.as_ref().unwrap())?;
let idl_ts_out = match idl_ts {
Some(idl_ts) => Some(PathBuf::from(idl_ts)),
None => Some(target_dir()?.join("types")),
};
fs::create_dir_all(idl_ts_out.as_ref().unwrap())?;
if !cfg.workspace.idls.is_empty() {
fs::create_dir_all(cfg_parent.join(&cfg.workspace.idls))?;
};
if !cfg.workspace.types.is_empty() {
fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
};
cfg.run_hooks(HookType::PreBuild)?;
let cargo = Manifest::discover()?;
let build_config = BuildConfig {
verifiable,
solana_version: solana_version.or_else(|| cfg.toolchain.solana_version.clone()),
docker_image: docker_image.unwrap_or_else(|| cfg.docker()),
bootstrap,
};
let built_idl_paths = match cargo {
None => build_all(
&cfg,
cfg.path(),
no_idl,
idl_out.clone(),
idl_ts_out.clone(),
&build_config,
stdout,
stderr,
env_vars,
cargo_args,
skip_lint,
no_docs,
)?,
Some(cargo) if cargo.path().parent() == cfg.path().parent() => build_all(
&cfg,
cfg.path(),
no_idl,
idl_out.clone(),
idl_ts_out.clone(),
&build_config,
stdout,
stderr,
env_vars,
cargo_args,
skip_lint,
no_docs,
)?,
Some(cargo) => build_cwd(
&cfg,
cargo.path().to_path_buf(),
no_idl,
idl_out.clone(),
idl_ts_out.clone(),
&build_config,
stdout,
stderr,
env_vars,
cargo_args,
skip_lint,
no_docs,
)?,
};
cfg.run_hooks(HookType::PostBuild)?;
if cfg.clients.auto && !no_idl {
codama::auto_generate_for_workspace(&cfg.clients, cfg_parent, &built_idl_paths)?;
}
set_workspace_dir_or_exit();
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn build_all(
cfg: &WithPath<Config>,
cfg_path: &Path,
no_idl: bool,
idl_out: Option<PathBuf>,
idl_ts_out: Option<PathBuf>,
build_config: &BuildConfig,
stdout: Option<File>, stderr: Option<File>, env_vars: Vec<String>,
cargo_args: Vec<String>,
skip_lint: bool,
no_docs: bool,
) -> Result<Vec<PathBuf>> {
let cur_dir = std::env::current_dir()?;
let r = (|| -> Result<Vec<PathBuf>> {
match cfg_path.parent() {
None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())),
Some(_parent) => {
let mut idl_paths = Vec::new();
for p in get_metadata_ordered_program_list(cfg)? {
idl_paths.extend(build_cwd(
cfg,
p.join("Cargo.toml"),
no_idl,
idl_out.clone(),
idl_ts_out.clone(),
build_config,
stdout.as_ref().map(|f| f.try_clone()).transpose()?,
stderr.as_ref().map(|f| f.try_clone()).transpose()?,
env_vars.clone(),
cargo_args.clone(),
skip_lint,
no_docs,
)?);
}
Ok(idl_paths)
}
}
})();
std::env::set_current_dir(cur_dir)?;
r
}
fn get_metadata_ordered_program_list(cfg: &WithPath<Config>) -> Result<Vec<PathBuf>> {
let programs = cfg.get_program_list()?;
let ordered = order_programs_by_metadata(cfg, &programs);
Ok(ordered.unwrap_or(programs))
}
fn order_programs_by_metadata(
cfg: &WithPath<Config>,
programs: &[PathBuf],
) -> Result<Vec<PathBuf>> {
let workspace_dir = cfg
.path()
.parent()
.ok_or_else(|| anyhow!("Invalid Anchor.toml at {}", cfg.path().display()))?;
let metadata = MetadataCommand::new()
.current_dir(workspace_dir)
.exec()
.context("Failed to run `cargo metadata`")?;
let mut package_dirs = HashMap::new();
for (idx, package) in metadata.packages.iter().enumerate() {
if package.source.is_some() {
continue;
}
let manifest_path = package.manifest_path.clone().into_std_path_buf();
if let Some(package_dir) = manifest_path.parent() {
if let Ok(package_dir) = package_dir.canonicalize() {
package_dirs.insert(package_dir, idx);
}
}
}
let program_indices = programs
.iter()
.filter_map(|program| package_dirs.get(program).copied())
.collect::<HashSet<_>>();
if program_indices.len() != programs.len() {
bail!("Failed to match all Anchor programs in `cargo metadata`");
}
let mut local_deps = vec![Vec::new(); metadata.packages.len()];
for (idx, package) in metadata.packages.iter().enumerate() {
for dep in &package.dependencies {
if dep.kind == DependencyKind::Development {
continue;
}
let Some(dep_path) = dep.path.as_ref() else {
continue;
};
if let Ok(dep_path) = dep_path.clone().into_std_path_buf().canonicalize() {
if let Some(dep_idx) = package_dirs.get(&dep_path) {
local_deps[idx].push(*dep_idx);
}
}
}
}
let mut program_closures = HashMap::new();
for idx in &program_indices {
program_closures.insert(*idx, local_dependency_closure(*idx, &local_deps));
}
let original_order_by_package = programs
.iter()
.enumerate()
.map(|(idx, program)| (package_dirs[program], idx))
.collect::<HashMap<_, _>>();
let program_by_index = programs
.iter()
.map(|program| (package_dirs[program], program.clone()))
.collect::<HashMap<_, _>>();
let ordered = order_program_indices_by_dependency_cache_heuristic(
&program_indices,
&program_closures,
&original_order_by_package,
)
.into_iter()
.map(|idx| program_by_index[&idx].clone())
.collect();
Ok(ordered)
}
fn order_program_indices_by_dependency_cache_heuristic(
program_indices: &HashSet<usize>,
program_closures: &HashMap<usize, HashSet<usize>>,
original_order: &HashMap<usize, usize>,
) -> Vec<usize> {
let mut reverse_dependents = HashMap::new();
for idx in program_indices {
reverse_dependents.insert(*idx, 0usize);
}
for (program_idx, deps) in program_closures {
for dep_idx in deps {
if program_indices.contains(dep_idx) && dep_idx != program_idx {
*reverse_dependents.entry(*dep_idx).or_default() += 1;
}
}
}
let mut ordered = program_indices.iter().copied().collect::<Vec<_>>();
ordered.sort_by(|a, b| {
let a_deps = &program_closures[a];
let b_deps = &program_closures[b];
let a_program_deps = a_deps
.iter()
.filter(|idx| program_indices.contains(idx))
.count();
let b_program_deps = b_deps
.iter()
.filter(|idx| program_indices.contains(idx))
.count();
let a_reverse = reverse_dependents[a];
let b_reverse = reverse_dependents[b];
let a_isolated = a_program_deps == 0 && a_reverse == 0;
let b_isolated = b_program_deps == 0 && b_reverse == 0;
b_isolated
.cmp(&a_isolated)
.then_with(|| b_program_deps.cmp(&a_program_deps))
.then_with(|| b_deps.len().cmp(&a_deps.len()))
.then_with(|| a_reverse.cmp(&b_reverse))
.then_with(|| original_order[a].cmp(&original_order[b]))
});
ordered
}
fn local_dependency_closure(start: usize, deps: &[Vec<usize>]) -> HashSet<usize> {
let mut seen = HashSet::new();
let mut stack = deps[start].clone();
while let Some(idx) = stack.pop() {
if seen.insert(idx) {
stack.extend(deps[idx].iter().copied());
}
}
seen
}
#[allow(clippy::too_many_arguments)]
fn build_cwd(
cfg: &WithPath<Config>,
cargo_toml: PathBuf,
no_idl: bool,
idl_out: Option<PathBuf>,
idl_ts_out: Option<PathBuf>,
build_config: &BuildConfig,
stdout: Option<File>,
stderr: Option<File>,
env_vars: Vec<String>,
cargo_args: Vec<String>,
skip_lint: bool,
no_docs: bool,
) -> Result<Vec<PathBuf>> {
match cargo_toml.parent() {
None => return Err(anyhow!("Unable to find parent")),
Some(p) => std::env::set_current_dir(p)?,
};
match build_config.verifiable {
false => _build_cwd(
cfg, no_idl, idl_out, idl_ts_out, skip_lint, no_docs, cargo_args,
),
true => build_cwd_verifiable(
cfg,
cargo_toml,
build_config,
stdout,
stderr,
skip_lint,
env_vars,
cargo_args,
no_docs,
),
}
}
#[allow(clippy::too_many_arguments)]
fn build_cwd_verifiable(
cfg: &WithPath<Config>,
cargo_toml: PathBuf,
build_config: &BuildConfig,
stdout: Option<File>,
stderr: Option<File>,
skip_lint: bool,
env_vars: Vec<String>,
cargo_args: Vec<String>,
no_docs: bool,
) -> Result<Vec<PathBuf>> {
let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
let target_dir = target_dir()?;
fs::create_dir_all(target_dir.join("verifiable"))?;
fs::create_dir_all(target_dir.join("idl"))?;
fs::create_dir_all(target_dir.join("types"))?;
if !&cfg.workspace.idls.is_empty() {
fs::create_dir_all(workspace_dir.join(&cfg.workspace.idls))?;
}
if !&cfg.workspace.types.is_empty() {
fs::create_dir_all(workspace_dir.join(&cfg.workspace.types))?;
}
let container_name = "anchor-program";
let result = docker_build(
cfg,
container_name,
cargo_toml,
build_config,
stdout,
stderr,
env_vars,
cargo_args.clone(),
);
match result {
Err(e) => {
eprintln!("Error during Docker build: {e:?}");
Err(e)
}
Ok(_) => {
println!("Extracting the IDL");
let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
println!("Writing the IDL file");
let out_file = target_dir
.join("idl")
.join(&idl.metadata.name)
.with_extension("json");
write_idl(&idl, OutFile::File(out_file.clone()))?;
if !&cfg.workspace.idls.is_empty() {
write_idl(
&idl,
OutFile::File(
workspace_dir
.join(&cfg.workspace.idls)
.join(&idl.metadata.name)
.with_extension("json"),
),
)?;
}
println!("Writing the .ts file");
let ts_file = target_dir
.join("types")
.join(&idl.metadata.name)
.with_extension("ts");
fs::write(&ts_file, idl_ts(&idl)?)?;
if !&cfg.workspace.types.is_empty() {
fs::copy(
ts_file,
workspace_dir
.join(&cfg.workspace.types)
.join(idl.metadata.name)
.with_extension("ts"),
)?;
}
println!("Build success");
Ok(vec![out_file])
}
}
}
#[allow(clippy::too_many_arguments)]
fn docker_build(
cfg: &WithPath<Config>,
container_name: &str,
cargo_toml: PathBuf,
build_config: &BuildConfig,
stdout: Option<File>,
stderr: Option<File>,
env_vars: Vec<String>,
cargo_args: Vec<String>,
) -> Result<()> {
let binary_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
let workdir = Path::new("/workdir");
let volume_mount = format!(
"{}:{}",
cfg.path().parent().unwrap().canonicalize()?.display(),
workdir.to_str().unwrap(),
);
println!("Using image {:?}", build_config.docker_image);
let target_dir = workdir.join("docker-target");
println!("Run docker image");
let exit = std::process::Command::new("docker")
.args([
"run",
"-it",
"-d",
"--name",
container_name,
"--env",
&format!(
"CARGO_TARGET_DIR={}",
target_dir.as_path().to_str().unwrap()
),
"-v",
&volume_mount,
"-w",
workdir.to_str().unwrap(),
&build_config.docker_image,
"bash",
])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("Docker build failed: {}", e))?;
if !exit.status.success() {
return Err(anyhow!("Failed to build program"));
}
let result = docker_prep(container_name, build_config).and_then(|_| {
let cfg_parent = cfg.path().parent().unwrap();
docker_build_bpf(
container_name,
cargo_toml.as_path(),
cfg_parent,
target_dir.as_path(),
binary_name,
stdout,
stderr,
env_vars,
cargo_args,
)
});
docker_cleanup(container_name, target_dir.as_path())?;
result
}
fn docker_prep(container_name: &str, build_config: &BuildConfig) -> Result<()> {
match build_config.bootstrap {
BootstrapMode::Debian => {
docker_exec(container_name, &["apt", "update"])?;
docker_exec(
container_name,
&["apt", "install", "-y", "curl", "build-essential"],
)?;
docker_exec(
container_name,
&["curl", "https://sh.rustup.rs", "-sfo", "rustup.sh"],
)?;
docker_exec(container_name, &["sh", "rustup.sh", "-y"])?;
docker_exec(container_name, &["rm", "-f", "rustup.sh"])?;
}
BootstrapMode::None => {}
}
if let Some(solana_version) = &build_config.solana_version {
println!("Using solana version: {solana_version}");
docker_exec(
container_name,
&[
"curl",
"-sSfL",
&format!("https://release.anza.xyz/v{solana_version}/install",),
"-o",
"solana_installer.sh",
],
)?;
docker_exec(container_name, &["sh", "solana_installer.sh"])?;
docker_exec(container_name, &["rm", "-f", "solana_installer.sh"])?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn docker_build_bpf(
container_name: &str,
cargo_toml: &Path,
cfg_parent: &Path,
target_dir: &Path,
binary_name: String,
stdout: Option<File>,
stderr: Option<File>,
env_vars: Vec<String>,
cargo_args: Vec<String>,
) -> Result<()> {
let manifest_path =
pathdiff::diff_paths(cargo_toml.canonicalize()?, cfg_parent.canonicalize()?)
.ok_or_else(|| anyhow!("Unable to diff paths"))?;
println!(
"Building {} manifest: {:?}",
binary_name,
manifest_path.display()
);
let exit = std::process::Command::new("docker")
.args([
"exec",
"--env",
"PATH=/root/.local/share/solana/install/active_release/bin:/root/.cargo/bin:/usr/\
local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
])
.args(
env_vars
.iter()
.map(|x| ["--env", x.as_str()])
.collect::<Vec<[&str; 2]>>()
.concat(),
)
.args([container_name, "cargo"])
.args(BUILD_SUBCOMMAND)
.args(["--manifest-path", &manifest_path.display().to_string()])
.args(cargo_args)
.stdout(match stdout {
None => Stdio::inherit(),
Some(f) => f.into(),
})
.stderr(match stderr {
None => Stdio::inherit(),
Some(f) => f.into(),
})
.output()
.map_err(|e| anyhow::format_err!("Docker build failed: {}", e))?;
if !exit.status.success() {
return Err(anyhow!("Failed to build program"));
}
println!("Copying out the build artifacts");
let out_file = crate::target_dir()?
.join("verifiable")
.join(&binary_name)
.with_extension("so")
.display()
.to_string();
let mut bin_path = target_dir.join("deploy");
bin_path.push(format!("{binary_name}.so"));
let bin_artifact = format!(
"{}:{}",
container_name,
bin_path.as_path().to_str().unwrap()
);
let exit = std::process::Command::new("docker")
.args(["cp", &bin_artifact, &out_file])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{}", e))?;
if !exit.status.success() {
Err(anyhow!(
"Failed to copy binary out of docker. Is the target directory set correctly?"
))
} else {
Ok(())
}
}
fn docker_cleanup(container_name: &str, target_dir: &Path) -> Result<()> {
println!("Cleaning up the docker target directory");
docker_exec(container_name, &["rm", "-rf", target_dir.to_str().unwrap()])?;
println!("Removing the docker container");
let exit = std::process::Command::new("docker")
.args(["rm", "-f", container_name])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{}", e))?;
if !exit.status.success() {
println!("Unable to remove the docker container");
std::process::exit(exit.status.code().unwrap_or(1));
}
Ok(())
}
fn docker_exec(container_name: &str, args: &[&str]) -> Result<()> {
let exit = std::process::Command::new("docker")
.args([&["exec", container_name], args].concat())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow!("Failed to run command \"{:?}\": {:?}", args, e))?;
if !exit.status.success() {
Err(anyhow!("Failed to run command: {:?}", args))
} else {
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
fn _build_cwd(
cfg: &WithPath<Config>,
no_idl: bool,
idl_out: Option<PathBuf>,
idl_ts_out: Option<PathBuf>,
skip_lint: bool,
no_docs: bool,
cargo_args: Vec<String>,
) -> Result<Vec<PathBuf>> {
let exit = std::process::Command::new("cargo")
.args(BUILD_SUBCOMMAND)
.args(cargo_args.clone())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{}", e))?;
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
if !no_idl {
let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
let out = match idl_out {
None => PathBuf::from(".")
.join(&idl.metadata.name)
.with_extension("json"),
Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("json")),
};
let ts_out = match idl_ts_out {
None => PathBuf::from(".")
.join(&idl.metadata.name)
.with_extension("ts"),
Some(o) => PathBuf::from(&o.join(&idl.metadata.name).with_extension("ts")),
};
write_idl(&idl, OutFile::File(out.clone()))?;
if !&cfg.workspace.idls.is_empty() {
write_idl(
&idl,
OutFile::File(
cfg_parent
.join(&cfg.workspace.idls)
.join(&idl.metadata.name)
.with_extension("json"),
),
)?;
}
fs::write(&ts_out, idl_ts(&idl)?)?;
if !&cfg.workspace.types.is_empty() {
fs::copy(
&ts_out,
cfg_parent
.join(&cfg.workspace.types)
.join(&idl.metadata.name)
.with_extension("ts"),
)?;
}
Ok(vec![out])
} else {
Ok(Vec::new())
}
}
const BUILD_SUBCOMMAND: &[&str] = &["build-sbf", "--tools-version", "v1.52"];
pub fn cargo_build_sbf(cwd: Option<&Path>, extra_args: &[String]) -> Result<()> {
let mut cmd = std::process::Command::new("cargo");
if let Some(d) = cwd {
cmd.current_dir(d);
}
let status = cmd
.args(BUILD_SUBCOMMAND)
.args(extra_args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("running cargo build-sbf")?;
if !status.success() {
return Err(anyhow!(
"`cargo {}` failed with status {status}",
BUILD_SUBCOMMAND.join(" ")
));
}
Ok(())
}
pub fn verify(
program_id: Pubkey,
repo_url: Option<String>,
commit_hash: Option<String>,
current_dir: bool,
program_name: Option<String>,
args: Vec<String>,
) -> Result<()> {
let mut command_args = Vec::new();
match (current_dir, repo_url) {
(true, _) => {
let current_path = std::env::current_dir()?
.to_str()
.ok_or_else(|| anyhow!("Invalid current directory path"))?
.to_owned();
command_args.push(current_path);
command_args.push("--current-dir".into());
}
(false, Some(url)) => {
command_args.push(url);
}
(false, None) => {
return Err(anyhow!(
"You must provide either --repo-url or --current-dir"
));
}
}
if let Some(commit) = commit_hash {
command_args.push("--commit-hash".into());
command_args.push(commit);
}
if let Some(name) = program_name {
command_args.push("--library-name".into());
command_args.push(name);
}
command_args.push("--program-id".into());
command_args.push(program_id.to_string());
command_args.extend(args);
println!("Verifying program {program_id}");
let verify_path = AVM_HOME.join("bin").join("solana-verify");
if !verify_path.exists() {
install_with_avm(env!("CARGO_PKG_VERSION"), true)
.context("installing Anchor with solana-verify")?;
}
let status = std::process::Command::new(verify_path)
.arg("verify-from-repo")
.args(&command_args)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.with_context(|| "Failed to run `solana-verify`")?;
if !status.success() {
return Err(anyhow!("Failed to verify program"));
}
Ok(())
}
fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> {
let programs = program::get_programs_from_workspace(cfg_override, None)?;
for program in programs {
let cargo_toml = program.path.join("Cargo.toml");
if !cargo_toml.exists() {
return Err(anyhow!(
"Did not find Cargo.toml at the path: {}",
program.path.display()
));
}
let manifest = Manifest::from_path(&cargo_toml)?;
let pkg_name = manifest.package().name();
if program_name == pkg_name || program_name == program.lib_name {
std::env::set_current_dir(&program.path)?;
return Ok(());
}
}
Err(anyhow!("{} is not part of the workspace", program_name,))
}
fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
match subcmd {
IdlCommand::Init {
program_id,
filepath,
priority_fee,
non_canonical,
#[cfg(feature = "idl-localnet-testing")]
allow_localnet,
} => {
#[cfg(feature = "idl-localnet-testing")]
let allow_localnet = allow_localnet;
#[cfg(not(feature = "idl-localnet-testing"))]
let allow_localnet = false;
idl_init(
program_id,
cfg_override,
filepath,
priority_fee,
non_canonical,
allow_localnet,
)
}
IdlCommand::Upgrade {
program_id,
filepath,
priority_fee,
#[cfg(feature = "idl-localnet-testing")]
allow_localnet,
} => {
#[cfg(feature = "idl-localnet-testing")]
let allow_localnet = allow_localnet;
#[cfg(not(feature = "idl-localnet-testing"))]
let allow_localnet = false;
idl_upgrade(
program_id,
cfg_override,
filepath,
priority_fee,
allow_localnet,
)
}
IdlCommand::Build {
program_name,
out,
out_ts,
no_docs,
skip_lint,
cargo_args,
} => idl_build(
cfg_override,
program_name,
out,
out_ts,
no_docs,
skip_lint,
cargo_args,
),
IdlCommand::Fetch {
program_id: address,
out,
non_canonical,
} => idl_fetch(cfg_override, address, out, non_canonical),
IdlCommand::FetchHistorical {
program_id: address,
authority,
slot,
before,
after,
out_dir,
rpc_workers,
no_parallel,
rpc_max_retries,
rpc_retry_backoff_ms,
max_signatures,
verbose,
} => fetch::idl_fetch_historical(
cfg_override,
address,
authority,
slot,
before,
after,
out_dir,
fetch::FetchTuning {
workers: rpc_workers,
no_parallel,
max_retries: rpc_max_retries,
retry_backoff_ms: rpc_retry_backoff_ms,
max_signatures,
verbose,
},
),
IdlCommand::Convert {
path,
out,
program_id,
to_legacy,
} => idl_convert(path, out, program_id, to_legacy),
IdlCommand::Type { path, out } => idl_type(path, out),
IdlCommand::Close {
program_id,
seed,
priority_fee,
} => idl_close_metadata(cfg_override, program_id, seed, priority_fee),
IdlCommand::CreateBuffer {
filepath,
priority_fee,
} => idl_create_buffer(cfg_override, filepath, priority_fee),
IdlCommand::SetBufferAuthority {
buffer,
new_authority,
priority_fee,
} => idl_set_buffer_authority(cfg_override, buffer, new_authority, priority_fee),
IdlCommand::WriteBuffer {
program_id,
buffer,
seed,
close_buffer,
priority_fee,
} => idl_write_buffer_metadata(
cfg_override,
program_id,
buffer,
seed,
close_buffer,
priority_fee,
),
}
}
fn idl_init(
program_id: Option<Pubkey>,
cfg_override: &ConfigOverride,
idl_filepath: PathBuf,
priority_fee: Option<u64>,
non_canonical: bool,
allow_localnet: bool,
) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
if is_localnet && !allow_localnet {
#[cfg(feature = "idl-localnet-testing")]
println!(
"Skipping IDL initialization on localnet. To deploy on localnet, use --allow-localnet"
);
#[cfg(not(feature = "idl-localnet-testing"))]
println!("Skipping IDL initialization on localnet");
return Ok(());
}
let program_id = match program_id {
Some(id) => id.to_string(),
_ => {
let idl = fs::read(&idl_filepath)?;
let idl = convert_idl(&idl)?;
idl.address
}
};
let command = metadata::IdlCommand::funded(
cluster_url,
wallet_path,
priority_fee,
metadata::FundedIdlSubcommand::Write {
program_id,
idl_filepath: idl_filepath
.to_str()
.ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
.to_string(),
non_canonical,
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to initialize IDL"));
}
println!("IDL initialized.");
Ok(())
}
fn idl_upgrade(
program_id: Option<Pubkey>,
cfg_override: &ConfigOverride,
idl_filepath: PathBuf,
priority_fee: Option<u64>,
allow_localnet: bool,
) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
if is_localnet && !allow_localnet {
#[cfg(feature = "idl-localnet-testing")]
println!("Skipping IDL upgrade on localnet. To deploy on localnet, use --allow-localnet");
#[cfg(not(feature = "idl-localnet-testing"))]
println!("Skipping IDL upgrade on localnet");
return Ok(());
}
let program_id = match program_id {
Some(id) => id.to_string(),
_ => {
let idl = fs::read(&idl_filepath)?;
let idl = convert_idl(&idl)?;
idl.address
}
};
let command = metadata::IdlCommand::funded(
cluster_url,
wallet_path,
priority_fee,
metadata::FundedIdlSubcommand::Write {
program_id,
idl_filepath: idl_filepath
.to_str()
.ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
.to_string(),
non_canonical: false,
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to upgrade IDL"));
}
println!("IDL upgraded.");
Ok(())
}
fn idl_build(
cfg_override: &ConfigOverride,
program_name: Option<String>,
out: Option<String>,
out_ts: Option<String>,
no_docs: bool,
skip_lint: bool,
cargo_args: Vec<String>,
) -> Result<()> {
let cfg = Config::discover(cfg_override)?
.ok_or_else(|| anyhow!("The 'anchor idl build' command requires an Anchor workspace."))?;
let current_dir = std::env::current_dir()?;
let program_path = match program_name {
Some(name) => cfg.get_program(&name)?.path,
None => {
let programs = cfg.read_all_programs()?;
if programs.len() == 1 {
programs.into_iter().next().unwrap().path
} else {
programs
.into_iter()
.find(|program| program.path == current_dir)
.ok_or_else(|| anyhow!("Not in a program directory"))?
.path
}
}
};
std::env::set_current_dir(program_path)?;
let idl = generate_idl(&cfg, skip_lint, no_docs, &cargo_args)?;
std::env::set_current_dir(current_dir)?;
let out = match out {
Some(path) => OutFile::File(PathBuf::from(path)),
None => OutFile::Stdout,
};
write_idl(&idl, out)?;
if let Some(path) = out_ts {
fs::write(path, idl_ts(&idl)?)?;
}
Ok(())
}
fn generate_idl(
cfg: &WithPath<Config>,
skip_lint: bool,
no_docs: bool,
cargo_args: &[String],
) -> Result<Idl> {
check_idl_build_feature()?;
let idl = anchor_lang_idl::build::IdlBuilder::new()
.resolution(cfg.features.resolution)
.skip_lint(cfg.features.skip_lint || skip_lint)
.no_docs(no_docs)
.cargo_args(cargo_args.into())
.build()?;
idl.instructions
.iter()
.filter(|ix| anchor_lang::event::EVENT_IX_TAG_LE.starts_with(&ix.discriminator))
.for_each(|ix| {
eprintln!(
"Warning: Instruction conflicts with `event-cpi` instruction discriminator: `{}`",
ix.name
);
});
Ok(idl)
}
fn idl_fetch(
cfg_override: &ConfigOverride,
address: Pubkey,
out: Option<String>,
non_canonical: bool,
) -> Result<()> {
let (cluster_url, _) = get_cluster_and_wallet(cfg_override)?;
let command = metadata::IdlCommand::unfunded(
cluster_url,
metadata::UnfundedIdlSubcommand::Fetch {
program_id: address.to_string(),
out,
non_canonical,
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to fetch IDL"));
}
Ok(())
}
fn apply_program_id_override(idl: &[u8], program_id: Pubkey) -> Result<Vec<u8>> {
let mut idl = serde_json::from_slice::<serde_json::Value>(idl)?;
let obj = idl
.as_object_mut()
.ok_or_else(|| anyhow!("IDL must be an object"))?;
let pid = program_id.to_string();
let is_current_spec = obj.get("metadata").and_then(|m| m.get("spec")).is_some();
if is_current_spec {
obj.insert("address".into(), serde_json::Value::String(pid));
} else {
match obj.get_mut("metadata") {
Some(serde_json::Value::Object(m)) => {
m.insert("address".into(), serde_json::Value::String(pid));
}
_ => {
obj.insert("metadata".into(), serde_json::json!({ "address": pid }));
}
}
}
serde_json::to_vec(&idl).map_err(Into::into)
}
fn idl_convert(
path: PathBuf,
out: Option<PathBuf>,
program_id: Option<Pubkey>,
to_legacy: bool,
) -> Result<()> {
let idl = fs::read(path)?;
let idl = match program_id {
Some(program_id) => apply_program_id_override(&idl, program_id)?,
None => idl,
};
let parsed = convert_idl(&idl)?;
let out = match out {
None => OutFile::Stdout,
Some(out) => OutFile::File(out),
};
if to_legacy {
let bytes = convert_idl_to_legacy(&parsed)?;
match out {
OutFile::Stdout => {
let s =
std::str::from_utf8(&bytes).context("legacy IDL JSON was not valid UTF-8")?;
println!("{s}");
Ok(())
}
OutFile::File(path) => fs::write(path, bytes).map_err(Into::into),
}
} else {
write_idl(&parsed, out)
}
}
fn idl_type(path: PathBuf, out: Option<PathBuf>) -> Result<()> {
let idl = fs::read(path)?;
let idl = convert_idl(&idl)?;
let types = idl_ts(&idl)?;
match out {
Some(out) => fs::write(out, types)?,
_ => println!("{types}"),
};
Ok(())
}
fn idl_close_metadata(
cfg_override: &ConfigOverride,
program_id: Pubkey,
seed: String,
priority_fee: Option<u64>,
) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let command = metadata::IdlCommand::funded(
cluster_url,
wallet_path,
priority_fee,
metadata::FundedIdlSubcommand::Close {
program_id: program_id.to_string(),
seed,
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to close metadata account"));
}
println!("Metadata account closed successfully.");
Ok(())
}
fn idl_create_buffer(
cfg_override: &ConfigOverride,
filepath: PathBuf,
priority_fee: Option<u64>,
) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let command = metadata::IdlCommand::funded(
cluster_url,
wallet_path,
priority_fee,
metadata::FundedIdlSubcommand::CreateBuffer {
filepath: filepath
.to_str()
.ok_or_else(|| anyhow!("IDL filepath is not valid UTF-8"))?
.to_string(),
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to create buffer"));
}
println!("Buffer created successfully.");
Ok(())
}
fn idl_set_buffer_authority(
cfg_override: &ConfigOverride,
buffer: Pubkey,
new_authority: Pubkey,
priority_fee: Option<u64>,
) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let command = metadata::IdlCommand::funded(
cluster_url,
wallet_path,
priority_fee,
metadata::FundedIdlSubcommand::SetBufferAuthority {
buffer: buffer.to_string(),
new_authority: new_authority.to_string(),
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to set buffer authority"));
}
println!("Buffer authority set successfully.");
Ok(())
}
fn idl_write_buffer_metadata(
cfg_override: &ConfigOverride,
program_id: Pubkey,
buffer: Pubkey,
seed: String,
close_buffer: bool,
priority_fee: Option<u64>,
) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let command = metadata::IdlCommand::funded(
cluster_url,
wallet_path,
priority_fee,
metadata::FundedIdlSubcommand::WriteBuffer {
program_id: program_id.to_string(),
buffer: buffer.to_string(),
seed,
close_buffer,
},
);
if !command.status()?.success() {
return Err(anyhow!("Failed to write metadata using buffer"));
}
println!("Metadata written successfully using buffer.");
Ok(())
}
fn idl_ts(idl: &Idl) -> Result<String> {
let idl_name = &idl.metadata.name;
let type_name = idl_name.to_pascal_case();
let mut camel_idl = serde_json::to_value(idl)?;
camel_case_idl_identifiers(&mut camel_idl);
let camel_idl = serde_json::to_string_pretty(&serde_json::from_value::<Idl>(camel_idl)?)?;
Ok(format!(
r#"/**
* Program IDL in camelCase format in order to be used in JS/TS.
*
* Note that this is only a type helper and is not the actual IDL. The original
* IDL can be found at `target/idl/{idl_name}.json`.
*/
export type {type_name} = {camel_idl};
"#
))
}
fn camel_case_idl_identifiers(value: &mut JsonValue) {
match value {
JsonValue::Array(values) => {
for value in values {
camel_case_idl_identifiers(value);
}
}
JsonValue::Object(map) => {
for (key, value) in map {
if is_idl_identifier_key(key) {
camel_case_idl_identifier(value);
} else {
camel_case_idl_identifiers(value);
}
}
}
_ => {}
}
}
fn camel_case_idl_identifier(value: &mut JsonValue) {
match value {
JsonValue::String(s) => {
if Pubkey::try_from(s.as_str()).is_err() {
*s = s
.split('.')
.map(ToLowerCamelCase::to_lower_camel_case)
.collect::<Vec<_>>()
.join(".");
}
}
JsonValue::Array(values) => {
for value in values {
camel_case_idl_identifier(value);
}
}
_ => camel_case_idl_identifiers(value),
}
}
fn is_idl_identifier_key(key: &str) -> bool {
matches!(key, "name" | "path" | "account" | "relations" | "generic")
}
fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
let idl_json = serde_json::to_string_pretty(idl)?;
match out {
OutFile::Stdout => println!("{idl_json}"),
OutFile::File(out) => fs::write(out, idl_json)?,
};
Ok(())
}
fn account(
cfg_override: &ConfigOverride,
account_type: String,
address: Pubkey,
idl_filepath: Option<PathBuf>,
) -> Result<()> {
let (program_name, account_type_name) = account_type
.split_once('.') .and_then(|(x, y)| y.find('.').map_or_else(|| Some((x, y)), |_| None)) .ok_or_else(|| {
anyhow!(
"Please enter the account struct in the following format: <program_name>.<Account>",
)
})?;
let idl = idl_filepath.map_or_else(
|| {
let config = Config::discover(cfg_override)?.ok_or_else(|| {
anyhow!(
"The 'anchor account' command requires an Anchor workspace with Anchor.toml \
for IDL type generation."
)
})?;
let programs = config
.read_all_programs()
.expect("Workspace must contain atleast one program.");
let program = programs
.iter()
.find(|p| p.lib_name == *program_name)
.ok_or_else(|| {
let mut available_programs: Vec<String> =
programs.iter().map(|p| p.lib_name.clone()).collect();
available_programs.sort();
if available_programs.is_empty() {
anyhow!(
"Program '{program_name}' not found in workspace. No programs \
available."
)
} else {
anyhow!(
"Program '{program_name}' not found in workspace.\n\nAvailable \
programs:\n {}",
available_programs.join("\n ")
)
}
})?;
program.idl.clone().ok_or_else(|| {
anyhow!("IDL not found. Please build the program atleast once to generate the IDL.")
})
},
|idl_path| {
let idl = fs::read(idl_path)?;
let idl = convert_idl(&idl)?;
if idl.metadata.name != *program_name {
return Err(anyhow!("IDL does not match program {program_name}."));
}
Ok(idl)
},
)?;
let cluster = match &cfg_override.cluster {
Some(cluster) => cluster.clone(),
None => Config::discover(cfg_override)?
.map(|cfg| cfg.provider.cluster.clone())
.unwrap_or(Cluster::Localnet),
};
let data = create_client(cluster.url()).get_account_data(&address)?;
let disc_len = idl
.accounts
.iter()
.find(|acc| acc.name == *account_type_name)
.map(|acc| acc.discriminator.len())
.ok_or_else(|| {
let mut available_accounts: Vec<String> =
idl.accounts.iter().map(|acc| acc.name.clone()).collect();
available_accounts.sort();
if available_accounts.is_empty() {
anyhow!(
"Account '{account_type_name}' not found in IDL. No accounts available in \
program '{program_name}'."
)
} else {
anyhow!(
"Account '{account_type_name}' not found in IDL.\n\nAvailable accounts in \
program '{program_name}':\n {}",
available_accounts.join("\n ")
)
}
})?;
let mut data_view = &data[disc_len..];
let deserialized_json =
deserialize_idl_defined_type_to_json(&idl, account_type_name, &mut data_view)?;
println!(
"{}",
serde_json::to_string_pretty(&deserialized_json).unwrap()
);
Ok(())
}
fn deserialize_idl_defined_type_to_json(
idl: &Idl,
defined_type_name: &str,
data: &mut &[u8],
) -> Result<JsonValue, anyhow::Error> {
let defined_type = &idl
.accounts
.iter()
.find(|acc| acc.name == defined_type_name)
.and_then(|acc| idl.types.iter().find(|ty| ty.name == acc.name))
.or_else(|| idl.types.iter().find(|ty| ty.name == defined_type_name))
.ok_or_else(|| anyhow!("Type `{}` not found in IDL.", defined_type_name))?
.ty;
let mut deserialized_fields = Map::new();
match defined_type {
IdlTypeDefTy::Struct { fields } => {
if let Some(fields) = fields {
match fields {
IdlDefinedFields::Named(fields) => {
for field in fields {
deserialized_fields.insert(
field.name.clone(),
deserialize_idl_type_to_json(&field.ty, data, idl)?,
);
}
}
IdlDefinedFields::Tuple(fields) => {
let mut values = Vec::new();
for field in fields {
values.push(deserialize_idl_type_to_json(field, data, idl)?);
}
deserialized_fields
.insert(defined_type_name.to_owned(), JsonValue::Array(values));
}
}
}
}
IdlTypeDefTy::Enum { variants } => {
let repr = <u8 as AnchorDeserialize>::deserialize(data)?;
let variant = variants
.get(repr as usize)
.ok_or_else(|| anyhow!("Error while deserializing enum variant {repr}"))?;
let mut value = json!({});
if let Some(enum_field) = &variant.fields {
match enum_field {
IdlDefinedFields::Named(fields) => {
let mut values = Map::new();
for field in fields {
values.insert(
field.name.clone(),
deserialize_idl_type_to_json(&field.ty, data, idl)?,
);
}
value = JsonValue::Object(values);
}
IdlDefinedFields::Tuple(fields) => {
let mut values = Vec::new();
for field in fields {
values.push(deserialize_idl_type_to_json(field, data, idl)?);
}
value = JsonValue::Array(values);
}
}
}
deserialized_fields.insert(variant.name.clone(), value);
}
IdlTypeDefTy::Type { alias } => {
return deserialize_idl_type_to_json(alias, data, idl);
}
}
Ok(JsonValue::Object(deserialized_fields))
}
fn deserialize_idl_type_to_json(
idl_type: &IdlType,
data: &mut &[u8],
parent_idl: &Idl,
) -> Result<JsonValue, anyhow::Error> {
if data.is_empty() {
return Err(anyhow::anyhow!("Unable to parse from empty bytes"));
}
Ok(match idl_type {
IdlType::Bool => json!(<bool as AnchorDeserialize>::deserialize(data)?),
IdlType::U8 => {
json!(<u8 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I8 => {
json!(<i8 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::U16 => {
json!(<u16 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I16 => {
json!(<i16 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::U32 => {
json!(<u32 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I32 => {
json!(<i32 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::F32 => json!(<f32 as AnchorDeserialize>::deserialize(data)?),
IdlType::U64 => {
json!(<u64 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I64 => {
json!(<i64 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::F64 => json!(<f64 as AnchorDeserialize>::deserialize(data)?),
IdlType::U128 => {
json!(<u128 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I128 => {
json!(<i128 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::U256 => todo!("Upon completion of u256 IDL standard"),
IdlType::I256 => todo!("Upon completion of i256 IDL standard"),
IdlType::Bytes => JsonValue::Array(
<Vec<u8> as AnchorDeserialize>::deserialize(data)?
.iter()
.map(|i| json!(*i))
.collect(),
),
IdlType::String => json!(<String as AnchorDeserialize>::deserialize(data)?),
IdlType::Pubkey => {
json!(<Pubkey as AnchorDeserialize>::deserialize(data)?.to_string())
}
IdlType::Array(ty, size) => match size {
IdlArrayLen::Value(size) => {
let mut array_data: Vec<JsonValue> = Vec::with_capacity(*size);
for _ in 0..*size {
array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
}
JsonValue::Array(array_data)
}
IdlArrayLen::Generic(_) => unimplemented!("Generic array length is not yet supported"),
},
IdlType::Option(ty) => {
let is_present = <u8 as AnchorDeserialize>::deserialize(data)?;
if is_present == 0 {
JsonValue::String("None".to_string())
} else {
deserialize_idl_type_to_json(ty, data, parent_idl)?
}
}
IdlType::Vec(ty) => {
let size: usize = <u32 as AnchorDeserialize>::deserialize(data)?
.try_into()
.unwrap();
let mut vec_data: Vec<JsonValue> = Vec::with_capacity(size);
for _ in 0..size {
vec_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
}
JsonValue::Array(vec_data)
}
IdlType::Defined {
name,
generics: _generics,
} => {
deserialize_idl_defined_type_to_json(parent_idl, name, data)?
}
IdlType::Generic(generic) => json!(generic),
_ => unimplemented!("{idl_type:?}"),
})
}
enum OutFile {
Stdout,
File(PathBuf),
}
#[allow(clippy::too_many_arguments)]
fn test(
cfg_override: &ConfigOverride,
program_name: Option<String>,
skip_deploy: bool,
skip_local_validator: bool,
skip_build: bool,
skip_lint: bool,
no_idl: bool,
detach: bool,
tests_to_run: Vec<String>,
script_name: Option<String>,
validator_type: ValidatorType,
profile: bool,
gdb: bool,
extra_args: Vec<String>,
env_vars: Vec<String>,
cargo_args: Vec<String>,
) -> Result<()> {
let test_paths = tests_to_run
.iter()
.map(|path| {
PathBuf::from(path)
.canonicalize()
.map_err(|_| anyhow!("Wrong path {}", path))
})
.collect::<Result<Vec<_>, _>>()?;
with_workspace(cfg_override, |cfg| -> Result<()> {
cfg.validator = Some(validator_type);
let cli_skip_local_validator = skip_local_validator;
let config_skip_local_validator = cfg.skip_local_validator.unwrap_or(false);
let workspace_root = cfg.path().parent().unwrap().to_owned();
#[cfg(windows)]
if profile {
return Err(anyhow!(
"`anchor test --profile` is not supported on Windows"
));
}
#[cfg(windows)]
let _ = gdb;
#[cfg(not(windows))]
let profile_dir = workspace_root.join(crate::profile::DEFAULT_PROFILE_DIR);
#[cfg(not(windows))]
let _gdb_guard: Option<crate::debugger::gdb::GdbDriver> = if profile {
let _ = fs::remove_dir_all(&profile_dir);
std::env::set_var("ANCHOR_PROFILE_DIR", &profile_dir);
std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
if let Some(test_script) = cfg.scripts.get_mut("test") {
if test_script.contains("cargo test") {
*test_script =
test_script.replacen("cargo test", "cargo test --features profile", 1);
if gdb {
let sep = if test_script.contains(" -- ") {
" "
} else {
" -- "
};
*test_script = format!("{test_script}{sep}--test-threads=1");
}
} else {
eprintln!(
"warning: --profile requires the `test` script in Anchor.toml to invoke \
`cargo test`; got: {test_script:?}. Profiling will not activate."
);
}
} else {
eprintln!(
"warning: --profile requires a [scripts] test entry in Anchor.toml; none \
found. Profiling will not activate."
);
}
if gdb {
let driver = crate::debugger::gdb::start_gdb_driver(&profile_dir)?;
std::env::set_var(crate::debugger::gdb::SOCKET_ENV, driver.sock_path());
std::env::set_var("RUST_TEST_THREADS", "1");
Some(driver)
} else {
None
}
} else {
None
};
if !skip_build {
build(
cfg_override,
no_idl,
None,
None,
false,
skip_lint,
true,
program_name.clone(),
None,
None,
BootstrapMode::None,
None,
None,
env_vars,
cargo_args,
false,
)?;
}
cfg.add_test_config(workspace_root, test_paths)?;
let is_localnet = cfg.provider.cluster == Cluster::Localnet;
let validator_plan = test_validator_plan(
skip_deploy,
is_localnet,
cli_skip_local_validator,
config_skip_local_validator,
);
if validator_plan.predeploy {
deploy(cfg_override, None, None, false, true, vec![])?;
}
cfg.run_hooks(HookType::PreTest)?;
let mut is_first_suite = true;
let script_name_to_use = script_name.as_deref().unwrap_or("test");
if let Some(test_script) = cfg.scripts.get_mut(script_name_to_use) {
is_first_suite = false;
match program_name {
Some(program_name) => {
if let Some((from, to)) = Regex::new("\\s(tests/\\S+\\.(js|ts))")
.unwrap()
.captures_iter(&test_script.clone())
.last()
.and_then(|c| c.get(1).and_then(|mtch| c.get(2).map(|ext| (mtch, ext))))
.map(|(mtch, ext)| {
(
mtch.as_str(),
format!("tests/{program_name}.{}", ext.as_str()),
)
})
{
println!("\nRunning tests of program `{program_name}`!");
*test_script = test_script.replace(from, &to);
}
}
_ => println!(
"\nFound a '{}' script in the Anchor.toml. Running it as a test suite!",
script_name_to_use
),
}
run_test_suite(
cfg,
cfg.path(),
is_localnet,
validator_plan.skip_local_validator,
skip_deploy,
detach,
validator_type,
&cfg.test_validator,
&cfg.scripts,
script_name_to_use,
validator_plan.stream_program_logs,
&extra_args,
&cfg.surfpool_config,
)?;
}
if let Some(test_config) = &cfg.test_config {
for test_suite in test_config.iter() {
if !is_first_suite {
std::thread::sleep(std::time::Duration::from_millis(
test_suite
.1
.test
.as_ref()
.map(|val| val.shutdown_wait)
.unwrap_or(SHUTDOWN_WAIT) as u64,
));
} else {
is_first_suite = false;
}
run_test_suite(
cfg,
test_suite.0,
is_localnet,
validator_plan.skip_local_validator,
skip_deploy,
detach,
validator_type,
&test_suite.1.test,
&test_suite.1.scripts,
script_name_to_use,
validator_plan.stream_program_logs,
&extra_args,
&cfg.surfpool_config,
)?;
}
}
cfg.run_hooks(HookType::PostTest)?;
#[cfg(not(windows))]
if profile {
render_profile(cfg, &profile_dir)?;
}
Ok(())
})?
}
fn should_predeploy_before_test(
skip_deploy: bool,
is_localnet: bool,
cli_skip_local_validator: bool,
) -> bool {
!skip_deploy && (!is_localnet || cli_skip_local_validator)
}
#[derive(Debug, PartialEq, Eq)]
struct TestValidatorPlan {
skip_local_validator: bool,
predeploy: bool,
stream_program_logs: bool,
}
fn test_validator_plan(
skip_deploy: bool,
is_localnet: bool,
cli_skip_local_validator: bool,
config_skip_local_validator: bool,
) -> TestValidatorPlan {
TestValidatorPlan {
skip_local_validator: cli_skip_local_validator || config_skip_local_validator,
predeploy: should_predeploy_before_test(skip_deploy, is_localnet, cli_skip_local_validator),
stream_program_logs: true,
}
}
#[cfg(not(windows))]
#[allow(clippy::too_many_arguments)]
fn debugger(
cfg_override: &ConfigOverride,
test_name: Option<String>,
skip_run: bool,
skip_build: bool,
skip_lint: bool,
gdb: bool,
cargo_args: Vec<String>,
) -> Result<()> {
let has_anchor_toml = match Config::discover(cfg_override) {
Ok(Some(_)) => true,
Ok(None) => false,
Err(e) => return Err(anyhow!("failed to probe for Anchor.toml: {e}")),
};
if has_anchor_toml {
debugger_anchor_workspace(
cfg_override,
test_name,
skip_run,
skip_build,
skip_lint,
gdb,
cargo_args,
)
} else {
debugger_loose(
cfg_override,
test_name,
skip_run,
skip_build,
gdb,
cargo_args,
)
}
}
#[cfg(not(windows))]
#[allow(clippy::too_many_arguments)]
fn debugger_anchor_workspace(
cfg_override: &ConfigOverride,
test_name: Option<String>,
skip_run: bool,
skip_build: bool,
skip_lint: bool,
gdb: bool,
cargo_args: Vec<String>,
) -> Result<()> {
if !skip_run {
test(
cfg_override,
None,
true,
true,
skip_build,
skip_lint,
true,
false,
Vec::new(),
None, ValidatorType::Surfpool,
true,
gdb,
Vec::new(),
Vec::new(),
cargo_args,
)?;
}
with_workspace(cfg_override, |cfg| -> Result<()> {
let workspace_root = cfg.path().parent().unwrap().to_owned();
let profile_dir = workspace_root.join(crate::profile::DEFAULT_PROFILE_DIR);
let (pubkey_to_so, sources) = resolve_anchor_workspace_programs(cfg);
if pubkey_to_so.is_empty() {
return Err(anyhow!(
"no programs resolved for the debugger.\n\nEither declare them in Anchor.toml:\n \
[programs.localnet]\n <name> = \"<pubkey>\"\n\nor run `anchor build` so \
`target/deploy/<name>-keypair.json` exists."
));
}
println!("\nResolved programs:");
for (pk, so) in &pubkey_to_so {
let src = sources.get(pk).copied().unwrap_or("unknown");
println!(" {pk} -> {} [{src}]", display_path_relative_to_cwd(so));
}
debugger::run(
&profile_dir,
&pubkey_to_so,
Some(&workspace_root),
None,
test_name.as_deref(),
)
})?
}
#[cfg(not(windows))]
#[allow(clippy::too_many_arguments)]
fn debugger_loose(
_cfg_override: &ConfigOverride,
test_name: Option<String>,
skip_run: bool,
skip_build: bool,
gdb: bool,
cargo_args: Vec<String>,
) -> Result<()> {
let cwd = std::env::current_dir().context("read current directory")?;
let ws = debugger::loose::LooseWorkspace::discover(cwd)?;
if !skip_run {
ws.check_dev_dep()?;
}
let profile_feature = ws.detect_profile_feature()?;
let profile_dir = ws.root.join(debugger::loose_profile_dir_name());
if !skip_run {
debugger::loose::clear_profile_dir(&profile_dir)?;
std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
let anchor_exe =
std::env::current_exe().context("resolve anchor binary path for RUSTC_WRAPPER")?;
std::env::set_var("RUSTC_WRAPPER", &anchor_exe);
std::env::set_var(debugger::rustc_wrapper::WRAPPER_SENTINEL, "1");
if !skip_build {
let build_cwd = ws.cargo_invocation_dir();
eprintln!("running `cargo build-sbf` from {}", build_cwd.display());
cargo_build_sbf(Some(build_cwd), &cargo_args)?;
}
std::env::remove_var("RUSTC_WRAPPER");
std::env::remove_var(debugger::rustc_wrapper::WRAPPER_SENTINEL);
eprintln!(
"running `cargo test{gdb} --features {profile_feature}{pkg}{filter}` from {dir}",
gdb = if gdb { " [gdb mode]" } else { "" },
pkg = ws
.current_package
.as_deref()
.map(|p| format!(" -p {p}"))
.unwrap_or_default(),
filter = test_name
.as_deref()
.map(|f| format!(" -- {f}"))
.unwrap_or_default(),
dir = ws.cargo_invocation_dir().display(),
);
if gdb {
debugger::gdb::run_gdb_mode(
ws.cargo_invocation_dir(),
ws.current_package.as_deref(),
&profile_feature,
&profile_dir,
test_name.as_deref(),
)?;
} else {
debugger::loose::run_cargo_test(
ws.cargo_invocation_dir(),
ws.current_package.as_deref(),
&profile_feature,
&profile_dir,
test_name.as_deref(),
)?;
}
}
let pubkey_to_so = debugger::loose::discover_programs(&ws.root, ws.current_package.as_deref())?;
if pubkey_to_so.is_empty() {
eprintln!(
"warning: no programs found under {}/target/deploy/.\nELFs are required for \
source/disasm symbolication. The debugger will still open but the static disasm pane \
will be empty.",
ws.root.display()
);
}
if !profile_dir.exists() {
return Err(anyhow!(
"no traces produced at {}.\n\nDid the test actually run? Check that:\n- the test \
calls `anchor_v2_testing::svm()` (NOT `LiteSVM::new()`)\n- the `{profile_feature}` \
feature is enabled in the test build\n- the test sent at least one transaction that \
hit a BPF program",
profile_dir.display()
));
}
debugger::run(
&profile_dir,
&pubkey_to_so,
Some(&ws.root),
Some(&ws.cwd),
test_name.as_deref(),
)
}
#[cfg(not(windows))]
fn run_coverage(
_cfg_override: &ConfigOverride,
skip_run: bool,
skip_build: bool,
output: &str,
trace_dir: &str,
cargo_args: Vec<String>,
) -> Result<()> {
let cwd = std::env::current_dir().context("read current directory")?;
let ws = debugger::loose::LooseWorkspace::discover(cwd)?;
let trace_path = ws.root.join(trace_dir);
let output_path = ws.root.join(output);
if !skip_run {
std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "2");
let anchor_exe =
std::env::current_exe().context("resolve anchor binary path for RUSTC_WRAPPER")?;
std::env::set_var("RUSTC_WRAPPER", &anchor_exe);
std::env::set_var(debugger::rustc_wrapper::WRAPPER_SENTINEL, "1");
if !skip_build {
let build_cwd = ws.cargo_invocation_dir();
eprintln!("building programs with DWARF...");
cargo_build_sbf(Some(build_cwd), &cargo_args)?;
}
if trace_path.exists() {
fs::remove_dir_all(&trace_path)?;
}
fs::create_dir_all(&trace_path)?;
let profile_feature = ws.detect_profile_feature().ok();
eprintln!("running tests with register tracing...");
let mut cmd = std::process::Command::new("cargo");
cmd.current_dir(ws.cargo_invocation_dir()).arg("test");
if let Some(feature) = &profile_feature {
cmd.env("ANCHOR_PROFILE_DIR", &trace_path)
.arg("--features")
.arg(feature);
} else {
cmd.env("SBF_TRACE_DIR", &trace_path);
}
if let Some(pkg) = &ws.current_package {
cmd.arg("-p").arg(pkg);
}
let status = cmd.status().context("spawn cargo test")?;
if !status.success() {
return Err(anyhow!("cargo test failed"));
}
}
if !trace_path.exists() {
return Err(anyhow!(
"no traces at {}. Run without --skip-run first.",
trace_path.display()
));
}
let programs = debugger::loose::discover_programs(&ws.root, ws.current_package.as_deref())?;
if programs.is_empty() {
return Err(anyhow!(
"no programs found. Ensure declare_id!() is present in source.",
));
}
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
coverage::generate_lcov(&trace_path, &programs, Some(&ws.root), &output_path)
}
#[cfg(not(windows))]
fn display_path_relative_to_cwd(p: &Path) -> String {
std::env::current_dir()
.ok()
.as_deref()
.and_then(|c| p.strip_prefix(c).ok())
.map(|rel| rel.display().to_string())
.unwrap_or_else(|| p.display().to_string())
}
#[cfg(not(windows))]
fn resolve_anchor_workspace_programs(
cfg: &WithPath<Config>,
) -> (BTreeMap<String, PathBuf>, BTreeMap<String, &'static str>) {
let workspace_root = cfg.path().parent().unwrap();
let deploy_dir = workspace_root.join("target").join("deploy");
let mut pubkey_to_so: BTreeMap<String, PathBuf> = BTreeMap::new();
let mut sources: BTreeMap<String, &'static str> = BTreeMap::new();
for programs in cfg.programs.values() {
for (name, deployment) in programs {
let pk = deployment.address.to_string();
pubkey_to_so.insert(pk.clone(), deploy_dir.join(format!("{name}.so")));
sources.insert(pk, "Anchor.toml");
}
}
if let Ok(discovered) = debugger::loose::discover_programs(workspace_root, None) {
for (pk, so) in discovered {
if !pubkey_to_so.contains_key(&pk) {
pubkey_to_so.insert(pk.clone(), so);
sources.insert(pk, "target/deploy");
}
}
}
(pubkey_to_so, sources)
}
#[cfg(not(windows))]
fn render_profile(cfg: &WithPath<Config>, profile_dir: &Path) -> Result<()> {
let workspace_root = cfg.path().parent().unwrap().to_owned();
let (pubkey_to_so, _sources) = resolve_anchor_workspace_programs(cfg);
let rendered = profile::render_all_tests(profile_dir, Some(&workspace_root), &pubkey_to_so)
.context("failed to render flamegraphs from trace directory")?;
if rendered.is_empty() {
eprintln!(
"warning: no per-test trace directories found under {}. Did your tests call \
`anchor_v2_testing::svm()` with the `profile` feature?",
profile_dir.display()
);
return Ok(());
}
let mut sorted: Vec<&profile::RenderedTest> = rendered.iter().collect();
sorted.sort_by(|a, b| a.test_name.cmp(&b.test_name));
let max_name = sorted
.iter()
.filter(|t| t.svg_paths.len() == 1)
.map(|t| t.test_name.len())
.max()
.unwrap_or(0);
println!("\nFlamegraphs:");
for test in &sorted {
if test.svg_paths.len() == 1 {
println!(
" {:<width$} -> {}",
test.test_name,
display_path_relative_to_cwd(&test.svg_paths[0]),
width = max_name,
);
} else {
println!(" {}", test.test_name);
for (i, svg) in test.svg_paths.iter().enumerate() {
println!(" tx{} -> {}", i + 1, display_path_relative_to_cwd(svg));
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_test_suite(
cfg: &WithPath<Config>,
test_suite_path: impl AsRef<Path>,
is_localnet: bool,
skip_local_validator: bool,
skip_deploy: bool,
detach: bool,
validator_type: ValidatorType,
test_validator: &Option<TestValidator>,
scripts: &ScriptsConfig,
script_name: &str,
stream_program_logs: bool,
extra_args: &[String],
surfpool_config: &Option<SurfpoolConfig>,
) -> Result<()> {
println!("\nRunning test suite: {:#?}\n", test_suite_path.as_ref());
let mut validator_handle = None;
if is_localnet && !skip_local_validator {
let generated_accounts = generated_validator_accounts(cfg, test_validator)?;
match validator_type {
ValidatorType::Surfpool => {
let full_simnet_mode = false;
let flags = Some(surfpool_flags(
cfg,
surfpool_config,
full_simnet_mode,
skip_deploy,
Some(test_suite_path.as_ref()),
&generated_accounts,
)?);
validator_handle = Some(start_surfpool_validator(
flags,
surfpool_config,
full_simnet_mode,
)?);
}
ValidatorType::Legacy => {
let flags = Some(validator_flags(
cfg,
test_validator,
skip_deploy,
&generated_accounts,
)?);
validator_handle = Some(start_solana_test_validator(
cfg,
test_validator,
flags,
true,
)?);
}
}
}
let url = cluster_url(cfg, test_validator, surfpool_config);
let node_options = format!(
"{} {}",
match std::env::var_os("NODE_OPTIONS") {
Some(value) => value
.into_string()
.map_err(std::env::VarError::NotUnicode)?,
None => "".to_owned(),
},
get_node_dns_option(),
);
let log_streams = if stream_program_logs {
match stream_logs(cfg, &url) {
Ok(streams) => Some(streams),
Err(e) => {
eprintln!("Warning: Failed to setup program log streaming: {:#}", e);
eprintln!("Program logs will still be visible in the test output.");
None
}
}
} else {
None
};
let test_result = {
let Some(cmd) = scripts.get(script_name) else {
bail!("Not able to find script for `{}`", script_name);
};
let cmd = cmd.clone();
let script_args = format!("{cmd} {}", extra_args.join(" "));
std::process::Command::new("bash")
.arg("-c")
.arg(script_args)
.env("ANCHOR_PROVIDER_URL", url)
.env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
.env("NODE_OPTIONS", node_options)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(anyhow::Error::from)
.context(cmd)
};
if test_result.is_ok() && detach {
println!("Local validator still running. Press Ctrl + C quit.");
std::io::stdin().lock().lines().next().unwrap().unwrap();
}
if let Some(mut child) = validator_handle {
if let Err(err) = child.kill() {
println!("Failed to kill subprocess {}: {}", child.id(), err);
}
}
if let Some(log_streams) = log_streams {
for handle in log_streams {
handle.shutdown();
}
}
match test_result {
Ok(exit) => {
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap());
}
}
Err(err) => {
println!("Failed to run test: {err:#}");
return Err(err);
}
}
Ok(())
}
fn validator_flags(
cfg: &WithPath<Config>,
test_validator: &Option<TestValidator>,
skip_deploy: bool,
generated_accounts: &[GeneratedAccount],
) -> Result<Vec<String>> {
let mut flags = match skip_deploy {
true => Vec::new(),
false => validator_deploy_flags(cfg, test_validator)?,
};
for acct in generated_accounts {
flags.push("--account".to_string());
flags.push(acct.pubkey.to_string());
flags.push(acct.file_path.display().to_string());
}
flags.extend(validator_config_flags(test_validator)?);
Ok(flags)
}
fn rent_exempt_minimum(data_len: u64) -> u64 {
(128 + data_len) * 3480 * 2
}
const RENT_EPOCH_NEVER: u64 = u64::MAX;
fn write_keypair_secure(keypair: &Keypair, path: &Path) -> Result<()> {
use std::io::Write;
let bytes = keypair.to_bytes().to_vec();
let json = serde_json::to_string(&bytes)
.with_context(|| format!("Failed to serialize keypair for {}", path.display()))?;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts
.open(path)
.with_context(|| format!("Failed to create keypair file: {}", path.display()))?;
file.write_all(json.as_bytes())
.with_context(|| format!("Failed to write keypair file: {}", path.display()))?;
Ok(())
}
fn pack_coption_pubkey(buf: &mut Vec<u8>, value: Option<Pubkey>) {
match value {
Some(pk) => {
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(pk.as_ref());
}
None => {
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&[0u8; 32]);
}
}
}
fn write_account_json(path: &Path, value: &JsonValue) -> Result<()> {
let mut file = File::create(path)
.with_context(|| format!("Failed to create account file: {}", path.display()))?;
serde_json::to_writer_pretty(&mut file, value)
.with_context(|| format!("Failed to write account JSON to: {}", path.display()))?;
Ok(())
}
fn is_new_address(address: &str) -> bool {
address.eq_ignore_ascii_case("new")
}
#[derive(Debug, Clone)]
struct GeneratedAccount {
pubkey: Pubkey,
file_path: PathBuf,
surfpool_snapshot_value: JsonValue,
}
fn resolve_workspace_path(cfg: &WithPath<Config>, path: &str) -> Result<PathBuf> {
let workspace_root = cfg
.path()
.parent()
.ok_or_else(|| anyhow!("Anchor.toml path has no parent directory"))?;
let candidate = Path::new(path);
Ok(if candidate.is_relative() {
workspace_root.join(candidate)
} else {
candidate.to_path_buf()
})
}
fn account_dir_pubkeys(cfg: &WithPath<Config>, validator: &Validator) -> Result<HashSet<Pubkey>> {
let mut pubkeys = HashSet::new();
for account_dir in validator.account_dir.iter().flatten() {
let directory = resolve_workspace_path(cfg, &account_dir.directory)?;
for entry in fs::read_dir(&directory)
.with_context(|| format!("Failed to read account directory: {}", directory.display()))?
{
let path = entry?.path();
if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
continue;
}
let fixture: JsonValue =
serde_json::from_reader(File::open(&path).with_context(|| {
format!("Failed to open account fixture: {}", path.display())
})?)
.with_context(|| format!("Failed to parse account fixture: {}", path.display()))?;
let Some(pubkey) = fixture.get("pubkey").and_then(JsonValue::as_str) else {
continue;
};
pubkeys.insert(
Pubkey::try_from(pubkey)
.map_err(|_| anyhow!("Invalid pubkey {} in {}", pubkey, path.display()))?,
);
}
}
Ok(pubkeys)
}
fn validator_supplied_account_pubkeys(
cfg: &WithPath<Config>,
validator: &Validator,
created_mints: &[Pubkey],
) -> Result<HashSet<Pubkey>> {
let mut pubkeys = created_mints.iter().copied().collect::<HashSet<_>>();
if let Some(accounts) = &validator.account {
for account in accounts {
pubkeys.insert(
Pubkey::try_from(account.address.as_str())
.map_err(|_| anyhow!("Invalid account pubkey: {}", account.address))?,
);
}
}
if let Some(clones) = &validator.clone {
for clone in clones {
pubkeys.insert(
Pubkey::try_from(clone.address.as_str())
.map_err(|_| anyhow!("Invalid clone pubkey: {}", clone.address))?,
);
}
}
pubkeys.extend(account_dir_pubkeys(cfg, validator)?);
Ok(pubkeys)
}
fn materialize_validator_accounts(
cfg: &WithPath<Config>,
validator: &Validator,
) -> Result<Vec<GeneratedAccount>> {
let mut out = Vec::new();
let needs_dir = validator.mints.is_some()
|| validator.token_accounts.is_some()
|| validator.fund_accounts.is_some();
if !needs_dir {
return Ok(out);
}
let workspace_root = cfg
.path()
.parent()
.ok_or_else(|| anyhow!("Anchor.toml path has no parent directory"))?;
let accounts_dir = workspace_root.join(".anchor").join("generated_accounts");
fs::create_dir_all(&accounts_dir).with_context(|| {
format!(
"Failed to create accounts directory: {}",
accounts_dir.display()
)
})?;
let mut seen_pubkeys: HashSet<Pubkey> = HashSet::new();
let mut record_pubkey = |pk: Pubkey, section: &str| -> Result<()> {
if !seen_pubkeys.insert(pk) {
bail!(
"Duplicate pubkey {} across [test.validator] sections (collision detected in \
`{}`). Each generated account must have a unique address.",
pk,
section
);
}
Ok(())
};
let mut created_mints: Vec<Pubkey> = Vec::new();
if let Some(mints) = &validator.mints {
for token_mint in mints {
let pubkey = if is_new_address(&token_mint.address) {
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
let keypair_path = accounts_dir.join(format!("{}.mint.json", pubkey));
write_keypair_secure(&keypair, &keypair_path)?;
pubkey
} else {
Pubkey::try_from(token_mint.address.as_str())
.map_err(|_| anyhow!("Invalid mint pubkey address: {}", token_mint.address))?
};
record_pubkey(pubkey, "mints")?;
created_mints.push(pubkey);
let parse_authority = |opt: &Option<String>, field: &str| -> Result<Option<Pubkey>> {
opt.as_ref()
.map(|s| {
Pubkey::try_from(s.as_str()).map_err(|_| {
anyhow!("Invalid {} pubkey for mint {}: {}", field, pubkey, s)
})
})
.transpose()
};
let mint_authority = parse_authority(&token_mint.mint_authority, "mint_authority")?;
let freeze_authority =
parse_authority(&token_mint.freeze_authority, "freeze_authority")?;
let mut data = Vec::with_capacity(82);
pack_coption_pubkey(&mut data, mint_authority);
data.extend_from_slice(&token_mint.supply.unwrap_or(0).to_le_bytes());
data.push(token_mint.decimals);
data.push(1u8); pack_coption_pubkey(&mut data, freeze_authority);
let account_json = json!({
"pubkey": pubkey.to_string(),
"account": {
"lamports": rent_exempt_minimum(82),
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"executable": false,
"rentEpoch": RENT_EPOCH_NEVER,
"data": [STANDARD.encode(&data), "base64"]
}
});
let file_path = accounts_dir.join(format!("{}.json", pubkey));
write_account_json(&file_path, &account_json)?;
out.push(GeneratedAccount {
pubkey,
file_path,
surfpool_snapshot_value: json!({
"lamports": rent_exempt_minimum(82),
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"executable": false,
"rentEpoch": RENT_EPOCH_NEVER,
"data": STANDARD.encode(&data),
"parsedData": JsonValue::Null,
}),
});
}
}
if let Some(token_accounts) = &validator.token_accounts {
let validator_supplied_pubkeys =
validator_supplied_account_pubkeys(cfg, validator, &created_mints)?;
for token_account in token_accounts {
let mint_pubkey = if is_new_address(&token_account.mint) {
*created_mints.last().ok_or_else(|| {
anyhow!(
"token_account specifies `mint = \"new\"` but no [[test.validator.mints]] \
entries are configured"
)
})?
} else {
let mint_pubkey = Pubkey::try_from(token_account.mint.as_str()).map_err(|_| {
anyhow!(
"Invalid mint pubkey in token_account: {}",
token_account.mint
)
})?;
if !validator_supplied_pubkeys.contains(&mint_pubkey) {
bail!(
"token_account mint {} is not loaded by the validator. Add it via \
[[test.validator.mints]], [[test.validator.clone]], \
[[test.validator.account]], or [[test.validator.account_dir]].",
mint_pubkey
);
}
mint_pubkey
};
let owner_pubkey = if is_new_address(&token_account.owner) {
let kp = Keypair::new();
let pk = kp.pubkey();
let owner_path = accounts_dir.join(format!("{}.owner.json", pk));
write_keypair_secure(&kp, &owner_path)?;
pk
} else {
Pubkey::try_from(token_account.owner.as_str()).map_err(|_| {
anyhow!(
"Invalid owner pubkey in token_account: {}",
token_account.owner
)
})?
};
let token_account_pubkey = match &token_account.address {
Some(addr) if !is_new_address(addr) => Pubkey::try_from(addr.as_str())
.map_err(|_| anyhow!("Invalid token_account address pubkey: {}", addr))?,
_ => {
let kp = Keypair::new();
let pk = kp.pubkey();
let ta_path = accounts_dir.join(format!("{}.token_account.json", pk));
write_keypair_secure(&kp, &ta_path)?;
pk
}
};
record_pubkey(token_account_pubkey, "token_accounts")?;
let mut data = Vec::with_capacity(165);
data.extend_from_slice(mint_pubkey.as_ref());
data.extend_from_slice(owner_pubkey.as_ref());
data.extend_from_slice(&token_account.amount.to_le_bytes());
pack_coption_pubkey(&mut data, None); data.push(1u8); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&[0u8; 8]); data.extend_from_slice(&0u64.to_le_bytes()); pack_coption_pubkey(&mut data, None);
let account_json = json!({
"pubkey": token_account_pubkey.to_string(),
"account": {
"lamports": rent_exempt_minimum(165),
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"executable": false,
"rentEpoch": RENT_EPOCH_NEVER,
"data": [STANDARD.encode(&data), "base64"]
}
});
let file_path = accounts_dir.join(format!("{}.json", token_account_pubkey));
write_account_json(&file_path, &account_json)?;
out.push(GeneratedAccount {
pubkey: token_account_pubkey,
file_path,
surfpool_snapshot_value: json!({
"lamports": rent_exempt_minimum(165),
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"executable": false,
"rentEpoch": RENT_EPOCH_NEVER,
"data": STANDARD.encode(&data),
"parsedData": JsonValue::Null,
}),
});
}
}
if let Some(fund_accounts) = &validator.fund_accounts {
for funded_account in fund_accounts {
let pubkey = if is_new_address(&funded_account.address) {
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
let keypair_path = accounts_dir.join(format!("{}.keypair.json", pubkey));
write_keypair_secure(&keypair, &keypair_path)?;
pubkey
} else {
Pubkey::try_from(funded_account.address.as_str())
.map_err(|_| anyhow!("Invalid pubkey address: {}", funded_account.address))?
};
record_pubkey(pubkey, "fund_accounts")?;
let lamports = funded_account.lamports.unwrap_or(1_000_000_000);
let account_json = json!({
"pubkey": pubkey.to_string(),
"account": {
"lamports": lamports,
"owner": "11111111111111111111111111111111",
"executable": false,
"rentEpoch": RENT_EPOCH_NEVER,
"data": ["", "base64"]
}
});
let file_path = accounts_dir.join(format!("{}.json", pubkey));
write_account_json(&file_path, &account_json)?;
out.push(GeneratedAccount {
pubkey,
file_path,
surfpool_snapshot_value: json!({
"lamports": lamports,
"owner": "11111111111111111111111111111111",
"executable": false,
"rentEpoch": RENT_EPOCH_NEVER,
"data": "",
"parsedData": JsonValue::Null,
}),
});
}
}
Ok(out)
}
fn generated_validator_accounts(
cfg: &WithPath<Config>,
test_validator: &Option<TestValidator>,
) -> Result<Vec<GeneratedAccount>> {
test_validator
.as_ref()
.and_then(|test| test.validator.as_ref())
.map(|validator| materialize_validator_accounts(cfg, validator))
.transpose()
.map(|accounts| accounts.unwrap_or_default())
}
fn write_surfpool_snapshot(
cfg: &WithPath<Config>,
generated_accounts: &[GeneratedAccount],
) -> Result<Option<PathBuf>> {
if generated_accounts.is_empty() {
return Ok(None);
}
let accounts_dir = resolve_workspace_path(cfg, ".anchor/generated_accounts")?;
fs::create_dir_all(&accounts_dir).with_context(|| {
format!(
"Failed to create accounts directory for Surfpool snapshot: {}",
accounts_dir.display()
)
})?;
let snapshot_path = accounts_dir.join("surfpool.snapshot.json");
let mut snapshot = Map::new();
for account in generated_accounts {
snapshot.insert(
account.pubkey.to_string(),
account.surfpool_snapshot_value.clone(),
);
}
let mut file = File::create(&snapshot_path).with_context(|| {
format!(
"Failed to create Surfpool snapshot file: {}",
snapshot_path.display()
)
})?;
serde_json::to_writer_pretty(&mut file, &JsonValue::Object(snapshot)).with_context(|| {
format!(
"Failed to write Surfpool snapshot file: {}",
snapshot_path.display()
)
})?;
Ok(Some(snapshot_path))
}
fn validator_deploy_flags(
cfg: &WithPath<Config>,
test_validator: &Option<TestValidator>,
) -> Result<Vec<String>> {
let programs = cfg.programs.get(&Cluster::Localnet);
let test_upgradeable_program = test_validator
.as_ref()
.map(|test_validator| test_validator.upgradeable)
.unwrap_or(false);
let mut flags = Vec::new();
for mut program in cfg.read_all_programs()? {
let verifiable = false;
let binary_path = program.binary_path(verifiable)?.display().to_string();
let address = programs
.and_then(|m| m.get(&program.lib_name))
.map(|deployment| Ok(deployment.address.to_string()))
.unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
if test_upgradeable_program {
flags.push("--upgradeable-program".to_string());
flags.push(address.clone());
flags.push(binary_path);
flags.push(cfg.wallet_kp()?.pubkey().to_string());
} else {
flags.push("--bpf-program".to_string());
flags.push(address.clone());
flags.push(binary_path);
}
if let Some(idl) = program.idl.as_mut() {
idl.address = address;
let idl_out = target_dir()?
.join("idl")
.join(&idl.metadata.name)
.with_extension("json");
write_idl(idl, OutFile::File(idl_out))?;
}
}
if let Some(test) = test_validator.as_ref() {
if let Some(genesis) = &test.genesis {
for entry in genesis {
let program_path = Path::new(&entry.program);
if !program_path.exists() {
return Err(anyhow!(
"Program in genesis configuration does not exist at path: {}",
program_path.display()
));
}
if entry.upgradeable.unwrap_or(false) {
flags.push("--upgradeable-program".to_string());
flags.push(entry.address.clone());
flags.push(entry.program.clone());
flags.push(cfg.wallet_kp()?.pubkey().to_string());
} else {
flags.push("--bpf-program".to_string());
flags.push(entry.address.clone());
flags.push(entry.program.clone());
}
}
}
}
Ok(flags)
}
fn validator_config_flags(test_validator: &Option<TestValidator>) -> Result<Vec<String>> {
let mut flags = Vec::new();
if let Some(validator) = test_validator
.as_ref()
.and_then(|test| test.validator.as_ref())
{
let entries = serde_json::to_value(validator)?;
for (key, value) in entries.as_object().unwrap() {
if key == "ledger" {
continue;
};
if key == "fund_accounts" || key == "mints" || key == "token_accounts" {
continue;
}
if key == "extra_args" {
for arg in value.as_array().unwrap() {
flags.push(arg.as_str().unwrap().to_string());
}
continue;
}
if key == "account" {
for entry in value.as_array().unwrap() {
flags.push("--account".to_string());
flags.push(entry["address"].as_str().unwrap().to_string());
flags.push(entry["filename"].as_str().unwrap().to_string());
}
} else if key == "account_dir" {
for entry in value.as_array().unwrap() {
flags.push("--account-dir".to_string());
flags.push(entry["directory"].as_str().unwrap().to_string());
}
} else if key == "clone" {
let client = if let Some(url) = entries["url"].as_str() {
create_client(url)
} else {
return Err(anyhow!(
"Validator url for Solana's JSON RPC should be provided in order to clone \
accounts from it"
));
};
let pubkeys = value
.as_array()
.unwrap()
.iter()
.map(|entry| {
let address = entry["address"].as_str().unwrap();
Pubkey::try_from(address).map_err(|_| anyhow!("Invalid pubkey {}", address))
})
.collect::<Result<HashSet<Pubkey>>>()?
.into_iter()
.collect::<Vec<_>>();
let accounts = client.get_multiple_accounts(&pubkeys)?;
for (pubkey, account) in pubkeys.into_iter().zip(accounts) {
match account {
Some(account) => {
if account.owner == bpf_loader_upgradeable::id()
&& matches!(
account.deserialize_data::<UpgradeableLoaderState>()?,
UpgradeableLoaderState::Program { .. }
)
{
flags.push("--clone-upgradeable-program".to_string());
flags.push(pubkey.to_string());
} else {
flags.push("--clone".to_string());
flags.push(pubkey.to_string());
}
}
_ => return Err(anyhow!("Account {} not found", pubkey)),
}
}
} else if key == "deactivate_feature" {
let pubkeys_result: Result<Vec<Pubkey>, _> = value
.as_array()
.unwrap()
.iter()
.map(|entry| {
let feature_flag = entry.as_str().unwrap();
Pubkey::try_from(feature_flag)
.map_err(|_| anyhow!("Invalid pubkey (feature flag) {}", feature_flag))
})
.collect();
let features = pubkeys_result?;
for feature in features {
flags.push("--deactivate-feature".to_string());
flags.push(feature.to_string());
}
} else {
flags.push(format!("--{}", key.replace('_', "-")));
if let serde_json::Value::String(v) = value {
flags.push(v.to_string());
} else {
flags.push(value.to_string());
}
}
}
}
Ok(flags)
}
fn surfpool_flags(
cfg: &WithPath<Config>,
surfpool_config: &Option<SurfpoolConfig>,
full_simnet_mode: bool,
skip_deploy: bool,
test_suite_path: Option<&Path>,
generated_accounts: &[GeneratedAccount],
) -> Result<Vec<String>> {
let programs = cfg.programs.get(&Cluster::Localnet);
let mut flags = Vec::new();
for mut program in cfg.read_all_programs()? {
let address = programs
.and_then(|m| m.get(&program.lib_name))
.map(|deployment| Ok(deployment.address.to_string()))
.unwrap_or_else(|| program.pubkey().map(|p| p.to_string()))?;
if let Some(idl) = program.idl.as_mut() {
idl.address = address;
let idl_out = target_dir()?
.join("idl")
.join(&idl.metadata.name)
.with_extension("json");
write_idl(idl, OutFile::File(idl_out))?;
}
}
if let Some(snapshot_path) = write_surfpool_snapshot(cfg, generated_accounts)? {
flags.push("--snapshot".to_string());
flags.push(snapshot_path.display().to_string());
}
if let Some(config) = &surfpool_config {
if let Some(airdrop_addresses) = &config.airdrop_addresses {
for address in airdrop_addresses {
flags.push("--airdrop".to_string());
flags.push(address.to_string());
}
}
if let Some(datasource_rpc_url) = &config.datasource_rpc_url {
flags.push("--rpc-url".to_string());
flags.push(datasource_rpc_url.to_string());
}
let host = &config.host;
flags.push("--host".to_string());
flags.push(host.to_string());
let rpc_port = &config.rpc_port;
flags.push("--port".to_string());
flags.push(rpc_port.to_string());
if let Some(ws_port) = &config.ws_port {
flags.push("--ws-port".to_string());
flags.push(ws_port.to_string());
}
if let Some(manifest_file_path) = &config.manifest_file_path {
flags.push("--manifest-file-path".to_string());
flags.push(manifest_file_path.to_string());
}
if let Some(runbooks) = &config.runbooks {
for runbook in runbooks {
flags.push("--runbook".to_string());
flags.push(runbook.to_string());
}
}
if let Some(slot_time) = &config.slot_time {
flags.push("--slot-time".to_string());
flags.push(slot_time.to_string());
}
}
let online = surfpool_config
.as_ref()
.and_then(|c| c.online)
.unwrap_or(false);
if !online {
flags.push("--offline".to_string());
}
let block_production_mode = surfpool_config
.as_ref()
.and_then(|c| c.block_production_mode.clone())
.unwrap_or("transaction".into());
flags.push("--block-production-mode".to_string());
flags.push(block_production_mode);
flags.push("--log-level".to_string());
flags.push(
surfpool_config
.as_ref()
.and_then(|c| c.log_level.clone())
.unwrap_or("none".into()),
);
if !full_simnet_mode {
flags.push("--no-tui".to_string());
flags.push("--disable-instruction-profiling".to_string());
flags.push("--max-profiles".to_string());
flags.push("1".to_string());
flags.push("--no-studio".to_string());
}
match skip_deploy {
true => flags.push("--no-deploy".to_string()),
false => {
flags.push("--legacy-anchor-compatibility".to_string());
if let Some(test_suite_path) = test_suite_path {
flags.push("--anchor-test-config-path".to_string());
flags.push(test_suite_path.display().to_string());
}
}
}
Ok(flags)
}
struct LogStreamHandle {
subscription: PubsubClientSubscription<RpcResponse<RpcLogsResponse>>,
}
impl LogStreamHandle {
fn shutdown(self) {
std::thread::spawn(move || {
let _ = self.subscription.send_unsubscribe();
});
}
}
fn spawn_log_receiver_thread<R>(receiver: R, log_file_path: PathBuf)
where
R: IntoIterator<Item = RpcResponse<RpcLogsResponse>> + Send + 'static,
{
std::thread::spawn(move || {
if let Ok(mut file) = File::create(&log_file_path) {
for response in receiver {
let _ = writeln!(
file,
"Transaction executed in slot {}:",
response.context.slot
);
let _ = writeln!(file, " Signature: {}", response.value.signature);
let _ = writeln!(
file,
" Status: {}",
response
.value
.err
.map(|err| err.to_string())
.unwrap_or_else(|| "Ok".to_string())
);
let _ = writeln!(file, " Log Messages:");
for log in response.value.logs {
let _ = writeln!(file, " {}", log);
}
let _ = writeln!(file); let _ = file.flush();
}
} else {
eprintln!("Failed to create log file: {:?}", log_file_path);
}
});
}
fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStreamHandle>> {
match &config.validator {
Some(ValidatorType::Surfpool) => {
if config
.surfpool_config
.as_ref()
.and_then(|s| {
s.log_level
.as_ref()
.map(|l| l.to_ascii_lowercase().ne("none"))
})
.unwrap_or(false)
{
println!("Surfpool validator logs: .surfpool/logs/ directory");
}
Ok(vec![])
}
Some(ValidatorType::Legacy) | None => stream_solana_logs(config, rpc_url),
}
}
fn stream_solana_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStreamHandle>> {
let program_logs_dir = Path::new(".anchor").join("program-logs");
if program_logs_dir.exists() {
fs::remove_dir_all(&program_logs_dir)?;
}
fs::create_dir_all(&program_logs_dir)?;
let ws_url = if rpc_url.contains("127.0.0.1") || rpc_url.contains("localhost") {
let rpc_port = rpc_url
.rsplit_once(':')
.and_then(|(_, port)| port.parse::<u16>().ok())
.unwrap_or(DEFAULT_RPC_PORT);
let ws_port = rpc_port + WEBSOCKET_PORT_OFFSET;
let url = format!("ws://127.0.0.1:{}", ws_port);
url
} else {
rpc_url
.replace("https://", "wss://")
.replace("http://", "ws://")
};
std::thread::sleep(std::time::Duration::from_millis(1500));
let mut handles = vec![];
for program in config.read_all_programs()? {
let idl_path = target_dir()?
.join("idl")
.join(&program.lib_name)
.with_extension("json");
let idl = fs::read(&idl_path)?;
let idl = convert_idl(&idl)?;
let log_file_path =
program_logs_dir.join(format!("{}.{}.log", idl.address, program.lib_name));
let program_address = idl.address.clone();
let (client, receiver) = match PubsubClient::logs_subscribe(
&ws_url,
RpcTransactionLogsFilter::Mentions(vec![program_address.clone()]),
RpcTransactionLogsConfig {
commitment: Some(CommitmentConfig::confirmed()),
},
) {
Ok(result) => result,
Err(e) => {
eprintln!(
"Warning: Failed to subscribe to logs for program {}: {}",
program.lib_name, e
);
continue;
}
};
spawn_log_receiver_thread(receiver, log_file_path);
handles.push(LogStreamHandle {
subscription: client,
});
}
if let Some(test) = config.test_validator.as_ref() {
if let Some(genesis) = &test.genesis {
for entry in genesis {
let log_file_path = program_logs_dir.join(&entry.address).with_extension("log");
let address = entry.address.clone();
let (client, receiver) = match PubsubClient::logs_subscribe(
&ws_url,
RpcTransactionLogsFilter::Mentions(vec![address.clone()]),
RpcTransactionLogsConfig {
commitment: Some(CommitmentConfig::confirmed()),
},
) {
Ok(result) => result,
Err(e) => {
eprintln!(
"Warning: Failed to subscribe to logs for genesis program {}: {}",
&entry.address, e
);
continue;
}
};
spawn_log_receiver_thread(receiver, log_file_path);
handles.push(LogStreamHandle {
subscription: client,
});
}
}
}
Ok(handles)
}
fn start_surfpool_validator(
flags: Option<Vec<String>>,
surfpool_config: &Option<SurfpoolConfig>,
full_simnet_mode: bool,
) -> Result<Child> {
let (host, port) = match surfpool_config {
Some(SurfpoolConfig { host, rpc_port, .. }) => (host.clone(), *rpc_port),
_ => (SURFPOOL_HOST.to_string(), DEFAULT_RPC_PORT),
};
let rpc_url = surfpool_rpc_url(surfpool_config);
if std::net::TcpStream::connect_timeout(
&format!("{host}:{port}")
.parse()
.map_err(|err| anyhow!("invalid surfpool host:port `{host}:{port}`: {err}"))?,
std::time::Duration::from_millis(200),
)
.is_ok()
{
return Err(anyhow!(
"port {port} on {host} is already in use - another validator is running there. Kill \
it or set `[surfpool] rpc_port = N` in Anchor.toml to pick a free port."
));
}
let test_validator_stdout = match full_simnet_mode {
true => Stdio::inherit(),
false => Stdio::null(),
};
let mut validator_handle = std::process::Command::new("surfpool")
.arg("start")
.args(flags.unwrap_or_default())
.stdout(test_validator_stdout)
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| anyhow!("Failed to spawn `surfpool`: {e}"))?;
let client = create_client(rpc_url.clone());
let mut count = 0;
let ms_wait = surfpool_config
.as_ref()
.map(|surfpool| surfpool.startup_wait)
.unwrap_or(STARTUP_WAIT);
while count < ms_wait {
if let Ok(Some(status)) = validator_handle.try_wait() {
return Err(anyhow!(
"`surfpool` exited during startup with {status} - see the stderr output above. \
Common causes: port {port} in use, missing deploy artifacts in `target/deploy/`, \
invalid Anchor.toml config."
));
}
let r = client.get_latest_blockhash();
if r.is_ok() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
count += 100;
}
if count >= ms_wait {
eprintln!(
"Unable to get latest blockhash. Surfpool validator does not look started. Check \
.surfpool/logs/ directory for errors. Consider increasing [surfpool.startup_wait] in \
Anchor.toml."
);
validator_handle.kill()?;
std::process::exit(1);
}
loop {
let resp = client
.send::<RpcResponse<SurfnetInfoResponse>>(
RpcRequest::Custom {
method: "surfnet_getSurfnetInfo",
},
serde_json::Value::Null,
)?
.value;
if resp
.runbook_executions
.iter()
.all(|ex| ex.completed_at.is_some())
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
Ok(validator_handle)
}
fn start_solana_test_validator(
cfg: &Config,
test_validator: &Option<TestValidator>,
flags: Option<Vec<String>>,
test_log_stdout: bool,
) -> Result<Child> {
let (test_ledger_directory, test_ledger_log_filename) =
test_validator_file_paths(test_validator)?;
let (test_validator_stdout, test_validator_stderr) = match test_log_stdout {
true => {
let test_validator_stdout_file =
File::create(&test_ledger_log_filename).with_context(|| {
format!(
"Failed to create validator log file {}",
test_ledger_log_filename.display()
)
})?;
let test_validator_sterr_file = test_validator_stdout_file.try_clone()?;
(
Stdio::from(test_validator_stdout_file),
Stdio::from(test_validator_sterr_file),
)
}
false => (Stdio::inherit(), Stdio::inherit()),
};
let rpc_url = test_validator_rpc_url(test_validator);
let rpc_port = cfg
.test_validator
.as_ref()
.and_then(|test| test.validator.as_ref().map(|v| v.rpc_port))
.unwrap_or(DEFAULT_RPC_PORT);
if !portpicker::is_free(rpc_port) {
return Err(anyhow!(
"Your configured rpc port: {rpc_port} is already in use"
));
}
let faucet_port = cfg
.test_validator
.as_ref()
.and_then(|test| test.validator.as_ref().and_then(|v| v.faucet_port))
.unwrap_or(DEFAULT_FAUCET_PORT);
if !portpicker::is_free(faucet_port) {
return Err(anyhow!(
"Your configured faucet port: {faucet_port} is already in use"
));
}
let mut validator_handle = std::process::Command::new("solana-test-validator")
.arg("--ledger")
.arg(test_ledger_directory)
.arg("--mint")
.arg(cfg.wallet_kp()?.pubkey().to_string())
.args(flags.unwrap_or_default())
.stdout(test_validator_stdout)
.stderr(test_validator_stderr)
.spawn()
.map_err(|e| anyhow!("Failed to spawn `solana-test-validator`: {e}"))?;
let client = create_client(rpc_url);
let mut count = 0;
let ms_wait = test_validator
.as_ref()
.map(|test| test.startup_wait)
.unwrap_or(STARTUP_WAIT);
while count < ms_wait {
let r = client.get_latest_blockhash();
if r.is_ok() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
count += 100;
}
if count >= ms_wait {
eprintln!(
"Unable to get latest blockhash. Test validator does not look started. Check \
{test_ledger_log_filename:?} for errors. Consider increasing [test.startup_wait] in \
Anchor.toml."
);
validator_handle.kill()?;
std::process::exit(1);
}
Ok(validator_handle)
}
fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
match test_validator {
Some(TestValidator {
validator: Some(validator),
..
}) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
_ => "http://127.0.0.1:8899".to_string(),
}
}
fn surfpool_rpc_url(surfpool_config: &Option<SurfpoolConfig>) -> String {
match surfpool_config {
Some(SurfpoolConfig { host, rpc_port, .. }) => format!("http://{}:{}", host, rpc_port),
_ => format!("http://{}:{}", SURFPOOL_HOST, DEFAULT_RPC_PORT),
}
}
fn test_validator_file_paths(test_validator: &Option<TestValidator>) -> Result<(PathBuf, PathBuf)> {
let ledger_path = match test_validator {
Some(TestValidator {
validator: Some(validator),
..
}) => PathBuf::from(&validator.ledger),
_ => get_default_ledger_path(),
};
if !ledger_path.is_relative() {
eprintln!("Ledger directory {ledger_path:?} must be relative");
std::process::exit(1);
}
if ledger_path.exists() {
fs::remove_dir_all(&ledger_path).with_context(|| {
format!(
"Failed to remove ledger directory {}",
ledger_path.display()
)
})?;
}
fs::create_dir_all(&ledger_path).with_context(|| {
format!(
"Failed to create ledger directory {}",
ledger_path.display()
)
})?;
let log_path = ledger_path.join("test-ledger-log.txt");
Ok((ledger_path, log_path))
}
pub(crate) fn cluster_url(
cfg: &Config,
test_validator: &Option<TestValidator>,
surfpool_config: &Option<SurfpoolConfig>,
) -> String {
let is_localnet = cfg.provider.cluster == Cluster::Localnet;
match is_localnet {
true => match &cfg.validator {
Some(ValidatorType::Surfpool) => surfpool_rpc_url(surfpool_config),
Some(ValidatorType::Legacy) | None => test_validator_rpc_url(test_validator),
},
false => cfg.provider.cluster.url().to_string(),
}
}
fn clean(cfg_override: &ConfigOverride) -> Result<()> {
let workspace_root = if let Ok(Some(cfg)) = Config::discover(cfg_override) {
cfg.path()
.parent()
.expect("Invalid Anchor.toml")
.to_path_buf()
} else {
std::env::current_dir()?
};
let dot_anchor_dir = workspace_root.join(".anchor");
let target_dir = crate::target_dir()?;
let deploy_dir = target_dir.join("deploy");
if dot_anchor_dir.exists() {
fs::remove_dir_all(&dot_anchor_dir)
.map_err(|e| anyhow!("Could not remove directory {:?}: {}", dot_anchor_dir, e))?;
}
if target_dir.exists() {
for entry in fs::read_dir(target_dir)? {
let path = entry?.path();
if path.is_dir() && path != deploy_dir {
fs::remove_dir_all(&path)
.map_err(|e| anyhow!("Could not remove directory {}: {}", path.display(), e))?;
} else if path.is_file() {
fs::remove_file(&path)
.map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
}
}
} else {
println!("skipping target directory: not found")
}
if deploy_dir.exists() {
for file in fs::read_dir(deploy_dir)? {
let path = file?.path();
if path.extension() != Some(&OsString::from("json")) {
fs::remove_file(&path)
.map_err(|e| anyhow!("Could not remove file {}: {}", path.display(), e))?;
}
}
} else {
println!("skipping deploy directory: not found")
}
Ok(())
}
fn deploy(
cfg_override: &ConfigOverride,
program_name: Option<String>,
program_keypair: Option<PathBuf>,
verifiable: bool,
no_idl: bool,
solana_args: Vec<String>,
) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
let keypair = cfg.provider.wallet.to_string();
cfg.run_hooks(HookType::PreDeploy)?;
println!("Deploying cluster: {url}");
println!("Upgrade authority: {keypair}");
for program in cfg.get_programs(program_name)? {
let binary_path = program.binary_path(verifiable)?;
println!("Deploying program {:?}...", program.lib_name);
println!("Program path: {}...", binary_path.display());
let program_keypair_filepath = match program_keypair.as_ref() {
Some(path) => path.clone(),
None => program.keypair_file()?.path().clone(),
};
program::program_deploy(
cfg_override,
Some(strip_workspace_prefix(binary_path)),
None, Some(strip_workspace_prefix(program_keypair_filepath)),
None, None, None, None, false, no_idl,
false, solana_args.clone(),
)?;
}
println!("Deploy success");
cfg.run_hooks(HookType::PostDeploy)?;
Ok(())
})?
}
fn upgrade(
cfg_override: &ConfigOverride,
program_id: Pubkey,
program_filepath: PathBuf,
max_retries: u32,
solana_args: Vec<String>,
) -> Result<()> {
program::program_upgrade(
cfg_override,
program_id,
Some(program_filepath),
None, None, None, max_retries,
false, solana_args,
)
}
fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
println!("Running migration deploy script");
let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
let cur_dir = std::env::current_dir()?;
let migrations_dir = cur_dir.join("migrations");
let deploy_ts = Path::new("deploy.ts");
let use_ts = Path::new("tsconfig.json").exists() && migrations_dir.join(deploy_ts).exists();
if !Path::new(".anchor").exists() {
fs::create_dir(".anchor")?;
}
std::env::set_current_dir(".anchor")?;
let exit = if use_ts {
let module_path = migrations_dir.join(deploy_ts);
let deploy_script_host_str =
template::deploy_ts_script_host(&url, &module_path.display().to_string());
fs::write(deploy_ts, deploy_script_host_str)?;
let pkg_manager_cmd =
resolve_package_manager(cfg.toolchain.package_manager.clone())?.to_string();
std::process::Command::new(pkg_manager_cmd)
.args([
"run",
"ts-node",
&fs::canonicalize(deploy_ts)?.to_string_lossy(),
])
.env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()?
} else {
let deploy_js = deploy_ts.with_extension("js");
let module_path = migrations_dir.join(&deploy_js);
let deploy_script_host_str =
template::deploy_js_script_host(&url, &module_path.display().to_string());
fs::write(&deploy_js, deploy_script_host_str)?;
std::process::Command::new("node")
.arg(&deploy_js)
.env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()?
};
if !exit.status.success() {
eprintln!("Deploy failed.");
std::process::exit(exit.status.code().unwrap());
}
println!("Deploy complete.");
Ok(())
})?
}
fn set_workspace_dir_or_exit() {
let d = match Config::discover(&ConfigOverride::default()) {
Err(err) => {
println!("Workspace configuration error: {err}");
std::process::exit(1);
}
Ok(d) => d,
};
match d {
None => {
let current_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
println!("Unable to determine current directory");
std::process::exit(1);
}
};
let cargo_toml_path = current_dir.join("Cargo.toml");
if !cargo_toml_path.exists() {
println!(
"Not in a Solana workspace. This command requires either Anchor.toml or a \
Cargo workspace with Solana programs."
);
std::process::exit(1);
}
match program::discover_solana_programs(None) {
Ok(programs) if !programs.is_empty() => {
}
_ => {
println!(
"Not in a Solana workspace. This command requires either Anchor.toml or a \
Cargo workspace with Solana programs."
);
std::process::exit(1);
}
}
}
Some(cfg) => {
match cfg.path().parent() {
None => {
println!("Unable to make new program");
}
Some(parent) => {
if std::env::set_current_dir(parent).is_err() {
println!(
"Not in a Solana workspace. This command requires either Anchor.toml \
or a Cargo workspace with Solana programs."
);
std::process::exit(1);
}
}
};
}
}
}
fn airdrop(cfg_override: &ConfigOverride, amount: f64, pubkey: Option<Pubkey>) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let client = RpcClient::new_with_commitment(cluster_url, CommitmentConfig::confirmed());
let recipient_pubkey = if let Some(pubkey) = pubkey {
pubkey
} else {
let keypair = Keypair::read_from_file(&wallet_path)
.map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
keypair.pubkey()
};
let lamports = (amount * 1_000_000_000.0) as u64;
let starting_balance = client
.get_balance_with_commitment(&recipient_pubkey, CommitmentConfig::confirmed())?
.value;
let recent_blockhash = client
.get_latest_blockhash()
.map_err(|e| anyhow!("Failed to get recent blockhash: {}", e))?;
println!("Requesting airdrop of {} SOL...", amount);
let signature = client
.request_airdrop_with_blockhash(&recipient_pubkey, lamports, &recent_blockhash)
.map_err(|e| anyhow!("Airdrop request failed: {}", e))?;
println!("Signature: {}", signature);
client
.confirm_transaction_with_spinner(&signature, &recent_blockhash, client.commitment())
.map_err(|e| anyhow!("Transaction confirmation failed: {}", e))?;
println!("Airdrop confirmed!");
let balance = wait_for_airdrop_balance(&client, &recipient_pubkey, starting_balance, lamports)?;
println!("Balance: {}", format_sol(balance));
Ok(())
}
fn wait_for_airdrop_balance(
client: &RpcClient,
recipient_pubkey: &Pubkey,
starting_balance: u64,
lamports: u64,
) -> Result<u64> {
let expected_balance = starting_balance.saturating_add(lamports);
let mut last_balance = starting_balance;
for attempt in 0..10 {
let balance = client
.get_balance_with_commitment(recipient_pubkey, CommitmentConfig::confirmed())?
.value;
if balance >= expected_balance {
return Ok(balance);
}
last_balance = balance;
if attempt < 9 {
std::thread::sleep(std::time::Duration::from_millis(500));
}
}
eprintln!(
"warning: confirmed balance has not reflected the airdrop yet; showing latest confirmed \
balance"
);
Ok(last_balance)
}
fn cluster(_cmd: ClusterCommand) -> Result<()> {
println!("Cluster Endpoints:\n");
println!("* Mainnet - https://api.mainnet-beta.solana.com");
println!("* Devnet - https://api.devnet.solana.com");
println!("* Testnet - https://api.testnet.solana.com");
Ok(())
}
fn config_cmd(cfg_override: &ConfigOverride, cmd: ConfigCommand) -> Result<()> {
match cmd {
ConfigCommand::Get => config_get(cfg_override),
ConfigCommand::Set { url, keypair } => config_set(cfg_override, url, keypair),
}
}
fn config_get(cfg_override: &ConfigOverride) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
println!("Anchor Configuration:");
println!();
println!("Cluster: {}", cfg.provider.cluster.url());
println!("Wallet: {}", cfg.provider.wallet);
Ok(())
})?
}
fn config_set(
cfg_override: &ConfigOverride,
url: Option<String>,
keypair: Option<PathBuf>,
) -> Result<()> {
let anchor_toml_path = match Config::discover(cfg_override)? {
Some(cfg) => cfg.path().parent().unwrap().join("Anchor.toml"),
None => bail!("Not in an Anchor workspace"),
};
let mut toml_content =
fs::read_to_string(&anchor_toml_path).context("Failed to read Anchor.toml")?;
let mut toml_doc: toml::Value =
toml::from_str(&toml_content).context("Failed to parse Anchor.toml")?;
let mut updated = false;
if let Some(cluster_url) = url {
let expanded_url = match cluster_url.as_str() {
"m" => "https://api.mainnet-beta.solana.com".to_string(),
"d" => "https://api.devnet.solana.com".to_string(),
"t" => "https://api.testnet.solana.com".to_string(),
"l" => "http://127.0.0.1:8899".to_string(),
_ => cluster_url,
};
if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) {
provider.insert(
"cluster".to_string(),
toml::Value::String(expanded_url.clone()),
);
println!("Updated cluster to: {}", expanded_url);
updated = true;
}
}
if let Some(keypair_path) = keypair {
let expanded_path = shellexpand::tilde(&keypair_path.to_string_lossy()).to_string();
if !Path::new(&expanded_path).exists() {
eprintln!("Warning: Wallet file does not exist: {}", expanded_path);
}
if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) {
provider.insert(
"wallet".to_string(),
toml::Value::String(expanded_path.clone()),
);
println!("Updated wallet to: {}", expanded_path);
updated = true;
}
}
if updated {
toml_content =
toml::to_string_pretty(&toml_doc).context("Failed to serialize Anchor.toml")?;
fs::write(&anchor_toml_path, toml_content).context("Failed to write Anchor.toml")?;
println!("\nConfiguration updated successfully!");
} else {
println!("No changes made. Use --url or --keypair to update settings.");
}
Ok(())
}
fn shell(cfg_override: &ConfigOverride) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
let programs = {
let mut idls: HashMap<String, Idl> = cfg
.read_all_programs()?
.iter()
.filter(|program| program.idl.is_some())
.map(|program| {
(
program.idl.as_ref().unwrap().metadata.name.clone(),
program.idl.clone().unwrap(),
)
})
.collect();
if let Some(programs) = cfg.programs.get(&cfg.provider.cluster) {
let _ = programs
.iter()
.map(|(name, pd)| {
if let Some(idl_fp) = &pd.idl {
let file_str =
fs::read_to_string(idl_fp).expect("Unable to read IDL file");
let idl = serde_json::from_str(&file_str).expect("Idl not readable");
idls.insert(name.clone(), idl);
}
})
.collect::<Vec<_>>();
}
match cfg.programs.get(&cfg.provider.cluster) {
None => Vec::new(),
Some(programs) => programs
.iter()
.filter_map(|(name, program_deployment)| {
Some(ProgramWorkspace {
name: name.to_string(),
program_id: program_deployment.address,
idl: match idls.get(name) {
None => return None,
Some(idl) => idl.clone(),
},
})
})
.collect::<Vec<ProgramWorkspace>>(),
}
};
let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
let js_code = template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?;
let mut child = std::process::Command::new("node")
.args(["-e", &js_code, "-i", "--experimental-repl-await"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| anyhow::format_err!("{}", e))?;
if !child.wait()?.success() {
println!("Error running node shell");
return Ok(());
}
Ok(())
})?
}
fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec<String>) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
let url = cluster_url(cfg, &cfg.test_validator, &cfg.surfpool_config);
let script_cmd = cfg.scripts.get(&script).ok_or_else(|| {
let mut available_scripts: Vec<String> = cfg.scripts.keys().cloned().collect();
available_scripts.sort();
if available_scripts.is_empty() {
anyhow!("Script '{script}' not found. No scripts defined in Anchor.toml.")
} else {
anyhow!(
"Script '{script}' not found.\n\nAvailable scripts:\n {}",
available_scripts.join("\n ")
)
}
})?;
let script_with_args = format!("{script_cmd} {}", script_args.join(" "));
let exit = std::process::Command::new("bash")
.arg("-c")
.arg(&script_with_args)
.env("ANCHOR_PROVIDER_URL", url)
.env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
Ok(())
})?
}
fn keys(cfg_override: &ConfigOverride, cmd: KeysCommand) -> Result<()> {
match cmd {
KeysCommand::List => keys_list(cfg_override),
KeysCommand::Sync { program_name } => keys_sync(cfg_override, program_name),
}
}
fn keys_list(cfg_override: &ConfigOverride) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
for program in cfg.read_all_programs()? {
let pubkey = program.pubkey()?;
println!("{}: {}", program.lib_name, pubkey);
}
Ok(())
})?
}
fn keys_sync(cfg_override: &ConfigOverride, program_name: Option<String>) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
.multi_line(true)
.build()
.unwrap();
let cfg_cluster = cfg.provider.cluster.to_owned();
println!("Syncing program ids for the configured cluster ({cfg_cluster})\n");
let mut changed_src = false;
for program in cfg.get_programs(program_name)? {
let actual_program_id = program.pubkey()?.to_string();
let src_path = program.path.join("src");
let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
for path in files_to_check {
let mut content = match fs::read_to_string(&path) {
Ok(content) => content,
Err(_) => continue,
};
let incorrect_program_id = declare_id_regex
.captures(&content)
.and_then(|captures| captures.get(3))
.filter(|program_id_match| program_id_match.as_str() != actual_program_id);
if let Some(program_id_match) = incorrect_program_id {
println!("Found incorrect program id declaration in {path:?}");
content.replace_range(program_id_match.range(), &actual_program_id);
fs::write(&path, content)?;
changed_src = true;
println!("Updated to {actual_program_id}\n");
break;
}
}
'outer: for (cluster, programs) in &mut cfg.programs {
if cluster != &cfg_cluster {
continue;
}
for (name, deployment) in programs {
if name != &program.lib_name {
continue;
}
if deployment.address.to_string() != actual_program_id {
println!(
"Found incorrect program id declaration in Anchor.toml for the \
program `{name}`"
);
deployment.address = Pubkey::try_from(actual_program_id.as_str()).unwrap();
fs::write(cfg.path(), cfg.to_string())?;
println!("Updated to {actual_program_id}\n");
break 'outer;
}
}
}
}
println!("All program id declarations are synced.");
if changed_src {
println!("Please rebuild the program to update the generated artifacts.")
}
Ok(())
})?
}
enum ProgramIdComparison {
Same,
Mismatch {
lib_name: String,
actual_id: String,
declared_id: String,
},
}
fn check_program_id_mismatch(
cfg: &WithPath<Config>,
program_name: Option<String>,
) -> Result<ProgramIdComparison> {
let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#)
.multi_line(true)
.build()
.unwrap();
for program in cfg.get_programs(program_name)? {
let actual_program_id = program.pubkey()?.to_string();
let src_path = program.path.join("src");
let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")];
for path in files_to_check {
let content = match fs::read_to_string(&path) {
Ok(content) => content,
Err(_) => continue,
};
let incorrect_program_id = declare_id_regex
.captures(&content)
.and_then(|captures| captures.get(3))
.filter(|program_id_match| program_id_match.as_str() != actual_program_id);
if let Some(program_id_match) = incorrect_program_id {
return Ok(ProgramIdComparison::Mismatch {
lib_name: program.lib_name,
actual_id: actual_program_id,
declared_id: program_id_match.as_str().to_string(),
});
}
}
}
Ok(ProgramIdComparison::Same)
}
#[allow(clippy::too_many_arguments)]
fn localnet(
cfg_override: &ConfigOverride,
skip_build: bool,
skip_deploy: bool,
skip_lint: bool,
ignore_keys: bool,
validator_type: ValidatorType,
env_vars: Vec<String>,
cargo_args: Vec<String>,
) -> Result<()> {
with_workspace(cfg_override, |cfg| -> Result<()> {
if !skip_build {
build(
cfg_override,
false,
None,
None,
false,
skip_lint,
ignore_keys,
None,
None,
None,
BootstrapMode::None,
None,
None,
env_vars,
cargo_args,
false,
)?;
}
let generated_accounts = generated_validator_accounts(cfg, &cfg.test_validator)?;
let validator_handle: Option<Child> = match validator_type {
ValidatorType::Surfpool => {
let full_simnet_mode = true;
let flags = Some(surfpool_flags(
cfg,
&cfg.surfpool_config,
full_simnet_mode,
skip_deploy,
None,
&generated_accounts,
)?);
Some(start_surfpool_validator(
flags,
&cfg.surfpool_config,
full_simnet_mode,
)?)
}
ValidatorType::Legacy => {
let flags = Some(validator_flags(
cfg,
&cfg.test_validator,
skip_deploy,
&generated_accounts,
)?);
Some(start_solana_test_validator(
cfg,
&cfg.test_validator,
flags,
false,
)?)
}
};
let url = test_validator_rpc_url(&cfg.test_validator);
let log_streams = match stream_logs(cfg, &url) {
Ok(streams) => {
println!(
"Log streams set up successfully ({} streams)",
streams.len()
);
Some(streams)
}
Err(e) => {
eprintln!("Warning: Failed to setup program log streaming: {:#}", e);
eprintln!(" Program logs will still be visible in the validator output.");
None
}
};
std::io::stdin().lock().lines().next().unwrap().unwrap();
if let Some(mut handle) = validator_handle {
if let Err(err) = handle.kill() {
println!("Failed to kill subprocess {}: {}", handle.id(), err);
}
}
if let Some(log_streams) = log_streams {
for handle in log_streams {
handle.shutdown();
}
}
Ok(())
})?
}
pub fn target_dir() -> Result<&'static Path> {
static TARGET_DIR: OnceLock<PathBuf> = OnceLock::new();
if let Some(path) = TARGET_DIR.get() {
return Ok(path.as_path());
}
let path = target_dir_no_cache()?;
let _ = TARGET_DIR.set(path);
Ok(TARGET_DIR.get().expect("just set").as_path())
}
fn target_dir_no_cache() -> Result<PathBuf> {
let output = std::process::Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version=1"])
.output()
.context("Failed to execute 'cargo metadata'")?;
if !output.status.success() {
let stderr_msg = String::from_utf8_lossy(&output.stderr);
bail!("'cargo metadata' failed with: {stderr_msg}");
}
#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}
let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
.context("Failed to parse 'cargo metadata' output")?;
Ok(metadata.target_directory)
}
pub(crate) fn with_workspace<R>(
cfg_override: &ConfigOverride,
f: impl FnOnce(&mut WithPath<Config>) -> R,
) -> Result<R> {
set_workspace_dir_or_exit();
let mut cfg = Config::discover(cfg_override)
.map_err(|e| anyhow!("Workspace configuration error: {}", e))?
.ok_or_else(|| anyhow!("This command requires an Anchor workspace."))?;
let r = f(&mut cfg);
set_workspace_dir_or_exit();
Ok(r)
}
fn is_hidden(entry: &walkdir::DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| s == "." || s.starts_with('.') || s == "target")
.unwrap_or(false)
}
fn logs_websocket_url(cfg_override: &ConfigOverride, cluster_url: &str) -> String {
let ws_scheme_url = cluster_url
.replace("https://", "wss://")
.replace("http://", "ws://");
let is_local = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1");
if !is_local {
return ws_scheme_url;
}
let default_ws_port = extract_url_port(cluster_url)
.map(|port| port.saturating_add(1))
.unwrap_or(DEFAULT_RPC_PORT + 1);
let ws_port = Config::discover(cfg_override)
.ok()
.flatten()
.and_then(|cfg| {
cfg.surfpool_config
.as_ref()
.and_then(|surfpool| surfpool.ws_port)
})
.unwrap_or(default_ws_port);
replace_url_port(&ws_scheme_url, ws_port)
}
fn extract_url_port(url: &str) -> Option<u16> {
let (_, after_scheme) = url.split_once("://")?;
let host_port_end = after_scheme.find('/').unwrap_or(after_scheme.len());
let (_, port_str) = after_scheme[..host_port_end].rsplit_once(':')?;
port_str.parse().ok()
}
fn replace_url_port(url: &str, new_port: u16) -> String {
let Some((scheme, rest)) = url.split_once("://") else {
return url.to_string();
};
let (host_port_part, tail) = match rest.find('/') {
Some(index) => (&rest[..index], &rest[index..]),
None => (rest, ""),
};
let host = host_port_part
.rsplit_once(':')
.map(|(host, _)| host)
.unwrap_or(host_port_part);
format!("{scheme}://{host}:{new_port}{tail}")
}
fn get_node_version() -> Result<Version> {
let node_version = std::process::Command::new("node")
.arg("--version")
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("node failed: {}", e))?;
parse_node_version(std::str::from_utf8(&node_version.stdout)?)
}
fn parse_node_version(output: &str) -> Result<Version> {
let trimmed = output.trim();
let without_v = trimmed.strip_prefix('v').unwrap_or(trimmed);
Version::parse(without_v).map_err(Into::into)
}
pub const DEFAULT_MAX_SIGN_ATTEMPTS: usize = 5;
fn add_recommended_deployment_solana_args(
client: &RpcClient,
args: Vec<String>,
write_locked_accounts: &[Pubkey],
) -> Result<Vec<String>> {
let mut augmented_args = args.clone();
if !args.contains(&"--with-compute-unit-price".to_string()) {
let priority_fee = get_recommended_micro_lamport_fee(client, write_locked_accounts);
augmented_args.push("--with-compute-unit-price".to_string());
augmented_args.push(priority_fee.to_string());
}
if !args.contains(&"--max-sign-attempts".to_string()) {
augmented_args.push("--max-sign-attempts".to_string());
augmented_args.push(DEFAULT_MAX_SIGN_ATTEMPTS.to_string());
}
Ok(augmented_args)
}
fn get_node_dns_option() -> &'static str {
let Ok(version) = get_node_version() else {
return "";
};
let req = VersionReq::parse(">=16.4.0").unwrap();
if req.matches(&version) {
"--dns-result-order=ipv4first"
} else {
""
}
}
fn strip_workspace_prefix(absolute_path: PathBuf) -> PathBuf {
let workspace_prefix = std::env::current_dir().unwrap();
absolute_path
.strip_prefix(&workspace_prefix)
.unwrap_or(&absolute_path)
.into()
}
pub(crate) fn create_client<U: ToString>(url: U) -> RpcClient {
RpcClient::new_with_commitment(url, CommitmentConfig::confirmed())
}
fn address(cfg_override: &ConfigOverride) -> Result<()> {
let (_cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let keypair = Keypair::read_from_file(&wallet_path)
.map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
println!("{}", keypair.pubkey());
Ok(())
}
fn balance(cfg_override: &ConfigOverride, pubkey: Option<Pubkey>, lamports: bool) -> Result<()> {
let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?;
let client = RpcClient::new(cluster_url);
let account_pubkey = if let Some(pubkey) = pubkey {
pubkey
} else {
let keypair = Keypair::read_from_file(&wallet_path)
.map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?;
keypair.pubkey()
};
let balance = client.get_balance(&account_pubkey)?;
if lamports {
println!("{}", balance);
} else {
println!("{}", format_sol(balance));
}
Ok(())
}
fn epoch(cfg_override: &ConfigOverride) -> Result<()> {
let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
let client = RpcClient::new(cluster_url);
let epoch_info = client.get_epoch_info()?;
println!("{}", epoch_info.epoch);
Ok(())
}
fn epoch_info(cfg_override: &ConfigOverride) -> Result<()> {
let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
let client = RpcClient::new(cluster_url);
let epoch_info = client.get_epoch_info()?;
let first_slot_in_epoch = epoch_info.absolute_slot - epoch_info.slot_index;
let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch;
let epoch_completed_percent =
epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100.0;
let remaining_slots = epoch_info.slots_in_epoch - epoch_info.slot_index;
println!("Block height: {}", epoch_info.block_height);
println!("Slot: {}", epoch_info.absolute_slot);
println!("Epoch: {}", epoch_info.epoch);
if let Some(tx_count) = epoch_info.transaction_count {
println!("Transaction Count: {}", tx_count);
}
println!(
"Epoch Slot Range: [{}..{})",
first_slot_in_epoch, last_slot_in_epoch
);
println!("Epoch Completed Percent: {:>3.3}%", epoch_completed_percent);
println!(
"Epoch Completed Slots: {}/{} ({} remaining)",
epoch_info.slot_index, epoch_info.slots_in_epoch, remaining_slots
);
if let Ok(samples) = client.get_recent_performance_samples(Some(60)) {
let (total_slots, total_secs) =
samples.iter().fold((0u64, 0u64), |(slots, secs), sample| {
(
slots.saturating_add(sample.num_slots),
secs.saturating_add(sample.sample_period_secs as u64),
)
});
if total_slots > 0 {
let avg_slot_time_ms = (total_secs * 1000) / total_slots;
let remaining_secs = (remaining_slots * avg_slot_time_ms) / 1000;
let start_block_time = client
.get_blocks_with_limit(first_slot_in_epoch, 1)
.ok()
.and_then(|slots| slots.first().cloned())
.and_then(|first_actual_block| {
client.get_block_time(first_actual_block).ok().map(|time| {
let slot_diff = first_actual_block.saturating_sub(first_slot_in_epoch);
let time_adjustment = (slot_diff * avg_slot_time_ms / 1000) as i64;
time.saturating_sub(time_adjustment)
})
});
let current_block_time = client.get_block_time(epoch_info.absolute_slot).ok();
let (elapsed_secs, is_estimated) = if let (Some(start_time), Some(current_time)) =
(start_block_time, current_block_time)
{
((current_time - start_time) as u64, false)
} else {
((epoch_info.slot_index * avg_slot_time_ms) / 1000, true)
};
let total_secs = elapsed_secs + remaining_secs;
let estimated_marker = if is_estimated { "*" } else { "" };
println!(
"Epoch Completed Time: {}{}/{} ({} remaining)",
format_duration_secs(elapsed_secs),
estimated_marker,
format_duration_secs(total_secs),
format_duration_secs(remaining_secs)
);
}
}
Ok(())
}
fn format_duration_secs(total_seconds: u64) -> String {
let seconds = total_seconds % 60;
let total_minutes = total_seconds / 60;
let minutes = total_minutes % 60;
let total_hours = total_minutes / 60;
let hours = total_hours % 24;
let days = total_hours / 24;
let mut parts = Vec::new();
if days > 0 {
parts.push(format!("{}day", days));
}
if hours > 0 {
parts.push(format!("{}h", hours));
}
if minutes > 0 {
parts.push(format!("{}m", minutes));
}
if seconds > 0 || parts.is_empty() {
parts.push(format!("{}s", seconds));
}
parts.join(" ")
}
fn logs_subscribe(
cfg_override: &ConfigOverride,
include_votes: bool,
address: Option<Vec<Pubkey>>,
) -> Result<()> {
let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?;
let ws_url = logs_websocket_url(cfg_override, &cluster_url);
println!("Connecting to {}", ws_url);
let filter = match (include_votes, address) {
(true, Some(address)) => {
RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect())
}
(true, None) => RpcTransactionLogsFilter::AllWithVotes,
(false, Some(address)) => {
RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect())
}
(false, None) => RpcTransactionLogsFilter::All,
};
let (_client, receiver) = PubsubClient::logs_subscribe(
&ws_url,
filter,
RpcTransactionLogsConfig {
commitment: cfg_override.commitment.map(|c| CommitmentConfig {
commitment: c.into(),
}),
},
)?;
loop {
match receiver.recv() {
Ok(logs) => {
println!("Transaction executed in slot {}:", logs.context.slot);
println!(" Signature: {}", logs.value.signature);
println!(
" Status: {}",
logs.value
.err
.map(|err| err.to_string())
.unwrap_or_else(|| "Ok".to_string())
);
println!(" Log Messages:");
for log in logs.value.logs {
println!(" {log}");
}
}
Err(err) => {
return Err(anyhow!("Disconnected: {err}"));
}
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
anchor_lang_idl::types::{
IdlGenericArg, IdlInstructionAccount, IdlInstructionAccountItem, IdlPda, IdlSeed,
IdlSeedAccount, IdlTypeDef, IdlTypeDefGeneric,
},
std::collections::{HashMap, HashSet},
tempfile::tempdir,
};
#[test]
fn test_init_accepts_anchor_version() {
let opts =
Opts::try_parse_from(["anchor", "init", "example", "--anchor-version", "v2"]).unwrap();
let Command::Init { anchor_version, .. } = opts.command else {
panic!("expected init command");
};
assert_eq!(anchor_version, AnchorVersion::V2);
}
#[test]
fn test_new_accepts_anchor_version() {
let opts =
Opts::try_parse_from(["anchor", "new", "example", "--anchor-version", "v2"]).unwrap();
let Command::New { anchor_version, .. } = opts.command else {
panic!("expected new command");
};
assert_eq!(anchor_version, AnchorVersion::V2);
}
#[test]
#[cfg(not(windows))]
fn test_debugger_and_coverage_commands_parse() {
let opts =
Opts::try_parse_from(["anchor", "debugger", "initialize", "--skip-run"]).unwrap();
let Command::Debugger {
test_name,
skip_run,
..
} = opts.command
else {
panic!("expected debugger command");
};
assert_eq!(test_name.as_deref(), Some("initialize"));
assert!(skip_run);
let opts =
Opts::try_parse_from(["anchor", "coverage", "--skip-run", "--output", "lcov.info"])
.unwrap();
let Command::Coverage {
skip_run, output, ..
} = opts.command
else {
panic!("expected coverage command");
};
assert!(skip_run);
assert_eq!(output, "lcov.info");
}
#[test]
fn test_validator_defaults_to_surfpool() {
let opts = Opts::try_parse_from(["anchor", "test"]).unwrap();
let Command::Test { validator, .. } = opts.command else {
panic!("expected test command");
};
assert_eq!(validator, ValidatorType::Surfpool);
let opts = Opts::try_parse_from(["anchor", "localnet"]).unwrap();
let Command::Localnet { validator, .. } = opts.command else {
panic!("expected localnet command");
};
assert_eq!(validator, ValidatorType::Surfpool);
}
#[test]
fn test_codama_command_parses() {
let opts = Opts::try_parse_from([
"anchor",
"codama",
"generate",
"-l",
"rust,go",
"-p",
"clients",
"target/idl/demo.json",
])
.unwrap();
let Command::Codama { subcmd } = opts.command else {
panic!("expected codama command");
};
let codama::CodamaCommand::Generate {
language,
path,
idl,
} = subcmd
else {
panic!("expected codama generate command");
};
assert_eq!(language, vec![codama::Language::Rust, codama::Language::Go]);
assert_eq!(path, "clients");
assert_eq!(idl, "target/idl/demo.json");
}
#[test]
#[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
fn test_init_reserved_word() {
init(
&ConfigOverride {
cluster: None,
wallet: None,
commitment: None,
},
"await".to_string(),
true,
true,
None,
false,
ProgramTemplate::default(),
AnchorVersion::default(),
TestTemplate::default(),
true,
true,
)
.unwrap();
}
#[test]
#[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
fn test_init_reserved_word_from_syn() {
init(
&ConfigOverride {
cluster: None,
wallet: None,
commitment: None,
},
"fn".to_string(),
true,
true,
None,
false,
ProgramTemplate::default(),
AnchorVersion::default(),
TestTemplate::default(),
true,
true,
)
.unwrap();
}
#[test]
#[should_panic(expected = "Anchor workspace name must be a valid Rust identifier.")]
fn test_init_starting_with_digit() {
init(
&ConfigOverride {
cluster: None,
wallet: None,
commitment: None,
},
"1project".to_string(),
true,
true,
None,
false,
ProgramTemplate::default(),
AnchorVersion::default(),
TestTemplate::default(),
true,
true,
)
.unwrap();
}
fn index_set(indices: &[usize]) -> HashSet<usize> {
indices.iter().copied().collect()
}
#[test]
fn program_order_prefers_programs_with_more_anchor_program_deps() {
let program_indices = index_set(&[0, 1]);
let program_closures = HashMap::from([(0, index_set(&[1])), (1, index_set(&[]))]);
let original_order = HashMap::from([(0, 0), (1, 1)]);
let ordered = order_program_indices_by_dependency_cache_heuristic(
&program_indices,
&program_closures,
&original_order,
);
assert_eq!(ordered, vec![0, 1]);
}
#[test]
fn program_order_uses_total_dependency_closure_as_tiebreaker() {
let program_indices = index_set(&[0, 1, 2, 3]);
let program_closures = HashMap::from([
(0, index_set(&[2, 4])),
(1, index_set(&[3])),
(2, index_set(&[])),
(3, index_set(&[])),
]);
let original_order = HashMap::from([(0, 1), (1, 0), (2, 2), (3, 3)]);
let ordered = order_program_indices_by_dependency_cache_heuristic(
&program_indices,
&program_closures,
&original_order,
);
assert_eq!(ordered, vec![0, 1, 2, 3]);
}
#[test]
fn program_order_places_isolated_programs_first() {
let program_indices = index_set(&[0, 1, 2]);
let program_closures = HashMap::from([
(0, index_set(&[])),
(1, index_set(&[2])),
(2, index_set(&[])),
]);
let original_order = HashMap::from([(0, 0), (1, 1), (2, 2)]);
let ordered = order_program_indices_by_dependency_cache_heuristic(
&program_indices,
&program_closures,
&original_order,
);
assert_eq!(ordered, vec![0, 1, 2]);
}
#[test]
fn program_order_preserves_original_order_for_unrelated_programs() {
let program_indices = index_set(&[0, 1, 2]);
let program_closures = HashMap::from([
(0, index_set(&[3])),
(1, index_set(&[])),
(2, index_set(&[])),
]);
let original_order = HashMap::from([(0, 0), (1, 1), (2, 2)]);
let ordered = order_program_indices_by_dependency_cache_heuristic(
&program_indices,
&program_closures,
&original_order,
);
assert_eq!(ordered, vec![0, 1, 2]);
}
#[test]
fn test_predeploy_preserves_explicit_external_validator() {
assert!(should_predeploy_before_test(false, false, false));
assert!(should_predeploy_before_test(false, true, true));
assert!(!should_predeploy_before_test(false, true, false));
assert!(!should_predeploy_before_test(true, true, true));
}
#[test]
fn test_validator_plan_handles_in_process_template_skip() {
assert_eq!(
test_validator_plan(false, true, false, true),
TestValidatorPlan {
skip_local_validator: true,
predeploy: false,
stream_program_logs: true,
}
);
}
#[test]
fn test_validator_plan_preserves_explicit_external_validator() {
assert_eq!(
test_validator_plan(false, true, true, false),
TestValidatorPlan {
skip_local_validator: true,
predeploy: true,
stream_program_logs: true,
}
);
}
#[test]
fn surfpool_flags_do_not_force_runtime_features() {
let dir = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), dir.path().join("Anchor.toml"));
let flags = surfpool_flags(&cfg, &None, false, false, None, &[]).unwrap();
assert!(!flags.iter().any(|flag| flag == "--feature"));
}
#[test]
fn surfpool_flags_include_snapshot_for_generated_accounts() {
let workspace = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
fund_accounts: Some(vec![crate::config::FundedAccount {
address: "new".to_string(),
lamports: Some(2_000_000_000),
}]),
..Default::default()
}),
..Default::default()
});
let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
let flags = surfpool_flags(&cfg, &None, false, false, None, &generated_accounts).unwrap();
let snapshot_index = flags.iter().position(|flag| flag == "--snapshot").unwrap();
let snapshot_path = PathBuf::from(&flags[snapshot_index + 1]);
let snapshot: JsonValue =
serde_json::from_reader(File::open(&snapshot_path).unwrap()).unwrap();
assert!(snapshot_path.exists());
assert!(snapshot
.get(generated_accounts[0].pubkey.to_string())
.is_some());
}
#[test]
fn test_jest_package_json_pins_uuid_for_commonjs() {
for package_json in [
template::package_json(true, "ISC".to_owned(), AnchorVersion::V1),
template::ts_package_json(true, "ISC".to_owned(), AnchorVersion::V1),
] {
let package: JsonValue = serde_json::from_str(&package_json).unwrap();
assert_eq!(package["overrides"]["uuid"], "^9.0.1");
assert_eq!(package["resolutions"]["uuid"], "^9.0.1");
assert_eq!(package["pnpm"]["overrides"]["uuid"], "^9.0.1");
}
}
#[test]
fn parse_node_version_with_v_prefix() {
let version = parse_node_version("v20.10.0\n").unwrap();
assert_eq!(version.major, 20);
assert_eq!(version.minor, 10);
assert_eq!(version.patch, 0);
}
#[test]
fn parse_node_version_without_v_prefix() {
let version = parse_node_version("20.10.0").unwrap();
assert_eq!(version.major, 20);
}
#[test]
fn parse_node_version_ignores_surrounding_whitespace() {
let version = parse_node_version(" v18.17.1 \n").unwrap();
assert_eq!(version.major, 18);
assert_eq!(version.minor, 17);
}
#[test]
fn parse_node_version_errors_on_garbage() {
assert!(parse_node_version("not a version").is_err());
assert!(parse_node_version("").is_err());
}
#[test]
fn extract_url_port_common_shapes() {
assert_eq!(extract_url_port("http://127.0.0.1:8899"), Some(8899));
assert_eq!(extract_url_port("http://127.0.0.1:8899/"), Some(8899));
assert_eq!(extract_url_port("ws://localhost:8900/path?q=1"), Some(8900));
assert_eq!(
extract_url_port("https://api.mainnet-beta.solana.com"),
None
);
assert_eq!(extract_url_port("http://127.0.0.1"), None);
assert_eq!(extract_url_port("not a url"), None);
}
#[test]
fn replace_url_port_preserves_structure() {
assert_eq!(
replace_url_port("http://127.0.0.1:8899", 9001),
"http://127.0.0.1:9001"
);
assert_eq!(
replace_url_port("ws://127.0.0.1:8899/path?q=1", 9050),
"ws://127.0.0.1:9050/path?q=1"
);
assert_eq!(
replace_url_port("http://127.0.0.1", 8900),
"http://127.0.0.1:8900"
);
}
#[test]
fn idl_ts_preserves_literal_values() {
let idl = Idl {
address: "11111111111111111111111111111111".to_string(),
metadata: anchor_lang_idl::types::IdlMetadata {
name: "test_program".to_string(),
version: "0.1.0".to_string(),
spec: "0.1.0".to_string(),
description: None,
repository: None,
dependencies: Vec::new(),
contact: None,
deployments: None,
},
docs: Vec::new(),
instructions: vec![anchor_lang_idl::types::IdlInstruction {
name: "do_thing".to_string(),
docs: Vec::new(),
discriminator: vec![0, 1, 2, 3, 4, 5, 6, 7],
accounts: vec![IdlInstructionAccountItem::Single(IdlInstructionAccount {
name: "target_account".to_string(),
docs: Vec::new(),
writable: false,
signer: false,
optional: false,
address: None,
pda: Some(IdlPda {
seeds: vec![IdlSeed::Account(IdlSeedAccount {
path: "source_account.authority".to_string(),
account: Some("source_account".to_string()),
})],
program: None,
}),
relations: vec!["source_account".to_string()],
})],
args: vec![anchor_lang_idl::types::IdlField {
name: "some_arg".to_string(),
docs: Vec::new(),
ty: IdlType::U8,
}],
returns: None,
}],
accounts: vec![anchor_lang_idl::types::IdlAccount {
name: "source_account".to_string(),
discriminator: vec![8, 7, 6, 5, 4, 3, 2, 1],
}],
events: Vec::new(),
errors: vec![anchor_lang_idl::types::IdlErrorCode {
code: 6000,
name: "Unauthorized".to_string(),
msg: Some("Unauthorized".to_string()),
}],
types: vec![IdlTypeDef {
name: "wrapper_type".to_string(),
docs: Vec::new(),
serialization: Default::default(),
repr: None,
generics: vec![IdlTypeDefGeneric::Type {
name: "item_type".to_string(),
}],
ty: IdlTypeDefTy::Type {
alias: IdlType::Defined {
name: "generic_holder".to_string(),
generics: vec![
IdlGenericArg::Type {
ty: IdlType::Generic("item_type".to_string()),
},
IdlGenericArg::Const {
value: "SEED_PREFIX".to_string(),
},
],
},
},
}],
constants: vec![anchor_lang_idl::types::IdlConst {
name: "seed_prefix".to_string(),
docs: Vec::new(),
ty: IdlType::String,
value: "SEED_PREFIX".to_string(),
}],
};
let ts = idl_ts(&idl).unwrap();
assert!(ts.contains(r#""name": "doThing""#));
assert!(ts.contains(r#""name": "targetAccount""#));
assert!(ts.contains(r#""path": "sourceAccount.authority""#));
assert!(ts.contains(r#""account": "sourceAccount""#));
assert!(ts.contains(r#""sourceAccount""#));
assert!(ts.contains(r#""name": "someArg""#));
assert!(ts.contains(r#""name": "sourceAccount""#));
assert!(ts.contains(r#""name": "unauthorized""#));
assert!(ts.contains(r#""msg": "Unauthorized""#));
assert!(ts.contains(r#""name": "wrapperType""#));
assert!(ts.contains(r#""name": "itemType""#));
assert!(ts.contains(r#""name": "genericHolder""#));
assert!(ts.contains(r#""generic": "itemType""#));
assert!(ts.contains(r#""name": "seedPrefix""#));
assert!(ts.contains(r#""value": "SEED_PREFIX""#));
}
const TEST_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[test]
fn apply_program_id_override_current_spec_sets_top_level_address() {
let idl = serde_json::json!({
"address": "11111111111111111111111111111111",
"metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
"instructions": [],
})
.to_string();
let out = apply_program_id_override(idl.as_bytes(), TEST_PROGRAM_ID).unwrap();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["address"], TEST_PROGRAM_ID.to_string());
assert_eq!(v["metadata"]["spec"], "0.1.0");
assert_eq!(v["metadata"]["name"], "demo");
assert_eq!(v["metadata"]["version"], "0.1.0");
}
#[test]
fn apply_program_id_override_legacy_merges_into_existing_metadata() {
let idl = serde_json::json!({
"version": "0.1.0",
"name": "demo",
"instructions": [],
"metadata": { "origin": "anchor", "address": "11111111111111111111111111111111" },
})
.to_string();
let out = apply_program_id_override(idl.as_bytes(), TEST_PROGRAM_ID).unwrap();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["metadata"]["address"], TEST_PROGRAM_ID.to_string());
assert_eq!(v["metadata"]["origin"], "anchor");
}
#[test]
fn apply_program_id_override_legacy_no_metadata_creates_object() {
let idl = serde_json::json!({
"version": "0.1.0",
"name": "demo",
"instructions": [],
})
.to_string();
let out = apply_program_id_override(idl.as_bytes(), TEST_PROGRAM_ID).unwrap();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["metadata"]["address"], TEST_PROGRAM_ID.to_string());
}
#[test]
fn skip_deploy_preserves_legacy_validator_config_flags() {
let cfg = WithPath::new(Config::default(), PathBuf::from("Anchor.toml"));
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
warp_slot: Some(42),
..Default::default()
}),
..Default::default()
});
let flags = validator_flags(&cfg, &test_validator, true, &[]).unwrap();
assert!(flags
.windows(2)
.any(|args| args[0] == "--rpc-port" && args[1] == "18999"));
assert!(flags
.windows(2)
.any(|args| args[0] == "--warp-slot" && args[1] == "42"));
assert!(!flags.iter().any(|arg| arg == "--bpf-program"));
assert!(!flags.iter().any(|arg| arg == "--upgradeable-program"));
}
#[test]
fn validator_flags_emits_extra_args() {
let workspace = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
let expected = vec![
"--rpc-pubsub-enable-block-subscription".to_string(),
"--geyser-plugin-config".to_string(),
"geyser.json".to_string(),
];
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
extra_args: Some(expected.clone()),
..Default::default()
}),
..Default::default()
});
let flags = validator_flags(&cfg, &test_validator, true, &[]).unwrap();
assert!(flags
.windows(expected.len())
.any(|args| args == expected.as_slice()));
}
#[test]
fn skip_deploy_keeps_generated_account_flags() {
let workspace = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
let funded_pubkey = Pubkey::new_unique();
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
fund_accounts: Some(vec![crate::config::FundedAccount {
address: funded_pubkey.to_string(),
lamports: Some(2_000_000_000),
}]),
..Default::default()
}),
..Default::default()
});
let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
let flags = validator_flags(&cfg, &test_validator, true, &generated_accounts).unwrap();
let expected_path = generated_accounts[0].file_path.display().to_string();
assert_eq!(generated_accounts.len(), 1);
assert!(flags.windows(3).any(|args| {
args[0] == "--account"
&& args[1] == funded_pubkey.to_string()
&& args[2] == expected_path
}));
}
#[test]
fn token_account_requires_loaded_explicit_mint() {
let workspace = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
let missing_mint = Pubkey::new_unique();
let owner = Pubkey::new_unique();
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
token_accounts: Some(vec![crate::config::TokenAccount {
mint: missing_mint.to_string(),
owner: owner.to_string(),
amount: 1,
address: None,
}]),
..Default::default()
}),
..Default::default()
});
let err = generated_validator_accounts(&cfg, &test_validator).unwrap_err();
assert!(err.to_string().contains("token_account mint"));
}
#[test]
fn token_account_accepts_cloned_explicit_mint() {
let workspace = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
let cloned_mint = Pubkey::new_unique();
let owner = Pubkey::new_unique();
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
clone: Some(vec![crate::config::CloneEntry {
address: cloned_mint.to_string(),
}]),
token_accounts: Some(vec![crate::config::TokenAccount {
mint: cloned_mint.to_string(),
owner: owner.to_string(),
amount: 1,
address: None,
}]),
..Default::default()
}),
..Default::default()
});
let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
assert_eq!(generated_accounts.len(), 1);
assert!(generated_accounts[0]
.file_path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.ends_with(".json")));
}
#[test]
fn multiple_new_funded_accounts_get_distinct_pubkeys() {
let workspace = tempdir().unwrap();
let cfg = WithPath::new(Config::default(), workspace.path().join("Anchor.toml"));
let test_validator = Some(TestValidator {
validator: Some(crate::config::Validator {
bind_address: "127.0.0.1".to_string(),
ledger: ".anchor/test-ledger".to_string(),
rpc_port: 18999,
fund_accounts: Some(vec![
crate::config::FundedAccount {
address: "new".to_string(),
lamports: Some(15_000_000_000_000),
},
crate::config::FundedAccount {
address: "new".to_string(),
lamports: Some(20_000_000_000_000),
},
]),
..Default::default()
}),
..Default::default()
});
let generated_accounts = generated_validator_accounts(&cfg, &test_validator).unwrap();
assert_eq!(generated_accounts.len(), 2);
assert_ne!(generated_accounts[0].pubkey, generated_accounts[1].pubkey);
}
}