use {
crate::{
config::ProgramWorkspace, create_files, override_or_create_files, AbsolutePath, Files,
PackageManager, VERSION,
},
anyhow::Result,
clap::{Parser, ValueEnum},
heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase},
solana_keypair::{read_keypair_file, write_keypair_file, Keypair},
solana_pubkey::Pubkey,
solana_signer::Signer,
std::{
fmt::Write as _,
fs::{self, File},
io::Write as _,
path::Path,
process::Stdio,
},
};
const ANCHOR_MSRV: &str = "1.89.0";
const ANCHOR_V2_TEMPLATE_VERSION: &str = "2.0.0";
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
pub enum AnchorVersion {
#[default]
V1,
V2,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
pub enum ProgramTemplate {
Single,
#[default]
Multiple,
}
pub fn create_program(
name: &str,
template: ProgramTemplate,
test_template: Option<&TestTemplate>,
anchor_version: AnchorVersion,
) -> Result<()> {
let program_path = Path::new("programs").join(name);
let lib_rs_path = program_path.join("src").join("lib.rs");
let common_files = vec![
("Cargo.toml".into(), workspace_manifest()),
("rust-toolchain.toml".into(), rust_toolchain_toml()),
(
program_path.join("Cargo.toml"),
cargo_toml(name, test_template, anchor_version),
),
(lib_rs_path.clone(), "".into()),
];
create_files(&common_files)?;
let target_path = crate::target_dir()?;
fs::remove_file(&lib_rs_path)?;
let template_files = match template {
ProgramTemplate::Single => {
println!(
"Note: Using single-file template. For better code organization and \
maintainability, consider using --template multiple (default)."
);
create_program_template_single(name, &program_path, target_path, anchor_version)
}
ProgramTemplate::Multiple => {
create_program_template_multiple(name, &program_path, target_path, anchor_version)
}
};
create_files(&template_files)
}
fn rust_toolchain_toml() -> String {
format!(
r#"[toolchain]
channel = "{ANCHOR_MSRV}"
components = ["rustfmt","clippy"]
profile = "minimal"
"#
)
}
fn create_program_template_single(
name: &str,
program_path: &Path,
target_path: &Path,
anchor_version: AnchorVersion,
) -> Files {
match anchor_version {
AnchorVersion::V1 => create_program_template_single_v1(name, program_path, target_path),
AnchorVersion::V2 => create_program_template_single_v2(name, program_path, target_path),
}
}
fn create_program_template_single_v1(name: &str, program_path: &Path, target_path: &Path) -> Files {
vec![(
program_path.join("src").join("lib.rs"),
format!(
r#"use anchor_lang::prelude::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {{
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
}};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}}
pub fn increment(ctx: Context<Increment>) -> Result<()> {{
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {{}}", ctx.accounts.counter.count);
Ok(())
}}
}}
#[derive(Accounts)]
pub struct Initialize<'info> {{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}}
#[derive(Accounts)]
pub struct Increment<'info> {{
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}}
pub mod constants {{
use super::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
}}
pub mod error {{
use super::*;
#[error_code]
pub enum ErrorCode {{
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}}
}}
pub mod state {{
use super::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {{
pub count: u64,
pub authority: Pubkey,
}}
}}
use constants::*;
use error::ErrorCode;
use state::Counter;
"#,
get_or_create_program_id(name, target_path),
name.to_snake_case(),
),
)]
}
fn create_program_template_single_v2(name: &str, program_path: &Path, target_path: &Path) -> Files {
vec![(
program_path.join("src").join("lib.rs"),
format!(
r#"use anchor_lang_v2::prelude::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {{
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = *ctx.accounts.payer.address();
msg!("Counter initialized");
Ok(())
}}
}}
pub mod state {{
use super::*;
#[account]
pub struct Counter {{
pub count: u64,
pub authority: Address,
}}
}}
use state::Counter;
#[derive(Accounts)]
pub struct Initialize {{
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}}
"#,
get_or_create_program_id(name, target_path),
name.to_snake_case(),
),
)]
}
fn create_program_template_multiple(
name: &str,
program_path: &Path,
target_path: &Path,
anchor_version: AnchorVersion,
) -> Files {
match anchor_version {
AnchorVersion::V1 => create_program_template_multiple_v1(name, program_path, target_path),
AnchorVersion::V2 => create_program_template_multiple_v2(name, program_path, target_path),
}
}
fn create_program_template_multiple_v1(
name: &str,
program_path: &Path,
target_path: &Path,
) -> Files {
let src_path = program_path.join("src");
vec![
(
src_path.join("lib.rs"),
format!(
r#"pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
crate::instructions::initialize::handle_initialize(ctx)
}}
pub fn increment(ctx: Context<Increment>) -> Result<()> {{
crate::instructions::increment::handle_increment(ctx)
}}
}}
"#,
get_or_create_program_id(name, target_path),
name.to_snake_case(),
),
),
(
src_path.join("constants.rs"),
r#"use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
"#
.into(),
),
(
src_path.join("error.rs"),
r#"use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
"#
.into(),
),
(
src_path.join("instructions.rs"),
r#"pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;
"#
.into(),
),
(
src_path.join("instructions").join("initialize.rs"),
r#"use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}
"#
.into(),
),
(
src_path.join("instructions").join("increment.rs"),
r#"use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}
"#
.into(),
),
(
src_path.join("state.rs"),
r#"use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
"#
.into(),
),
]
}
fn create_program_template_multiple_v2(
name: &str,
program_path: &Path,
target_path: &Path,
) -> Files {
let src_path = program_path.join("src");
vec![
(
src_path.join("lib.rs"),
format!(
r#"pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang_v2::prelude::*;
pub use instructions::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {{
initialize::handler(ctx)
}}
}}
"#,
get_or_create_program_id(name, target_path),
name.to_snake_case(),
),
),
(
src_path.join("constants.rs"),
r#"use anchor_lang_v2::prelude::*;
#[constant]
pub const SEED: &str = "anchor";
"#
.into(),
),
(
src_path.join("error.rs"),
r#"use anchor_lang_v2::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Custom error message")]
CustomError,
}
"#
.into(),
),
(
src_path.join("instructions.rs"),
r#"pub mod initialize;
pub use initialize::*;
"#
.into(),
),
(
src_path.join("instructions").join("initialize.rs"),
r#"use anchor_lang_v2::prelude::*;
use crate::state::Counter;
#[derive(Accounts)]
pub struct Initialize {
#[account(mut)]
pub payer: Signer,
#[account(init, payer = payer)]
pub counter: Account<Counter>,
pub system_program: Program<System>,
}
pub fn handler(ctx: &mut Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = *ctx.accounts.payer.address();
msg!("Counter initialized");
Ok(())
}
"#
.into(),
),
(
src_path.join("state.rs"),
r#"use anchor_lang_v2::prelude::*;
#[account]
pub struct Counter {
pub count: u64,
pub authority: Address,
}
"#
.into(),
),
]
}
fn workspace_manifest() -> String {
format!(
r#"[workspace]
members = [
"programs/*"
]
resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "{ANCHOR_MSRV}"
[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
"#
)
}
fn cargo_toml(
name: &str,
test_template: Option<&TestTemplate>,
anchor_version: AnchorVersion,
) -> String {
match anchor_version {
AnchorVersion::V1 => cargo_toml_v1(name, test_template),
AnchorVersion::V2 => cargo_toml_v2(name, test_template),
}
}
fn cargo_toml_v1(name: &str, test_template: Option<&TestTemplate>) -> String {
let template_features = match test_template {
Some(TestTemplate::Mollusk) => r#"test-sbf = []"#,
_ => "",
};
let dev_dependencies = match test_template {
Some(TestTemplate::Mollusk) => {
r#"
[dev-dependencies]
mollusk-svm = "~0.10"
solana-account = "3"
solana-pubkey = "3"
solana-sdk-ids = "3"
"#
}
Some(TestTemplate::Litesvm) => {
r#"
[dev-dependencies]
litesvm = "0.10.0"
solana-message = "3.0.1"
solana-transaction = "3.0.2"
solana-signer = "3.0.0"
solana-keypair = "3.0.1"
"#
}
_ => "",
};
format!(
r#"[package]
name = "{0}"
version = "0.1.0"
description = "Created with Anchor"
edition.workspace = true
rust-version.workspace = true
[lib]
crate-type = ["cdylib", "lib"]
name = "{1}"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
anchor-debug = []
custom-heap = []
custom-panic = []
{2}
[dependencies]
anchor-lang = "{3}"
{4}
[lints.rust]
unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }}
"#,
name,
name.to_snake_case(),
template_features,
VERSION,
dev_dependencies,
)
}
fn cargo_toml_v2(name: &str, test_template: Option<&TestTemplate>) -> String {
let template_features = match test_template {
Some(TestTemplate::Mollusk) => r#"test-sbf = []"#,
Some(TestTemplate::Litesvm) => r#"profile = ["anchor-v2-testing/profile"]"#,
_ => "",
};
let dev_dependencies = match test_template {
Some(TestTemplate::Mollusk) => {
r#"
[dev-dependencies]
mollusk-svm = "~0.10"
solana-account = "3"
solana-pubkey = "4"
solana-sdk-ids = "3"
bytemuck = "1"
"#
}
Some(TestTemplate::Litesvm) => {
r#"
[dev-dependencies]
anchor-v2-testing = { git = "https://github.com/solana-foundation/anchor.git", branch = "anchor-next" }
"#
}
_ => "",
};
format!(
r#"[package]
name = "{0}"
version = "0.1.0"
description = "Created with Anchor"
edition.workspace = true
rust-version.workspace = true
[lib]
crate-type = ["cdylib", "lib"]
name = "{1}"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-log-ix-name = []
idl-build = []
{2}
[dependencies]
# Once anchor-lang-v2 is published to crates.io, swap to: anchor-lang-v2 = "{3}"
anchor-lang-v2 = {{ git = "https://github.com/solana-foundation/anchor.git", branch = "anchor-next" }}
solana-program-log = {{ version = "1.1", features = ["macro"] }}
wincode = {{ version = "0.5", features = ["derive"] }}
{4}
[lints.rust]
unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }}
"#,
name,
name.to_snake_case(),
template_features,
ANCHOR_V2_TEMPLATE_VERSION,
dev_dependencies,
)
}
pub fn get_or_create_program_id(name: &str, target_path: impl AsRef<Path>) -> Pubkey {
let keypair_path = target_path
.as_ref()
.join("deploy")
.join(format!("{}-keypair.json", name.to_snake_case()));
read_keypair_file(&keypair_path)
.unwrap_or_else(|_| {
let keypair = Keypair::new();
write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
keypair
})
.pubkey()
}
pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
format!(
r#"
const anchor = require('@anchor-lang/core');
// Deploy script defined by the user.
const userScript = require("{script_path}");
async function main() {{
const connection = new anchor.web3.Connection(
"{cluster_url}",
anchor.AnchorProvider.defaultOptions().commitment
);
const wallet = anchor.Wallet.local();
const provider = new anchor.AnchorProvider(connection, wallet);
// Run the user's deploy script.
userScript(provider);
}}
main();
"#,
)
}
pub fn deploy_ts_script_host(cluster_url: &str, script_path: &str) -> String {
format!(
r#"import * as anchor from '@anchor-lang/core';
// Deploy script defined by the user.
const userScript = require("{script_path}");
async function main() {{
const connection = new anchor.web3.Connection(
"{cluster_url}",
anchor.AnchorProvider.defaultOptions().commitment
);
const wallet = anchor.Wallet.local();
const provider = new anchor.AnchorProvider(connection, wallet);
// Run the user's deploy script.
userScript(provider);
}}
main();
"#,
)
}
pub fn deploy_script() -> &'static str {
r#"// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
const anchor = require("@anchor-lang/core");
module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
};
"#
}
pub fn ts_deploy_script() -> &'static str {
r#"// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
import * as anchor from "@anchor-lang/core";
module.exports = async function (provider: anchor.AnchorProvider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
};
"#
}
pub fn mocha(name: &str, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => mocha_v1(name),
AnchorVersion::V2 => mocha_v2(name),
}
}
fn mocha_v1(name: &str) -> String {
format!(
r#"const anchor = require("@anchor-lang/core");
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
it("Initializes and increments a counter", async () => {{
const program = anchor.workspace.{};
const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("counter")],
program.programId
);
const initializeTx = await program.methods
.initialize()
.accountsPartial({{ counter }})
.rpc();
console.log("Initialize transaction signature", initializeTx);
const incrementTx = await program.methods
.increment()
.accountsPartial({{ counter }})
.rpc();
console.log("Increment transaction signature", incrementTx);
}});
}});
"#,
name,
name.to_lower_camel_case(),
)
}
fn mocha_v2(name: &str) -> String {
format!(
r#"const anchor = require("@anchor-lang/core");
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
it("Is initialized!", async () => {{
// Add your test here.
const program = anchor.workspace.{};
const counter = anchor.web3.Keypair.generate();
const tx = await program.methods
.initialize()
.accounts({{ counter: counter.publicKey }})
.signers([counter])
.rpc();
console.log("Your transaction signature", tx);
}});
}});
"#,
name,
name.to_lower_camel_case(),
)
}
pub fn js_jest(name: &str, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => js_jest_v1(name),
AnchorVersion::V2 => js_jest_v2(name),
}
}
fn js_jest_v1(name: &str) -> String {
format!(
r#"const anchor = require("@anchor-lang/core");
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
it("Initializes and increments a counter", async () => {{
const program = anchor.workspace.{};
const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("counter")],
program.programId
);
const initializeTx = await program.methods
.initialize()
.accountsPartial({{ counter }})
.rpc();
console.log("Initialize transaction signature", initializeTx);
const incrementTx = await program.methods
.increment()
.accountsPartial({{ counter }})
.rpc();
console.log("Increment transaction signature", incrementTx);
}});
}});
"#,
name,
name.to_lower_camel_case(),
)
}
fn js_jest_v2(name: &str) -> String {
format!(
r#"const anchor = require("@anchor-lang/core");
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
it("Is initialized!", async () => {{
// Add your test here.
const program = anchor.workspace.{};
const counter = anchor.web3.Keypair.generate();
const tx = await program.methods
.initialize()
.accounts({{ counter: counter.publicKey }})
.signers([counter])
.rpc();
console.log("Your transaction signature", tx);
}});
}});
"#,
name,
name.to_lower_camel_case(),
)
}
pub fn package_json(jest: bool, license: String, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => package_json_v1(jest, license),
AnchorVersion::V2 => package_json_v2(jest, license),
}
}
fn package_json_v1(jest: bool, license: String) -> String {
if jest {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^{VERSION}"
}},
"devDependencies": {{
"jest": "^29.0.3",
"prettier": "^2.6.2"
}},
"overrides": {{
"uuid": "^9.0.1"
}},
"resolutions": {{
"uuid": "^9.0.1"
}},
"pnpm": {{
"overrides": {{
"uuid": "^9.0.1"
}}
}}
}}
"#
)
} else {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^{VERSION}"
}},
"devDependencies": {{
"chai": "^4.3.4",
"mocha": "^9.0.3",
"prettier": "^2.6.2"
}}
}}
"#
)
}
}
fn package_json_v2(jest: bool, license: String) -> String {
if jest {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^1.0.0"
}},
"devDependencies": {{
"jest": "^30.3.0",
"prettier": "^3.8.3"
}},
"overrides": {{
"uuid": "^9.0.1"
}},
"resolutions": {{
"uuid": "^9.0.1"
}},
"pnpm": {{
"overrides": {{
"uuid": "^9.0.1"
}}
}}
}}
"#
)
} else {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^1.0.0"
}},
"devDependencies": {{
"chai": "^4.5.0",
"mocha": "^11.7.5",
"prettier": "^3.8.3"
}}
}}
"#
)
}
}
pub fn ts_package_json(jest: bool, license: String, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => ts_package_json_v1(jest, license),
AnchorVersion::V2 => ts_package_json_v2(jest, license),
}
}
fn ts_package_json_v1(jest: bool, license: String) -> String {
if jest {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^{VERSION}"
}},
"devDependencies": {{
"@types/bn.js": "^5.1.0",
"@types/jest": "^29.0.3",
"jest": "^29.0.3",
"prettier": "^2.6.2",
"ts-jest": "^29.0.2",
"typescript": "^5.7.3"
}},
"overrides": {{
"uuid": "^9.0.1"
}},
"resolutions": {{
"uuid": "^9.0.1"
}},
"pnpm": {{
"overrides": {{
"uuid": "^9.0.1"
}}
}}
}}
"#
)
} else {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^{VERSION}"
}},
"devDependencies": {{
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^10.0.0",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"typescript": "^5.7.3",
"prettier": "^2.6.2"
}}
}}
"#
)
}
}
fn ts_package_json_v2(jest: bool, license: String) -> String {
if jest {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^1.0.0"
}},
"devDependencies": {{
"@types/bn.js": "^5.2.0",
"@types/jest": "^30.0.0",
"jest": "^30.3.0",
"prettier": "^3.8.3",
"ts-jest": "^29.4.9",
"typescript": "^5.9.3"
}},
"overrides": {{
"uuid": "^9.0.1"
}},
"resolutions": {{
"uuid": "^9.0.1"
}},
"pnpm": {{
"overrides": {{
"uuid": "^9.0.1"
}}
}}
}}
"#
)
} else {
format!(
r#"{{
"license": "{license}",
"scripts": {{
"lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
"lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
}},
"dependencies": {{
"@anchor-lang/core": "^1.0.0"
}},
"devDependencies": {{
"chai": "^4.5.0",
"mocha": "^11.7.5",
"ts-mocha": "^11.1.0",
"ts-node": "^10.9.2",
"@types/bn.js": "^5.2.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^10.0.10",
"@types/node": "^25.6.0",
"typescript": "^5.9.3",
"prettier": "^3.8.3"
}}
}}
"#
)
}
}
pub fn ts_mocha(name: &str, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => ts_mocha_v1(name),
AnchorVersion::V2 => ts_mocha_v2(name),
}
}
fn ts_mocha_v1(name: &str) -> String {
format!(
r#"import * as anchor from "@anchor-lang/core";
import {{ Program }} from "@anchor-lang/core";
import {{ {} }} from "../target/types/{}";
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.{} as Program<{}>;
it("Initializes and increments a counter", async () => {{
const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("counter")],
program.programId
);
const initializeTx = await program.methods
.initialize()
.accountsPartial({{ counter }})
.rpc();
console.log("Initialize transaction signature", initializeTx);
const incrementTx = await program.methods
.increment()
.accountsPartial({{ counter }})
.rpc();
console.log("Increment transaction signature", incrementTx);
}});
}});
"#,
name.to_pascal_case(),
name.to_snake_case(),
name,
name.to_lower_camel_case(),
name.to_pascal_case(),
)
}
fn ts_mocha_v2(name: &str) -> String {
format!(
r#"import * as anchor from "@anchor-lang/core";
import {{ Program }} from "@anchor-lang/core";
import {{ {} }} from "../target/types/{}";
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.{} as Program<{}>;
it("Is initialized!", async () => {{
// Add your test here.
const counter = anchor.web3.Keypair.generate();
const tx = await program.methods
.initialize()
.accounts({{ counter: counter.publicKey }})
.signers([counter])
.rpc();
console.log("Your transaction signature", tx);
}});
}});
"#,
name.to_pascal_case(),
name.to_snake_case(),
name,
name.to_lower_camel_case(),
name.to_pascal_case(),
)
}
pub fn ts_jest(name: &str, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => ts_jest_v1(name),
AnchorVersion::V2 => ts_jest_v2(name),
}
}
fn ts_jest_v1(name: &str) -> String {
format!(
r#"import * as anchor from "@anchor-lang/core";
import {{ Program }} from "@anchor-lang/core";
import {{ {} }} from "../target/types/{}";
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.{} as Program<{}>;
it("Initializes and increments a counter", async () => {{
const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("counter")],
program.programId
);
const initializeTx = await program.methods
.initialize()
.accountsPartial({{ counter }})
.rpc();
console.log("Initialize transaction signature", initializeTx);
const incrementTx = await program.methods
.increment()
.accountsPartial({{ counter }})
.rpc();
console.log("Increment transaction signature", incrementTx);
}});
}});
"#,
name.to_pascal_case(),
name.to_snake_case(),
name,
name.to_lower_camel_case(),
name.to_pascal_case(),
)
}
fn ts_jest_v2(name: &str) -> String {
format!(
r#"import * as anchor from "@anchor-lang/core";
import {{ Program }} from "@anchor-lang/core";
import {{ {} }} from "../target/types/{}";
describe("{}", () => {{
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.{} as Program<{}>;
it("Is initialized!", async () => {{
// Add your test here.
const counter = anchor.web3.Keypair.generate();
const tx = await program.methods
.initialize()
.accounts({{ counter: counter.publicKey }})
.signers([counter])
.rpc();
console.log("Your transaction signature", tx);
}});
}});
"#,
name.to_pascal_case(),
name.to_snake_case(),
name,
name.to_lower_camel_case(),
name.to_pascal_case(),
)
}
pub fn ts_config(jest: bool) -> &'static str {
if jest {
r#"{
"compilerOptions": {
"types": ["jest"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}
"#
} else {
r#"{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}
"#
}
}
pub fn git_ignore() -> &'static str {
r#".anchor
.DS_Store
target
**/*.rs.bk
node_modules
test-ledger
.yarn
.surfpool
"#
}
pub fn prettier_ignore() -> &'static str {
r#".anchor
.DS_Store
target
node_modules
dist
build
test-ledger
"#
}
pub fn node_shell(
cluster_url: &str,
wallet_path: &str,
programs: Vec<ProgramWorkspace>,
) -> Result<String> {
let mut eval_string = format!(
r#"
const anchor = require('@anchor-lang/core');
const web3 = anchor.web3;
const PublicKey = anchor.web3.PublicKey;
const Keypair = anchor.web3.Keypair;
const __wallet = new anchor.Wallet(
Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
require('fs').readFileSync(
"{wallet_path}",
{{
encoding: "utf-8",
}},
),
),
),
),
);
const __connection = new web3.Connection("{cluster_url}", "processed");
const provider = new anchor.AnchorProvider(__connection, __wallet, {{
commitment: "processed",
preflightcommitment: "processed",
}});
anchor.setProvider(provider);
"#,
);
for program in programs {
write!(
&mut eval_string,
r#"
anchor.workspace.{} = new anchor.Program({}, provider);
"#,
program.name.to_lower_camel_case(),
serde_json::to_string(&program.idl)?,
)?;
}
Ok(eval_string)
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
pub enum TestTemplate {
Mocha,
Jest,
Rust,
Mollusk,
#[default]
Litesvm,
}
impl TestTemplate {
pub fn uses_node(&self) -> bool {
matches!(self, Self::Mocha | Self::Jest)
}
pub fn get_test_script(&self, js: bool, pkg_manager: Option<&PackageManager>) -> String {
let pkg_manager_exec_cmd = match pkg_manager {
Some(PackageManager::Yarn) => "yarn run",
Some(PackageManager::NPM) => "npx",
Some(PackageManager::PNPM) => "pnpm exec",
Some(PackageManager::Bun) => "bunx",
None => "",
};
match &self {
Self::Mocha => {
if js {
format!("{pkg_manager_exec_cmd} mocha -t 1000000 tests/")
} else {
format!(
r#"{pkg_manager_exec_cmd} ts-mocha -p ./tsconfig.json -t 1000000 "tests/**/*.ts""#
)
}
}
Self::Jest => {
if js {
format!("{pkg_manager_exec_cmd} jest")
} else {
format!("{pkg_manager_exec_cmd} jest --preset ts-jest")
}
}
Self::Rust | Self::Litesvm => "cargo test".to_owned(),
Self::Mollusk => "cargo test-sbf".to_owned(),
}
}
pub fn create_test_files(
&self,
project_name: &str,
js: bool,
program_id: &str,
anchor_version: AnchorVersion,
) -> Result<()> {
match self {
Self::Mocha => {
fs::create_dir_all("tests")?;
if js {
let mut test = File::create(format!("tests/{}.js", &project_name))?;
test.write_all(mocha(project_name, anchor_version).as_bytes())?;
} else {
let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
mocha.write_all(ts_mocha(project_name, anchor_version).as_bytes())?;
}
}
Self::Jest => {
fs::create_dir_all("tests")?;
if js {
let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
test.write_all(js_jest(project_name, anchor_version).as_bytes())?;
} else {
let mut test = File::create(format!("tests/{}.test.ts", &project_name))?;
test.write_all(ts_jest(project_name, anchor_version).as_bytes())?;
}
}
Self::Rust => {
let exit = std::process::Command::new("cargo")
.arg("new")
.arg("--vcs")
.arg("none")
.arg("--lib")
.arg("tests")
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{}", e))?;
if !exit.status.success() {
eprintln!("'cargo new --lib tests' failed");
std::process::exit(exit.status.code().unwrap_or(1));
}
let mut files = Vec::new();
let tests_path = Path::new("tests");
files.extend(vec![(
tests_path.join("Cargo.toml"),
tests_cargo_toml(project_name, anchor_version),
)]);
files.extend(create_program_template_rust_test(
project_name,
tests_path,
program_id,
anchor_version,
));
override_or_create_files(&files)?;
}
Self::Mollusk => {
let tests_path_str = format!("programs/{}/tests", &project_name);
let tests_path = Path::new(&tests_path_str);
fs::create_dir_all(tests_path)?;
let mut files = Vec::new();
files.extend(create_program_template_mollusk_test(
project_name,
tests_path,
anchor_version,
));
override_or_create_files(&files)?;
}
Self::Litesvm => {
let tests_path_str = format!("programs/{}/tests", &project_name);
let tests_path = Path::new(&tests_path_str);
fs::create_dir_all(tests_path)?;
let mut files = Vec::new();
files.extend(create_program_template_litesvm_test(
project_name,
tests_path,
anchor_version,
));
override_or_create_files(&files)?;
}
}
Ok(())
}
}
pub fn tests_cargo_toml(name: &str, anchor_version: AnchorVersion) -> String {
match anchor_version {
AnchorVersion::V1 => tests_cargo_toml_v1(name),
AnchorVersion::V2 => tests_cargo_toml_v2(name),
}
}
fn tests_cargo_toml_v1(name: &str) -> String {
format!(
r#"[package]
name = "tests"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
rust-version = "{ANCHOR_MSRV}"
[dependencies]
anchor-client = "{VERSION}"
{name} = {{ version = "0.1.0", path = "../programs/{name}" }}
solana-keypair = "3.0.0"
solana-pubkey = "3.0.0"
solana-sdk-ids = "3"
solana-signer = "3"
"#
)
}
fn tests_cargo_toml_v2(name: &str) -> String {
format!(
r#"[package]
name = "tests"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
rust-version = "{ANCHOR_MSRV}"
[dependencies]
# Once anchor-client v2 is published to crates.io, swap to: anchor-client = "{ANCHOR_V2_TEMPLATE_VERSION}"
anchor-client = {{ git = "https://github.com/solana-foundation/anchor.git", branch = "anchor-next" }}
{name} = {{ version = "0.1.0", path = "../programs/{name}" }}
solana-keypair = "3.0.0"
solana-pubkey = "3.0.0"
solana-sdk-ids = "3"
solana-signer = "3"
"#
)
}
fn create_program_template_rust_test(
name: &str,
tests_path: &Path,
program_id: &str,
anchor_version: AnchorVersion,
) -> Files {
match anchor_version {
AnchorVersion::V1 => create_program_template_rust_test_v1(name, tests_path, program_id),
AnchorVersion::V2 => create_program_template_rust_test_v2(name, tests_path, program_id),
}
}
fn create_program_template_rust_test_v1(name: &str, tests_path: &Path, program_id: &str) -> Files {
let src_path = tests_path.join("src");
vec![
(
src_path.join("lib.rs"),
r#"#[cfg(test)]
mod test_initialize;
"#
.into(),
),
(
src_path.join("test_initialize.rs"),
format!(
r#"use anchor_client::{{
CommitmentConfig,
Client, Cluster,
}};
use solana_keypair::read_keypair_file;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
#[test]
fn test_initialize() {{
let program_id = "{0}";
let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
let payer = read_keypair_file(&anchor_wallet).unwrap();
let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
let program_id = Pubkey::try_from(program_id).unwrap();
let program = client.program(program_id).unwrap();
let counter = Pubkey::find_program_address(
&[{1}::constants::COUNTER_SEED],
&program_id,
)
.0;
let initialize_tx = program
.request()
.accounts({1}::accounts::Initialize {{
payer: payer.pubkey(),
counter,
system_program: solana_sdk_ids::system_program::id(),
}})
.args({1}::instruction::Initialize {{}})
.send()
.expect("");
println!("Initialize transaction signature {{}}", initialize_tx);
let increment_tx = program
.request()
.accounts({1}::accounts::Increment {{
counter,
authority: payer.pubkey(),
}})
.args({1}::instruction::Increment {{}})
.send()
.expect("");
println!("Increment transaction signature {{}}", increment_tx);
}}
"#,
program_id,
name.to_snake_case(),
),
),
]
}
fn create_program_template_rust_test_v2(name: &str, tests_path: &Path, program_id: &str) -> Files {
let src_path = tests_path.join("src");
vec![
(
src_path.join("lib.rs"),
r#"#[cfg(test)]
mod test_initialize;
"#
.into(),
),
(
src_path.join("test_initialize.rs"),
format!(
r#"use anchor_client::{{
CommitmentConfig,
Client, Cluster,
}};
use solana_keypair::{{read_keypair_file, Keypair}};
use solana_pubkey::Pubkey;
use solana_signer::Signer;
#[test]
fn test_initialize() {{
let program_id = "{0}";
let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
let payer = read_keypair_file(&anchor_wallet).unwrap();
let counter = Keypair::new();
let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
let program_id = Pubkey::try_from(program_id).unwrap();
let program = client.program(program_id).unwrap();
let tx = program
.request()
.accounts({1}::accounts::Initialize {{
payer: payer.pubkey(),
counter: counter.pubkey(),
system_program: solana_sdk_ids::system_program::id(),
}})
.args({1}::instruction::Initialize {{}})
.signer(&counter)
.send()
.expect("");
println!("Your transaction signature {{}}", tx);
}}
"#,
program_id,
name.to_snake_case(),
),
),
]
}
fn create_program_template_mollusk_test(
name: &str,
tests_path: &Path,
anchor_version: AnchorVersion,
) -> Files {
match anchor_version {
AnchorVersion::V1 => create_program_template_mollusk_test_v1(name, tests_path),
AnchorVersion::V2 => create_program_template_mollusk_test_v2(name, tests_path),
}
}
fn create_program_template_mollusk_test_v1(name: &str, tests_path: &Path) -> Files {
vec![(
tests_path.join("test_initialize.rs"),
format!(
r#"#![cfg(feature = "test-sbf")]
use {{
anchor_lang::{{
solana_program::instruction::Instruction, AccountDeserialize, InstructionData,
Space, ToAccountMetas,
}},
mollusk_svm::{{program::keyed_account_for_system_program, result::Check, Mollusk}},
solana_account::Account as SolanaAccount,
solana_pubkey::Pubkey,
}};
#[test]
fn test_initialize() {{
let program_id = {0}::id();
let mollusk = Mollusk::new(&program_id, "{0}");
let payer = Pubkey::new_unique();
let counter = Pubkey::find_program_address(
&[{0}::constants::COUNTER_SEED],
&program_id,
)
.0;
let instruction = Instruction::new_with_bytes(
program_id,
&{0}::instruction::Initialize {{}}.data(),
{0}::accounts::Initialize {{
payer,
counter,
system_program: solana_sdk_ids::system_program::id(),
}}
.to_account_metas(None),
);
let accounts = vec![
(
payer,
SolanaAccount::new(1_000_000_000, 0, &solana_sdk_ids::system_program::id()),
),
(counter, SolanaAccount::default()),
keyed_account_for_system_program(),
];
let result = mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::success()],
);
let payer_account = result
.resulting_accounts
.iter()
.find(|(pk, _)| *pk == payer)
.map(|(_, a)| a.clone())
.expect("payer account");
let counter_account = result
.resulting_accounts
.iter()
.find(|(pk, _)| *pk == counter)
.map(|(_, a)| a.clone())
.expect("counter account");
assert_eq!(
counter_account.data.len(),
8 + {0}::state::Counter::INIT_SPACE
);
let mut data: &[u8] = &counter_account.data;
let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer);
let instruction = Instruction::new_with_bytes(
program_id,
&{0}::instruction::Increment {{}}.data(),
{0}::accounts::Increment {{
counter,
authority: payer,
}}
.to_account_metas(None),
);
let accounts = vec![(counter, counter_account), (payer, payer_account)];
let result = mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::success()],
);
let counter_account = result
.resulting_accounts
.iter()
.find(|(pk, _)| *pk == counter)
.map(|(_, a)| a)
.expect("counter account");
let mut data: &[u8] = &counter_account.data;
let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer);
}}
"#,
name.to_snake_case(),
),
)]
}
fn create_program_template_mollusk_test_v2(name: &str, tests_path: &Path) -> Files {
vec![(
tests_path.join("test_initialize.rs"),
format!(
r#"#![cfg(feature = "test-sbf")]
use {{
anchor_lang_v2::{{
accounts::Account, solana_program::instruction::Instruction, InstructionData, Space,
ToAccountMetas,
}},
mollusk_svm::{{program::keyed_account_for_system_program, result::Check, Mollusk}},
solana_account::Account as SolanaAccount,
solana_pubkey::Pubkey,
}};
#[test]
fn test_initialize() {{
let program_id = {0}::id();
let mollusk = Mollusk::new(&program_id, "{0}");
let payer = Pubkey::new_unique();
let counter = Pubkey::new_unique();
let instruction = Instruction::new_with_bytes(
program_id,
&{0}::instruction::Initialize {{}}.data(),
{0}::accounts::Initialize {{
payer,
counter,
system_program: solana_sdk_ids::system_program::id(),
}}
.to_account_metas(None),
);
let counter_space = <Account<{0}::state::Counter> as Space>::INIT_SPACE;
let accounts = vec![
(
payer,
SolanaAccount::new(1_000_000_000, 0, &solana_sdk_ids::system_program::id()),
),
(counter, SolanaAccount::default()),
keyed_account_for_system_program(),
];
let result = mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::success()],
);
let counter_account = result
.resulting_accounts
.iter()
.find(|(pk, _)| *pk == counter)
.map(|(_, a)| a)
.expect("counter account");
assert_eq!(counter_account.data.len(), counter_space);
let counter_state: &{0}::state::Counter = bytemuck::from_bytes(&counter_account.data[8..]);
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer);
}}
"#,
name.to_snake_case(),
),
)]
}
fn create_program_template_litesvm_test(
name: &str,
tests_path: &Path,
anchor_version: AnchorVersion,
) -> Files {
match anchor_version {
AnchorVersion::V1 => create_program_template_litesvm_test_v1(name, tests_path),
AnchorVersion::V2 => create_program_template_litesvm_test_v2(name, tests_path),
}
}
fn create_program_template_litesvm_test_v1(name: &str, tests_path: &Path) -> Files {
vec![(
tests_path.join("test_initialize.rs"),
format!(
r#"
use {{
anchor_lang::{{
prelude::Pubkey,
solana_program::{{instruction::Instruction, system_program}},
AccountDeserialize, InstructionData, ToAccountMetas,
}},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{{Message, VersionedMessage}},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
}};
#[test]
fn test_initialize() {{
let program_id = {0}::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[{0}::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/{0}.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&{0}::instruction::Initialize {{}}.data(),
{0}::accounts::Initialize {{
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&{0}::instruction::Increment {{}}.data(),
{0}::accounts::Increment {{
counter,
authority: payer.pubkey(),
}}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}}
"#,
name.to_snake_case(),
),
)]
}
fn create_program_template_litesvm_test_v2(name: &str, tests_path: &Path) -> Files {
vec![(
tests_path.join("test_initialize.rs"),
format!(
r#"
use {{
anchor_lang_v2::{{
accounts::Account, bytemuck, programs::System,
solana_program::instruction::Instruction, Id, InstructionData, Space, ToAccountMetas,
}},
anchor_v2_testing::{{Keypair, Message, Signer, VersionedMessage, VersionedTransaction}},
}};
#[test]
fn test_initialize() {{
let program_id = {0}::id();
let payer = Keypair::new();
let counter = Keypair::new();
// `svm()` is `LiteSVM::new()` by default. When this crate is built
// with `--features profile` (which `anchor test --profile` and
// `anchor debugger` set automatically), it also installs the
// register-tracing callback that writes per-test SBF traces under
// `target/anchor-v2-profile/`. The cfg switch lives inside
// `anchor-v2-testing` so test code stays clean either way.
let mut svm = anchor_v2_testing::svm();
let bytes = include_bytes!("../../../target/deploy/{0}.so");
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&{0}::instruction::Initialize {{}}.data(),
{0}::accounts::Initialize {{
payer: payer.pubkey(),
counter: counter.pubkey(),
system_program: System::id(),
}}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(msg),
&[&payer, &counter],
)
.unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok(), "send_transaction failed: {{:?}}", res);
// Verify the counter account was initialized. Size comes from the same
// `Space::INIT_SPACE` expression the `init` constraint allocates with,
// so the assertion doesn't rot if `Counter` gains fields. The payload
// tail is a `Pod` struct, so we cast directly and read fields by name
// instead of hand-slicing bytes.
let account = svm.get_account(&counter.pubkey()).expect("counter account");
assert_eq!(account.data.len(), <Account<{0}::state::Counter> as Space>::INIT_SPACE);
let counter_state: &{0}::state::Counter = bytemuck::from_bytes(&account.data[8..]);
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
}}
"#,
name.to_snake_case(),
),
)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v1_templates_keep_legacy_anchor_lang_shape() {
let manifest = cargo_toml("counter", Some(&TestTemplate::Litesvm), AnchorVersion::V1);
assert!(manifest.contains("anchor-lang ="));
assert!(manifest.contains("litesvm = \"0.10.0\""));
assert!(!manifest.contains("anchor-lang-v2"));
let test = ts_mocha("counter", AnchorVersion::V1);
assert!(test.contains("[Buffer.from(\"counter\")]"));
assert!(test.contains(".accountsPartial({ counter })"));
assert!(!test.contains("counter: counter.publicKey"));
}
#[test]
fn v2_templates_use_anchor_next_counter_shape() {
let manifest = cargo_toml("counter", Some(&TestTemplate::Litesvm), AnchorVersion::V2);
assert!(manifest.contains("anchor-lang-v2 = { git = "));
assert!(manifest.contains("profile = [\"anchor-v2-testing/profile\"]"));
assert!(manifest.contains("anchor-v2-testing = { git = "));
let test = ts_mocha("counter", AnchorVersion::V2);
assert!(test.contains("const counter = anchor.web3.Keypair.generate();"));
assert!(test.contains("counter: counter.publicKey"));
}
}