feat(cli): Replace `anchor verify` with a robust `solana-verify` wrapper (#3768)

* feat: add support for tuple types in space calculation

This commit introduces the ability to handle tuple types in the space calculation logic. A new `TestTupleStruct` is added to validate various tuple configurations, including nested and option types, ensuring accurate space calculations through comprehensive unit tests.

* clean up unnacessary comments

* chore: fmt fix

* fix: fixing test assert

* fix: fixing test assert

* feat: replace anchor verify to use solana verify under the hood

* feat: Enhance installation process with support for local paths and solana-verify integration

- Made `get_bin_dir_path` public to allow external access.
- Updated `InstallTarget` enum to include a `Path` variant for local repository installations.
- Modified `install_version` function to handle installation from a local path, including reading the version from the Cargo.toml manifest.
- Integrated installation of `solana-verify` during the installation process.
- Updated CLI commands to support the new path option for installation.
- Enhanced verification command to allow for repository URL or current directory options.

* Refactor: Resolve avm binary collision by unifying proxy logic

* feat: Add symlink creation for anchor.exe on Windows

* fix: fmt

* feat: Specify version for solana-verify installation

* feat: Improve anchor command proxying and add testing script

- Updated the main function to enhance the logic for determining if the binary is named `anchor`.
- Introduced a new test script to verify that `anchor` commands correctly proxy to the installed Anchor CLI binary, ensuring expected behavior for both `avm` and `anchor` commands.

* docs: Add caution note regarding mainnet wallet usage in verification test script

* fix: CI fix

* docs: add feature to the changelog

* chore: fix clippy complains (#3776)

* chore: fix clippy complains

* fix lints

* simplify some parts

---------

Co-authored-by: Aursen <aursen@users.noreply.github.com>

* fix: correct median priority fee calculation in get_recommended_micro_lamport_fee function

* fix: clippy

---------

Co-authored-by: Jean (Exotic Markets) <jeanno11@orange.fr>
Co-authored-by: Aursen <aursen@users.noreply.github.com>
This commit is contained in:
Arthur Bretas 2025-07-17 17:24:47 -03:00 committed by GitHub
parent bf495ac3df
commit 702cbde3e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 427 additions and 274 deletions

View File

@ -13,6 +13,7 @@ The minor version will be incremented upon a breaking change and the patch versi
### Features
- lang: Add `#[error]` attribute to `declare_program!` ([#3757](https://github.com/coral-xyz/anchor/pull/3757)).
- cli: Replace `anchor verify` to use `solana-verify` under the hood, adding automatic installation via AVM, local path support, and future-proof argument passing ([#3768](https://github.com/solana-foundation/anchor/pull/3768)).
### Fixes

View File

@ -7,10 +7,6 @@ edition = "2021"
name = "avm"
path = "src/main.rs"
[[bin]]
name = "anchor"
path = "src/anchor/main.rs"
[dependencies]
anyhow = "1.0.32"
cargo_toml = "0.19.2"

View File

@ -1,29 +0,0 @@
use std::{env, process::Command};
fn main() -> anyhow::Result<()> {
let args = env::args().skip(1).collect::<Vec<String>>();
let version = avm::current_version()
.map_err(|_e| anyhow::anyhow!("Anchor version not set. Please run `avm use latest`."))?;
let binary_path = avm::version_binary_path(&version);
if !binary_path.exists() {
anyhow::bail!(
"anchor-cli {} not installed. Please run `avm use {}`.",
version,
version
);
}
let exit = Command::new(binary_path)
.args(args)
.spawn()?
.wait_with_output()
.expect("Failed to run anchor-cli");
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
Ok(())
}

View File

@ -35,7 +35,7 @@ fn current_version_file_path() -> PathBuf {
}
/// Path to the current version file $AVM_HOME/bin
fn get_bin_dir_path() -> PathBuf {
pub fn get_bin_dir_path() -> PathBuf {
AVM_HOME.join("bin")
}
@ -53,7 +53,45 @@ pub fn ensure_paths() {
let bin_dir = get_bin_dir_path();
if !bin_dir.exists() {
fs::create_dir_all(bin_dir).expect("Could not create .avm/bin directory");
fs::create_dir_all(&bin_dir).expect("Could not create .avm/bin directory");
}
// Copy the `avm` binary to `~/.avm/bin` so we can create symlinks to it.
let avm_in_bin = bin_dir.join("avm");
if let Ok(current_avm) = std::env::current_exe() {
// Only copy if the paths are different
if current_avm != avm_in_bin {
if let Err(e) = fs::copy(current_avm, &avm_in_bin) {
eprintln!("Failed to copy avm binary: {e}");
}
}
}
// Create a symlink from `anchor` to `avm` so that the user can run `anchor`
// from the command line.
#[cfg(unix)]
{
let anchor_in_bin = bin_dir.join("anchor");
if !anchor_in_bin.exists() {
if let Err(e) = std::os::unix::fs::symlink(&avm_in_bin, anchor_in_bin) {
eprintln!("Failed to create symlink: {e}");
}
}
}
// On Windows, we create a symlink named `anchor.exe` pointing to the `avm.exe` binary in the bin directory,
// so that the user can run `anchor` from the command line.
// Note: Creating symlinks on Windows may require administrator privileges or that Developer Mode is enabled.
#[cfg(windows)]
{
use std::os::windows::fs::symlink_file;
let anchor_in_bin = bin_dir.join("anchor.exe");
if !anchor_in_bin.exists() {
if let Err(e) = symlink_file(&avm_in_bin, &anchor_in_bin) {
eprintln!("Failed to create symlink: {}", e);
}
}
}
if !current_version_file_path().exists() {
@ -102,6 +140,7 @@ pub fn use_version(opt_version: Option<Version>) -> Result<()> {
pub enum InstallTarget {
Version(Version),
Commit(String),
Path(PathBuf),
}
/// Update to the latest version
@ -170,9 +209,21 @@ pub fn install_version(
force: bool,
from_source: bool,
) -> Result<()> {
let version = match &install_target {
InstallTarget::Version(version) => version.to_owned(),
InstallTarget::Commit(commit) => get_anchor_version_from_commit(commit)?,
let (version, from_source) = match &install_target {
InstallTarget::Version(version) => (version.to_owned(), from_source),
InstallTarget::Commit(commit) => (get_anchor_version_from_commit(commit)?, true),
InstallTarget::Path(path) => {
let manifest_path = path.join("cli/Cargo.toml");
let manifest = Manifest::from_path(&manifest_path).map_err(|e| {
anyhow!(
"Failed to read manifest at {}: {}",
manifest_path.display(),
e
)
})?;
let version = manifest.package().version().parse::<Version>()?;
(version, true)
}
};
// Return early if version is already installed
if !force && read_installed_versions()?.contains(&version) {
@ -183,21 +234,44 @@ pub fn install_version(
let is_commit = matches!(install_target, InstallTarget::Commit(_));
let is_older_than_v0_31_0 = version < Version::parse("0.31.0")?;
if from_source || is_commit || is_older_than_v0_31_0 {
// Build from source using `cargo install --git`
// Build from source using `cargo install`
let mut args: Vec<String> = vec![
"install".into(),
"anchor-cli".into(),
"--git".into(),
"https://github.com/coral-xyz/anchor".into(),
"--locked".into(),
"--root".into(),
AVM_HOME.to_str().unwrap().into(),
];
let conditional_args = match install_target {
InstallTarget::Version(version) => ["--tag".into(), format!("v{version}")],
InstallTarget::Commit(commit) => ["--rev".into(), commit],
};
args.extend_from_slice(&conditional_args);
match install_target {
InstallTarget::Version(version) => {
args.extend_from_slice(&[
"--git".into(),
"https://github.com/coral-xyz/anchor".into(),
"--tag".into(),
format!("v{version}"),
]);
}
InstallTarget::Commit(commit) => {
args.extend_from_slice(&[
"--git".into(),
"https://github.com/coral-xyz/anchor".into(),
"--rev".into(),
commit,
]);
}
InstallTarget::Path(path) => {
let cli_path = path.join("cli");
let path_str = cli_path
.to_str()
.ok_or_else(|| anyhow!("Invalid path string"))?;
args.extend_from_slice(&[
"--path".into(),
path_str.to_string(),
"--bin".into(),
"anchor".into(),
]);
}
}
// If the version is older than v0.31, install using `rustc 1.79.0` to get around the problem
// explained in https://github.com/coral-xyz/anchor/pull/3143
@ -280,6 +354,30 @@ pub fn install_version(
)?;
}
println!("Installing solana-verify...");
let solana_verify_install_output = Command::new("cargo")
.args([
"install",
"solana-verify",
"--git",
"https://github.com/Ellipsis-Labs/solana-verifiable-build",
"--rev",
"568cb334709e88b9b45fc24f1f440eecacf5db54",
"--root",
AVM_HOME.to_str().unwrap(),
"--force",
"--locked",
])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow!("`cargo install` for `solana-verify` failed: {e}"))?;
if !solana_verify_install_output.status.success() {
return Err(anyhow!("Failed to install `solana-verify`"));
}
println!("solana-verify successfully installed.");
// If .version file is empty or not parseable, write the newly installed version to it
if current_version().is_err() {
let mut current_version_file = fs::File::create(current_version_file_path())?;

View File

@ -2,6 +2,7 @@ use anyhow::{anyhow, Error, Result};
use avm::InstallTarget;
use clap::{CommandFactory, Parser, Subcommand};
use semver::Version;
use std::ffi::OsStr;
#[derive(Parser)]
#[clap(name = "avm", about = "Anchor version manager", version)]
@ -19,9 +20,12 @@ pub enum Commands {
},
#[clap(about = "Install a version of Anchor", alias = "i")]
Install {
/// Anchor version or commit
#[clap(value_parser = parse_install_target)]
version_or_commit: InstallTarget,
/// Anchor version or commit, conflicts with `--path`
#[clap(required_unless_present = "path")]
version_or_commit: Option<String>,
/// Path to local anchor repo, conflicts with `version_or_commit`
#[clap(long, conflicts_with = "version_or_commit")]
path: Option<String>,
#[clap(long)]
/// Flag to force installation even if the version
/// is already installed
@ -51,27 +55,21 @@ fn parse_version(version: &str) -> Result<Version, Error> {
if version == "latest" {
avm::get_latest_version()
} else {
Version::parse(version).map_err(|e| anyhow!(e))
Ok(Version::parse(version)?)
}
}
fn parse_install_target(version_or_commit: &str) -> Result<InstallTarget, Error> {
parse_version(version_or_commit)
.map(|version| {
if version.pre.is_empty() {
InstallTarget::Version(version)
} else {
// Allow `avm install 0.28.0-6cf200493a307c01487c7b492b4893e0d6f6cb23`
InstallTarget::Commit(version.pre.to_string())
}
})
.or_else(|version_error| {
avm::check_and_get_full_commit(version_or_commit)
.map(InstallTarget::Commit)
.map_err(|commit_error| {
anyhow!("Not a valid version or commit: {version_error}, {commit_error}")
})
})
if let Ok(version) = parse_version(version_or_commit) {
if version.pre.is_empty() {
return Ok(InstallTarget::Version(version));
}
// Allow for e.g. `avm install 0.28.0-6cf200493a307c01487c7b492b4893e0d6f6cb23`
return Ok(InstallTarget::Commit(version.pre.to_string()));
}
avm::check_and_get_full_commit(version_or_commit)
.map(InstallTarget::Commit)
.map_err(|e| anyhow!("Not a valid version or commit: {e}"))
}
pub fn entry(opts: Cli) -> Result<()> {
@ -79,9 +77,17 @@ pub fn entry(opts: Cli) -> Result<()> {
Commands::Use { version } => avm::use_version(version),
Commands::Install {
version_or_commit,
path,
force,
from_source,
} => avm::install_version(version_or_commit, force, from_source),
} => {
let install_target = if let Some(path) = path {
InstallTarget::Path(path.into())
} else {
parse_install_target(&version_or_commit.unwrap())?
};
avm::install_version(install_target, force, from_source)
}
Commands::Uninstall { version } => avm::uninstall_version(&version),
Commands::List {} => avm::list_versions(),
Commands::Update {} => avm::update(),
@ -92,7 +98,54 @@ pub fn entry(opts: Cli) -> Result<()> {
}
}
fn anchor_proxy() -> Result<()> {
let args = std::env::args().skip(1).collect::<Vec<String>>();
let version = avm::current_version()
.map_err(|_e| anyhow::anyhow!("Anchor version not set. Please run `avm use latest`."))?;
let binary_path = avm::version_binary_path(&version);
if !binary_path.exists() {
anyhow::bail!(
"anchor-cli {} not installed. Please run `avm use {}`.",
version,
version
);
}
let exit = std::process::Command::new(binary_path)
.args(args)
.env(
"PATH",
format!(
"{}:{}",
avm::get_bin_dir_path().to_string_lossy(),
std::env::var("PATH").unwrap_or_default()
),
)
.spawn()?
.wait_with_output()
.expect("Failed to run anchor-cli");
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
Ok(())
}
fn main() -> Result<()> {
// If the binary is named `anchor` then run the proxy.
if let Some(stem) = std::env::args()
.next()
.as_ref()
.and_then(|s| std::path::Path::new(s).file_stem().and_then(OsStr::to_str))
{
if stem == "anchor" {
return anchor_proxy();
}
}
// Make sure the user's home directory is setup with the paths required by AVM.
avm::ensure_paths();

View File

@ -25,9 +25,6 @@ use semver::{Version, VersionReq};
use serde::Deserialize;
use serde_json::{json, Map, Value as JsonValue};
use solana_client::rpc_client::RpcClient;
use solana_sdk::account_utils::StateMut;
use solana_sdk::bpf_loader;
use solana_sdk::bpf_loader_deprecated;
use solana_sdk::bpf_loader_upgradeable::{self, UpgradeableLoaderState};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::compute_budget::ComputeBudgetInstruction;
@ -161,34 +158,23 @@ pub enum Command {
/// Run this command inside a program subdirectory, i.e., in the dir
/// containing the program's Cargo.toml.
Verify {
/// The deployed program to compare against.
/// The program ID to verify.
program_id: Pubkey,
#[clap(short, long)]
/// The URL of the repository to verify against. Conflicts with `--current-dir`.
#[clap(long, conflicts_with = "current_dir")]
repo_url: Option<String>,
/// The commit hash to verify against. Requires `--repo-url`.
#[clap(long, requires = "repo_url")]
commit_hash: Option<String>,
/// Verify against the source code in the current directory. Conflicts with `--repo-url`.
#[clap(long)]
current_dir: bool,
/// Name of the program to run the command on. Defaults to the package name.
#[clap(long)]
program_name: Option<String>,
/// Version of the Solana toolchain to use. For --verifiable builds
/// only.
#[clap(short, long)]
solana_version: Option<String>,
/// Docker image to use. For --verifiable builds only.
#[clap(short, long)]
docker_image: Option<String>,
/// Bootstrap docker image from scratch, installing all requirements for
/// verifiable builds. Only works for debian-based images.
#[clap(value_enum, short, long, default_value = "none")]
bootstrap: BootstrapMode,
/// Architecture to use when building the program
#[clap(value_enum, long, default_value = "sbf")]
arch: ProgramArch,
/// Environment variables to pass into the docker container
#[clap(short, long, required = false)]
env: Vec<String>,
/// Arguments to pass to the underlying `cargo build-sbf` command.
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
/// Flag to skip building the program in the workspace,
/// use this to save time when running verify and the program code is already built.
#[clap(long, required = false)]
skip_build: bool,
/// Any additional arguments to pass to `solana-verify`.
#[clap(raw = true)]
args: Vec<String>,
},
#[clap(name = "test", alias = "t")]
/// Runs integration tests.
@ -820,25 +806,18 @@ fn process_command(opts: Opts) -> Result<()> {
),
Command::Verify {
program_id,
repo_url,
commit_hash,
current_dir,
program_name,
solana_version,
docker_image,
bootstrap,
env,
cargo_args,
skip_build,
arch,
args,
} => verify(
&opts.cfg_override,
program_id,
repo_url,
commit_hash,
current_dir,
program_name,
solana_version,
docker_image,
bootstrap,
env,
cargo_args,
skip_build,
arch,
args,
),
Command::Clean => clean(&opts.cfg_override),
Command::Deploy {
@ -2044,81 +2023,62 @@ fn _build_solidity_cwd(
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn verify(
cfg_override: &ConfigOverride,
pub fn verify(
program_id: Pubkey,
repo_url: Option<String>,
commit_hash: Option<String>,
current_dir: bool,
program_name: Option<String>,
solana_version: Option<String>,
docker_image: Option<String>,
bootstrap: BootstrapMode,
env_vars: Vec<String>,
cargo_args: Vec<String>,
skip_build: bool,
arch: ProgramArch,
args: Vec<String>,
) -> Result<()> {
// Change to the workspace member directory, if needed.
if let Some(program_name) = program_name.as_ref() {
cd_member(cfg_override, program_name)?;
}
let mut command_args = Vec::new();
// Proceed with the command.
let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
let cargo = Manifest::discover()?.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
// Build the program we want to verify.
let cur_dir = std::env::current_dir()?;
if !skip_build {
build(
cfg_override,
true,
None,
None,
true,
true,
None,
solana_version.or_else(|| cfg.toolchain.solana_version.clone()),
docker_image,
bootstrap,
None,
None,
env_vars,
cargo_args.clone(),
false,
arch,
)?;
}
std::env::set_current_dir(cur_dir)?;
// Verify binary.
let binary_name = cargo.lib_name()?;
let bin_path = cfg
.path()
.parent()
.ok_or_else(|| anyhow!("Unable to find workspace root"))?
.join("target")
.join("verifiable")
.join(&binary_name)
.with_extension("so");
let url = cluster_url(&cfg, &cfg.test_validator);
let bin_ver = verify_bin(program_id, &bin_path, &url)?;
if !bin_ver.is_verified {
println!("Error: Binaries don't match");
std::process::exit(1);
}
// Verify IDL (only if it's not a buffer account).
let local_idl = generate_idl(&cfg, true, false, &cargo_args)?;
if bin_ver.state != BinVerificationState::Buffer {
let deployed_idl = fetch_idl(cfg_override, program_id).map(serde_json::from_value)??;
if local_idl != deployed_idl {
println!("Error: IDLs don't match");
std::process::exit(1);
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"
));
}
}
println!("{program_id} is verified.");
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 status = std::process::Command::new("solana-verify")
.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(())
}
@ -2155,97 +2115,6 @@ fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> {
Err(anyhow!("{} is not part of the workspace", program_name,))
}
pub fn verify_bin(program_id: Pubkey, bin_path: &Path, cluster: &str) -> Result<BinVerification> {
// Use `finalized` state for verify
let client = RpcClient::new_with_commitment(cluster, CommitmentConfig::finalized());
// Get the deployed build artifacts.
let (deployed_bin, state) = {
let account = client.get_account(&program_id)?;
if account.owner == bpf_loader::id() || account.owner == bpf_loader_deprecated::id() {
let bin = account.data.to_vec();
let state = BinVerificationState::ProgramData {
slot: 0, // Need to look through the transaction history.
upgrade_authority_address: None,
};
(bin, state)
} else if account.owner == bpf_loader_upgradeable::id() {
match account.state()? {
UpgradeableLoaderState::Program {
programdata_address,
} => {
let account = client.get_account(&programdata_address)?;
let bin = account.data
[UpgradeableLoaderState::size_of_programdata_metadata()..]
.to_vec();
if let UpgradeableLoaderState::ProgramData {
slot,
upgrade_authority_address,
} = account.state()?
{
let state = BinVerificationState::ProgramData {
slot,
upgrade_authority_address,
};
(bin, state)
} else {
return Err(anyhow!("Expected program data"));
}
}
UpgradeableLoaderState::Buffer { .. } => {
let offset = UpgradeableLoaderState::size_of_buffer_metadata();
(
account.data[offset..].to_vec(),
BinVerificationState::Buffer,
)
}
_ => {
return Err(anyhow!(
"Invalid program id, not a buffer or program account"
))
}
}
} else {
return Err(anyhow!(
"Invalid program id, not owned by any loader program"
));
}
};
let mut local_bin = {
let mut f = File::open(bin_path)?;
let mut contents = vec![];
f.read_to_end(&mut contents)?;
contents
};
// The deployed program probably has zero bytes appended. The default is
// 2x the binary size in case of an upgrade.
if local_bin.len() < deployed_bin.len() {
local_bin.append(&mut vec![0; deployed_bin.len() - local_bin.len()]);
}
// Finally, check the bytes.
let is_verified = local_bin == deployed_bin;
Ok(BinVerification { state, is_verified })
}
#[derive(PartialEq, Eq)]
pub struct BinVerification {
pub state: BinVerificationState,
pub is_verified: bool,
}
#[derive(PartialEq, Eq)]
pub enum BinVerificationState {
Buffer,
ProgramData {
slot: u64,
upgrade_authority_address: Option<Pubkey>,
},
}
fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
match subcmd {
IdlCommand::Init {
@ -4822,7 +4691,7 @@ fn get_recommended_micro_lamport_fee(client: &RpcClient) -> Result<u64> {
fees.sort_unstable_by_key(|fee| fee.prioritization_fee);
let median_index = fees.len() / 2;
let median_priority_fee = if fees.len().is_multiple_of(2) {
let median_priority_fee = if fees.len() % 2 == 0 {
(fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2
} else {
fees[median_index].prioritization_fee

View File

@ -360,12 +360,15 @@ pub fn generate_constraint_raw(ident: &Ident, c: &ConstraintRaw) -> proc_macro2:
pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2::TokenStream {
let ident = &f.ident;
let maybe_deref = match &f.ty {
let maybe_deref = if match &f.ty {
Ty::Account(AccountTy { boxed, .. })
| Ty::InterfaceAccount(InterfaceAccountTy { boxed, .. }) => *boxed,
_ => false,
}
.then_some(quote!(*));
} {
quote!(*)
} else {
Default::default()
};
let owner_address = &c.owner_address;
let error = generate_custom_error(
ident,

View File

@ -0,0 +1,72 @@
#!/bin/bash
# This test script verifies that running 'anchor' commands
# correctly proxies to the installed Anchor CLI binary (e.g., shows Anchor "something")
# instead of launching AVM itself.
# Exit on first error
set -e
# --- Helper for timing and logging ---
function step_start {
echo ""
echo "============================================================"
echo "🚀 Starting Step: $1"
echo "============================================================"
STEP_START_TIME=$(date +%s)
}
function step_end {
local end_time=$(date +%s)
local duration=$((end_time - STEP_START_TIME))
echo "✅ Step completed in ${duration}s."
}
trap 'echo ""; echo "🧹 Cleaning up..."' INT TERM EXIT
step_start "Building local avm"
(cd ../.. && cargo build --package avm)
step_end
step_start "Installing local anchor-cli via avm (force to ensure fresh install)"
../../target/debug/avm install --path ../.. --force
step_end
# --- Set a Specific Version ---
step_start "Setting AVM to use a known version (e.g., latest)"
../../target/debug/avm use latest
step_end
# --- Test 'avm' Command (Should Show AVM Help) ---
step_start "Testing 'avm --help' (should show AVM usage)"
AVM_OUTPUT=$(~/.avm/bin/avm --help 2>&1) || true
if echo "$AVM_OUTPUT" | grep -q "Anchor version manager"; then
echo "✅ 'avm --help' shows AVM usage as expected."
else
echo "❌ Test failed: 'avm --help' did not show expected AVM output."
echo "$AVM_OUTPUT"
exit 1
fi
step_end
# --- Test 'anchor' Command (Should Proxy to Anchor CLI) ---
step_start "Testing 'anchor --version' (should show Anchor CLI version, not AVM)"
ANCHOR_OUTPUT=$(~/.avm/bin/anchor --version 2>&1) || true
if echo "$ANCHOR_OUTPUT" | grep -q "anchor-cli"; then
echo "✅ 'anchor --version' proxies to Anchor CLI successfully."
elif echo "$ANCHOR_OUTPUT" | grep -q "Anchor version manager"; then
echo "❌ Test failed: 'anchor --version' is still launching AVM instead of proxying."
echo "$ANCHOR_OUTPUT"
exit 1
else
echo "❌ Test failed: Unexpected output from 'anchor --version'."
echo "$ANCHOR_OUTPUT"
exit 1
fi
step_end
echo ""
echo "============================================================"
echo "🎉 All tests passed successfully! 🎉"
echo "============================================================"

View File

@ -0,0 +1,90 @@
#!/bin/bash
# This test script verifies that `avm` correctly installs `solana-verify`
# and that `anchor verify` can successfully verify a known public program.
# Exit on first error
set -e
# --- Helper for timing and logging ---
function step_start {
echo ""
echo "============================================================"
echo "🚀 Starting Step: $1"
echo "============================================================"
STEP_START_TIME=$(date +%s)
}
function step_end {
local end_time=$(date +%s)
local duration=$((end_time - STEP_START_TIME))
echo "✅ Step completed in ${duration}s."
}
# --- Cleanup ---
trap 'echo ""; echo "🧹 Cleaning up..."' INT TERM EXIT
# --- Dependency Checks ---
step_start "Checking dependencies"
# Check for docker
if ! command -v docker &> /dev/null; then
echo "❌ Docker is not installed. Please install it to run this test."
exit 1
fi
if ! docker info > /dev/null 2>&1; then
echo "❌ Docker is not running. Please start Docker and run the test again."
exit 1
fi
step_end
# --- Build and Install Anchor from Local Source ---
step_start "Building local avm"
(cd ../.. && cargo build --package avm)
step_end
step_start "Installing local anchor-cli and solana-verify via avm"
../../target/debug/avm install --path ../.. --force
step_end
# --- Verify `solana-verify` Installation ---
step_start "Checking for solana-verify installation"
if ! [ -x ~/.avm/bin/solana-verify ]; then
echo "❌ solana-verify was not found in ~/.avm/bin."
exit 1
fi
echo "-> solana-verify found successfully."
step_end
# --- Verify a Known Public Program ---
step_start "Verifying a known public program (Phoenix on Mainnet)"
OUTPUT=$(../../target/debug/anchor verify PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY \
--repo-url https://github.com/Ellipsis-Labs/phoenix-v1 \
-- \
--skip-prompt \
--url https://api.mainnet-beta.solana.com 2>&1) || true
step_end
# --- Check Results ---
step_start "Checking verification results"
if echo "$OUTPUT" | grep -q "Program hash matches ✅"; then
# This is the expected outcome in a test environment without a funded mainnet wallet.
# ⚠️ CAUTION: if you have a mainnet wallet with funds, and configured the CLI to use it, this will fail. And you will lose money.
if echo "$OUTPUT" | grep -q "Failed to send verification transaction to the blockchain"; then
echo "✅ Test successful: Verification passed and transaction failed as expected."
else
echo "❌ Test failed: Verification passed, but an unexpected error occurred."
echo "$OUTPUT"
exit 1
fi
else
echo "❌ Test failed: Verification did not pass."
echo "$OUTPUT"
exit 1
fi
step_end
echo ""
echo "============================================================"
echo "🎉 All tests passed successfully! 🎉"
echo "============================================================"