Skip to main content

anchor_cli/
rust_template.rs

1use {
2    crate::{
3        config::ProgramWorkspace, create_files, override_or_create_files, AbsolutePath, Files,
4        PackageManager, VERSION,
5    },
6    anyhow::Result,
7    clap::{Parser, ValueEnum},
8    heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase},
9    solana_keypair::{read_keypair_file, write_keypair_file, Keypair},
10    solana_pubkey::Pubkey,
11    solana_signer::Signer,
12    std::{
13        fmt::Write as _,
14        fs::{self, File},
15        io::Write as _,
16        path::Path,
17        process::Stdio,
18    },
19};
20
21const ANCHOR_MSRV: &str = "1.89.0";
22
23/// Program initialization template
24#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
25pub enum ProgramTemplate {
26    /// Program with a single `lib.rs` file (not recommended for production)
27    Single,
28    /// Program with multiple files for instructions, state... (recommended)
29    #[default]
30    Multiple,
31}
32
33/// Create a program from the given name and template.
34pub fn create_program(
35    name: &str,
36    template: ProgramTemplate,
37    test_template: Option<&TestTemplate>,
38) -> Result<()> {
39    let program_path = Path::new("programs").join(name);
40    let lib_rs_path = program_path.join("src").join("lib.rs");
41    let common_files = vec![
42        ("Cargo.toml".into(), workspace_manifest()),
43        ("rust-toolchain.toml".into(), rust_toolchain_toml()),
44        (
45            program_path.join("Cargo.toml"),
46            cargo_toml(name, test_template),
47        ),
48        // One of the create_program_template_* functions will write the full
49        // lib.rs, but we need an empty stub for now so cargo won't throw an
50        // error when asking it where the `target` dir is located.
51        (lib_rs_path.clone(), "".into()),
52        // Note: Xargo.toml is no longer needed for modern Solana builds using SBF.
53    ];
54
55    create_files(&common_files)?;
56
57    let target_path = crate::target_dir()?;
58
59    // Remove the stub version
60    fs::remove_file(&lib_rs_path)?;
61
62    let template_files = match template {
63        ProgramTemplate::Single => {
64            println!(
65                "Note: Using single-file template. For better code organization and \
66                 maintainability, consider using --template multiple (default)."
67            );
68            create_program_template_single(name, &program_path, target_path)
69        }
70        ProgramTemplate::Multiple => {
71            create_program_template_multiple(name, &program_path, target_path)
72        }
73    };
74
75    create_files(&template_files)
76}
77
78/// Helper to create a rust-toolchain.toml at the workspace root
79fn rust_toolchain_toml() -> String {
80    format!(
81        r#"[toolchain]
82channel = "{ANCHOR_MSRV}"
83components = ["rustfmt","clippy"]
84profile = "minimal"
85"#
86    )
87}
88
89/// Create a program with a single `lib.rs` file.
90fn create_program_template_single(name: &str, program_path: &Path, target_path: &Path) -> Files {
91    vec![(
92        program_path.join("src").join("lib.rs"),
93        format!(
94            r#"use anchor_lang::prelude::*;
95
96declare_id!("{}");
97
98#[program]
99pub mod {} {{
100    use super::*;
101
102    pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {{
103        ctx.accounts.counter.count = 0;
104        ctx.accounts.counter.authority = *ctx.accounts.payer.address();
105        msg!("Counter initialized");
106        Ok(())
107    }}
108}}
109
110pub mod state {{
111    use super::*;
112
113    #[account]
114    pub struct Counter {{
115        pub count: u64,
116        pub authority: Address,
117    }}
118}}
119
120use state::Counter;
121
122#[derive(Accounts)]
123pub struct Initialize {{
124    #[account(mut)]
125    pub payer: Signer,
126    #[account(init, payer = payer)]
127    pub counter: Account<Counter>,
128    pub system_program: Program<System>,
129}}
130"#,
131            get_or_create_program_id(name, target_path),
132            name.to_snake_case(),
133        ),
134    )]
135}
136
137/// Create a program with multiple files for instructions, state...
138fn create_program_template_multiple(name: &str, program_path: &Path, target_path: &Path) -> Files {
139    let src_path = program_path.join("src");
140    vec![
141        (
142            src_path.join("lib.rs"),
143            format!(
144                r#"pub mod constants;
145pub mod error;
146pub mod instructions;
147pub mod state;
148
149use anchor_lang::prelude::*;
150
151pub use instructions::*;
152
153declare_id!("{}");
154
155#[program]
156pub mod {} {{
157    use super::*;
158
159    pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {{
160        initialize::handler(ctx)
161    }}
162}}
163"#,
164                get_or_create_program_id(name, target_path),
165                name.to_snake_case(),
166            ),
167        ),
168        (
169            src_path.join("constants.rs"),
170            r#"use anchor_lang::prelude::*;
171
172#[constant]
173pub const SEED: &str = "anchor";
174"#
175            .into(),
176        ),
177        (
178            src_path.join("error.rs"),
179            r#"use anchor_lang::prelude::*;
180
181#[error_code]
182pub enum ErrorCode {
183    #[msg("Custom error message")]
184    CustomError,
185}
186"#
187            .into(),
188        ),
189        (
190            src_path.join("instructions.rs"),
191            r#"pub mod initialize;
192
193pub use initialize::*;
194"#
195            .into(),
196        ),
197        (
198            src_path.join("instructions").join("initialize.rs"),
199            r#"use anchor_lang::prelude::*;
200
201use crate::state::Counter;
202
203#[derive(Accounts)]
204pub struct Initialize {
205    #[account(mut)]
206    pub payer: Signer,
207    #[account(init, payer = payer)]
208    pub counter: Account<Counter>,
209    pub system_program: Program<System>,
210}
211
212pub fn handler(ctx: &mut Context<Initialize>) -> Result<()> {
213    ctx.accounts.counter.count = 0;
214    ctx.accounts.counter.authority = *ctx.accounts.payer.address();
215    msg!("Counter initialized");
216    Ok(())
217}
218"#
219            .into(),
220        ),
221        (
222            src_path.join("state.rs"),
223            r#"use anchor_lang::prelude::*;
224
225#[account]
226pub struct Counter {
227    pub count: u64,
228    pub authority: Address,
229}
230"#
231            .into(),
232        ),
233    ]
234}
235
236fn workspace_manifest() -> String {
237    format!(
238        r#"[workspace]
239members = [
240    "programs/*"
241]
242resolver = "2"
243
244[workspace.package]
245edition = "2021"
246rust-version = "{ANCHOR_MSRV}"
247
248[profile.release]
249overflow-checks = true
250lto = "fat"
251codegen-units = 1
252[profile.release.build-override]
253opt-level = 3
254incremental = false
255codegen-units = 1
256"#
257    )
258}
259
260fn cargo_toml(name: &str, test_template: Option<&TestTemplate>) -> String {
261    // Template-specific features carried into the emitted `[features]` block:
262    //   - Mollusk: `test-sbf` for host-mode integration tests.
263    //   - LiteSVM: `profile` forwards to `anchor-v2-testing/profile`, the
264    //     register-tracing hook that `anchor test --profile` activates.
265    let template_features = match test_template {
266        Some(TestTemplate::Mollusk) => r#"test-sbf = []"#,
267        Some(TestTemplate::Litesvm) => r#"profile = ["anchor-v2-testing/profile"]"#,
268        _ => "",
269    };
270    let dev_dependencies = match test_template {
271        Some(TestTemplate::Mollusk) => {
272            r#"
273[dev-dependencies]
274mollusk-svm = "~0.10"
275solana-account = "3"
276solana-pubkey = "4"
277solana-sdk-ids = "3"
278bytemuck = "1"
279"#
280        }
281        Some(TestTemplate::Litesvm) => {
282            r#"
283[dev-dependencies]
284anchor-v2-testing = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
285"#
286        }
287        _ => "",
288    };
289
290    format!(
291        r#"[package]
292name = "{0}"
293version = "0.1.0"
294description = "Created with Anchor"
295edition.workspace = true
296rust-version.workspace = true
297
298[lib]
299crate-type = ["cdylib", "lib"]
300name = "{1}"
301
302[features]
303default = []
304cpi = ["no-entrypoint"]
305no-entrypoint = []
306no-log-ix-name = []
307idl-build = []
308{2}
309
310[dependencies]
311# Once anchor-lang is published to crates.io, swap to: anchor-lang = "{3}"
312anchor-lang = {{ git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }}
313solana-program-log = {{ version = "1.1", features = ["macro"] }}
314wincode = {{ version = "0.5", features = ["derive"] }}
315{4}
316
317[lints.rust]
318unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }}
319"#,
320        name,
321        name.to_snake_case(),
322        template_features,
323        VERSION,
324        dev_dependencies,
325    )
326}
327
328/// Read the program keypair file or create a new one if it doesn't exist.
329pub fn get_or_create_program_id(name: &str, target_path: impl AsRef<Path>) -> Pubkey {
330    let keypair_path = target_path
331        .as_ref()
332        .join("deploy")
333        .join(format!("{}-keypair.json", name.to_snake_case()));
334
335    read_keypair_file(&keypair_path)
336        .unwrap_or_else(|_| {
337            let keypair = Keypair::new();
338            write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
339            keypair
340        })
341        .pubkey()
342}
343
344pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
345    format!(
346        r#"
347const anchor = require('@anchor-lang/core');
348
349// Deploy script defined by the user.
350const userScript = require("{script_path}");
351
352async function main() {{
353    const connection = new anchor.web3.Connection(
354      "{cluster_url}",
355      anchor.AnchorProvider.defaultOptions().commitment
356    );
357    const wallet = anchor.Wallet.local();
358    const provider = new anchor.AnchorProvider(connection, wallet);
359
360    // Run the user's deploy script.
361    userScript(provider);
362}}
363main();
364"#,
365    )
366}
367
368pub fn deploy_ts_script_host(cluster_url: &str, script_path: &str) -> String {
369    format!(
370        r#"import * as anchor from '@anchor-lang/core';
371
372// Deploy script defined by the user.
373const userScript = require("{script_path}");
374
375async function main() {{
376    const connection = new anchor.web3.Connection(
377      "{cluster_url}",
378      anchor.AnchorProvider.defaultOptions().commitment
379    );
380    const wallet = anchor.Wallet.local();
381    const provider = new anchor.AnchorProvider(connection, wallet);
382
383    // Run the user's deploy script.
384    userScript(provider);
385}}
386main();
387"#,
388    )
389}
390
391pub fn deploy_script() -> &'static str {
392    r#"// Migrations are an early feature. Currently, they're nothing more than this
393// single deploy script that's invoked from the CLI, injecting a provider
394// configured from the workspace's Anchor.toml.
395
396const anchor = require("@anchor-lang/core");
397
398module.exports = async function (provider) {
399  // Configure client to use the provider.
400  anchor.setProvider(provider);
401
402  // Add your deploy script here.
403};
404"#
405}
406
407pub fn ts_deploy_script() -> &'static str {
408    r#"// Migrations are an early feature. Currently, they're nothing more than this
409// single deploy script that's invoked from the CLI, injecting a provider
410// configured from the workspace's Anchor.toml.
411
412import * as anchor from "@anchor-lang/core";
413
414module.exports = async function (provider: anchor.AnchorProvider) {
415  // Configure client to use the provider.
416  anchor.setProvider(provider);
417
418  // Add your deploy script here.
419};
420"#
421}
422
423pub fn mocha(name: &str) -> String {
424    format!(
425        r#"const anchor = require("@anchor-lang/core");
426
427describe("{}", () => {{
428  // Configure the client to use the local cluster.
429  anchor.setProvider(anchor.AnchorProvider.env());
430
431  it("Is initialized!", async () => {{
432    // Add your test here.
433    const program = anchor.workspace.{};
434    const counter = anchor.web3.Keypair.generate();
435    const tx = await program.methods
436      .initialize()
437      .accounts({{ counter: counter.publicKey }})
438      .signers([counter])
439      .rpc();
440    console.log("Your transaction signature", tx);
441  }});
442}});
443"#,
444        name,
445        name.to_lower_camel_case(),
446    )
447}
448
449pub fn js_jest(name: &str) -> String {
450    format!(
451        r#"const anchor = require("@anchor-lang/core");
452
453describe("{}", () => {{
454  // Configure the client to use the local cluster.
455  anchor.setProvider(anchor.AnchorProvider.env());
456
457  it("Is initialized!", async () => {{
458    // Add your test here.
459    const program = anchor.workspace.{};
460    const counter = anchor.web3.Keypair.generate();
461    const tx = await program.methods
462      .initialize()
463      .accounts({{ counter: counter.publicKey }})
464      .signers([counter])
465      .rpc();
466    console.log("Your transaction signature", tx);
467  }});
468}});
469"#,
470        name,
471        name.to_lower_camel_case(),
472    )
473}
474
475// TODO(anchor-next): bump to `^2.0.0` once the TS package is published.
476// Pinned at `^1.0.0` because 2.0.0 isn't on npm yet.
477pub fn package_json(jest: bool, license: String) -> String {
478    if jest {
479        format!(
480            r#"{{
481  "license": "{license}",
482  "scripts": {{
483    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
484    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
485  }},
486  "dependencies": {{
487    "@anchor-lang/core": "^1.0.0"
488  }},
489  "devDependencies": {{
490    "jest": "^30.3.0",
491    "prettier": "^3.8.3"
492  }},
493  "overrides": {{
494    "uuid": "^9.0.1"
495  }},
496  "resolutions": {{
497    "uuid": "^9.0.1"
498  }},
499  "pnpm": {{
500    "overrides": {{
501      "uuid": "^9.0.1"
502    }}
503  }}
504}}
505    "#
506        )
507    } else {
508        format!(
509            r#"{{
510  "license": "{license}",
511  "scripts": {{
512    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
513    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
514  }},
515  "dependencies": {{
516    "@anchor-lang/core": "^1.0.0"
517  }},
518  "devDependencies": {{
519    "chai": "^4.5.0",
520    "mocha": "^11.7.5",
521    "prettier": "^3.8.3"
522  }}
523}}
524"#
525        )
526    }
527}
528
529// TODO(anchor-next): bump to `^2.0.0` once published (same as `package_json`).
530pub fn ts_package_json(jest: bool, license: String) -> String {
531    if jest {
532        format!(
533            r#"{{
534  "license": "{license}",
535  "scripts": {{
536    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
537    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
538  }},
539  "dependencies": {{
540    "@anchor-lang/core": "^1.0.0"
541  }},
542  "devDependencies": {{
543    "@types/bn.js": "^5.2.0",
544    "@types/jest": "^30.0.0",
545    "jest": "^30.3.0",
546    "prettier": "^3.8.3",
547    "ts-jest": "^29.4.9",
548    "typescript": "^5.9.3"
549  }},
550  "overrides": {{
551    "uuid": "^9.0.1"
552  }},
553  "resolutions": {{
554    "uuid": "^9.0.1"
555  }},
556  "pnpm": {{
557    "overrides": {{
558      "uuid": "^9.0.1"
559    }}
560  }}
561}}
562"#
563        )
564    } else {
565        format!(
566            r#"{{
567  "license": "{license}",
568  "scripts": {{
569    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
570    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
571  }},
572  "dependencies": {{
573    "@anchor-lang/core": "^1.0.0"
574  }},
575  "devDependencies": {{
576    "chai": "^4.5.0",
577    "mocha": "^11.7.5",
578    "ts-mocha": "^11.1.0",
579    "ts-node": "^10.9.2",
580    "@types/bn.js": "^5.2.0",
581    "@types/chai": "^4.3.0",
582    "@types/mocha": "^10.0.10",
583    "@types/node": "^25.6.0",
584    "typescript": "^5.9.3",
585    "prettier": "^3.8.3"
586  }}
587}}
588"#
589        )
590    }
591}
592
593pub fn ts_mocha(name: &str) -> String {
594    format!(
595        r#"import * as anchor from "@anchor-lang/core";
596import {{ Program }} from "@anchor-lang/core";
597import {{ {} }} from "../target/types/{}";
598
599describe("{}", () => {{
600  // Configure the client to use the local cluster.
601  anchor.setProvider(anchor.AnchorProvider.env());
602
603  const program = anchor.workspace.{} as Program<{}>;
604
605  it("Is initialized!", async () => {{
606    // Add your test here.
607    const counter = anchor.web3.Keypair.generate();
608    const tx = await program.methods
609      .initialize()
610      .accounts({{ counter: counter.publicKey }})
611      .signers([counter])
612      .rpc();
613    console.log("Your transaction signature", tx);
614  }});
615}});
616"#,
617        name.to_pascal_case(),
618        name.to_snake_case(),
619        name,
620        name.to_lower_camel_case(),
621        name.to_pascal_case(),
622    )
623}
624
625pub fn ts_jest(name: &str) -> String {
626    format!(
627        r#"import * as anchor from "@anchor-lang/core";
628import {{ Program }} from "@anchor-lang/core";
629import {{ {} }} from "../target/types/{}";
630
631describe("{}", () => {{
632  // Configure the client to use the local cluster.
633  anchor.setProvider(anchor.AnchorProvider.env());
634
635  const program = anchor.workspace.{} as Program<{}>;
636
637  it("Is initialized!", async () => {{
638    // Add your test here.
639    const counter = anchor.web3.Keypair.generate();
640    const tx = await program.methods
641      .initialize()
642      .accounts({{ counter: counter.publicKey }})
643      .signers([counter])
644      .rpc();
645    console.log("Your transaction signature", tx);
646  }});
647}});
648"#,
649        name.to_pascal_case(),
650        name.to_snake_case(),
651        name,
652        name.to_lower_camel_case(),
653        name.to_pascal_case(),
654    )
655}
656
657pub fn ts_config(jest: bool) -> &'static str {
658    if jest {
659        r#"{
660  "compilerOptions": {
661    "types": ["jest"],
662    "typeRoots": ["./node_modules/@types"],
663    "lib": ["es2015"],
664    "module": "commonjs",
665    "target": "es6",
666    "esModuleInterop": true
667  }
668}
669"#
670    } else {
671        r#"{
672  "compilerOptions": {
673    "types": ["mocha", "chai"],
674    "typeRoots": ["./node_modules/@types"],
675    "lib": ["es2015"],
676    "module": "commonjs",
677    "target": "es6",
678    "esModuleInterop": true
679  }
680}
681"#
682    }
683}
684
685pub fn git_ignore() -> &'static str {
686    r#".anchor
687.DS_Store
688target
689**/*.rs.bk
690node_modules
691test-ledger
692.yarn
693.surfpool
694"#
695}
696
697pub fn prettier_ignore() -> &'static str {
698    r#".anchor
699.DS_Store
700target
701node_modules
702dist
703build
704test-ledger
705"#
706}
707
708pub fn node_shell(
709    cluster_url: &str,
710    wallet_path: &str,
711    programs: Vec<ProgramWorkspace>,
712) -> Result<String> {
713    let mut eval_string = format!(
714        r#"
715const anchor = require('@anchor-lang/core');
716const web3 = anchor.web3;
717const PublicKey = anchor.web3.PublicKey;
718const Keypair = anchor.web3.Keypair;
719
720const __wallet = new anchor.Wallet(
721  Keypair.fromSecretKey(
722    Buffer.from(
723      JSON.parse(
724        require('fs').readFileSync(
725          "{wallet_path}",
726          {{
727            encoding: "utf-8",
728          }},
729        ),
730      ),
731    ),
732  ),
733);
734const __connection = new web3.Connection("{cluster_url}", "processed");
735const provider = new anchor.AnchorProvider(__connection, __wallet, {{
736  commitment: "processed",
737  preflightcommitment: "processed",
738}});
739anchor.setProvider(provider);
740"#,
741    );
742
743    for program in programs {
744        write!(
745            &mut eval_string,
746            r#"
747anchor.workspace.{} = new anchor.Program({}, provider);
748"#,
749            program.name.to_lower_camel_case(),
750            serde_json::to_string(&program.idl)?,
751        )?;
752    }
753
754    Ok(eval_string)
755}
756
757/// Test initialization template
758#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
759pub enum TestTemplate {
760    /// Generate template for Mocha unit-test
761    Mocha,
762    /// Generate template for Jest unit-test
763    Jest,
764    /// Generate template for Rust unit-test
765    Rust,
766    /// Generate template for Mollusk Rust unit-test
767    Mollusk,
768    /// Generate template for LiteSVM rust unit-test
769    #[default]
770    Litesvm,
771}
772
773impl TestTemplate {
774    pub fn get_test_script(&self, js: bool, pkg_manager: &PackageManager) -> String {
775        let pkg_manager_exec_cmd = match pkg_manager {
776            PackageManager::Yarn => "yarn run",
777            PackageManager::NPM => "npx",
778            PackageManager::PNPM => "pnpm exec",
779            PackageManager::Bun => "bunx",
780        };
781
782        match &self {
783            Self::Mocha => {
784                if js {
785                    format!("{pkg_manager_exec_cmd} mocha -t 1000000 tests/")
786                } else {
787                    format!(
788                        r#"{pkg_manager_exec_cmd} ts-mocha -p ./tsconfig.json -t 1000000 "tests/**/*.ts""#
789                    )
790                }
791            }
792            Self::Jest => {
793                if js {
794                    format!("{pkg_manager_exec_cmd} jest")
795                } else {
796                    format!("{pkg_manager_exec_cmd} jest --preset ts-jest")
797                }
798            }
799            Self::Rust | Self::Litesvm => "cargo test".to_owned(),
800            Self::Mollusk => "cargo test-sbf".to_owned(),
801        }
802    }
803
804    pub fn create_test_files(&self, project_name: &str, js: bool, program_id: &str) -> Result<()> {
805        match self {
806            Self::Mocha => {
807                // Build the test suite.
808                fs::create_dir_all("tests")?;
809
810                if js {
811                    let mut test = File::create(format!("tests/{}.js", &project_name))?;
812                    test.write_all(mocha(project_name).as_bytes())?;
813                } else {
814                    let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
815                    mocha.write_all(ts_mocha(project_name).as_bytes())?;
816                }
817            }
818            Self::Jest => {
819                // Build the test suite.
820                fs::create_dir_all("tests")?;
821
822                if js {
823                    let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
824                    test.write_all(js_jest(project_name).as_bytes())?;
825                } else {
826                    let mut test = File::create(format!("tests/{}.test.ts", &project_name))?;
827                    test.write_all(ts_jest(project_name).as_bytes())?;
828                }
829            }
830            Self::Rust => {
831                // Do not initialize git repo
832                let exit = std::process::Command::new("cargo")
833                    .arg("new")
834                    .arg("--vcs")
835                    .arg("none")
836                    .arg("--lib")
837                    .arg("tests")
838                    .stderr(Stdio::inherit())
839                    .output()
840                    .map_err(|e| anyhow::format_err!("{}", e))?;
841                if !exit.status.success() {
842                    eprintln!("'cargo new --lib tests' failed");
843                    std::process::exit(exit.status.code().unwrap_or(1));
844                }
845
846                let mut files = Vec::new();
847                let tests_path = Path::new("tests");
848                files.extend(vec![(
849                    tests_path.join("Cargo.toml"),
850                    tests_cargo_toml(project_name),
851                )]);
852                files.extend(create_program_template_rust_test(
853                    project_name,
854                    tests_path,
855                    program_id,
856                ));
857                override_or_create_files(&files)?;
858            }
859            Self::Mollusk => {
860                // Build the test suite.
861                let tests_path_str = format!("programs/{}/tests", &project_name);
862                let tests_path = Path::new(&tests_path_str);
863                fs::create_dir_all(tests_path)?;
864
865                let mut files = Vec::new();
866                files.extend(create_program_template_mollusk_test(
867                    project_name,
868                    tests_path,
869                ));
870                override_or_create_files(&files)?;
871            }
872
873            Self::Litesvm => {
874                let tests_path_str = format!("programs/{}/tests", &project_name);
875                let tests_path = Path::new(&tests_path_str);
876                fs::create_dir_all(tests_path)?;
877                let mut files = Vec::new();
878                files.extend(create_program_template_litesvm_test(
879                    project_name,
880                    tests_path,
881                ));
882                override_or_create_files(&files)?;
883            }
884        }
885
886        Ok(())
887    }
888}
889
890pub fn tests_cargo_toml(name: &str) -> String {
891    format!(
892        r#"[package]
893name = "tests"
894version = "0.1.0"
895description = "Created with Anchor"
896edition = "2021"
897rust-version = "{ANCHOR_MSRV}"
898
899[dependencies]
900# Once anchor-client v2 is published to crates.io, swap to: anchor-client = "{VERSION}"
901anchor-client = {{ git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }}
902{name} = {{ version = "0.1.0", path = "../programs/{name}" }}
903solana-keypair = "3.0.0"
904solana-pubkey = "3.0.0"
905solana-sdk-ids = "3"
906solana-signer = "3"
907"#
908    )
909}
910
911/// Generate template for Rust unit-test
912fn create_program_template_rust_test(name: &str, tests_path: &Path, program_id: &str) -> Files {
913    let src_path = tests_path.join("src");
914    vec![
915        (
916            src_path.join("lib.rs"),
917            r#"#[cfg(test)]
918mod test_initialize;
919"#
920            .into(),
921        ),
922        (
923            src_path.join("test_initialize.rs"),
924            format!(
925                r#"use anchor_client::{{
926    CommitmentConfig,
927    Client, Cluster,
928}};
929use solana_keypair::{{read_keypair_file, Keypair}};
930use solana_pubkey::Pubkey;
931use solana_signer::Signer;
932
933#[test]
934fn test_initialize() {{
935    let program_id = "{0}";
936    let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
937    let payer = read_keypair_file(&anchor_wallet).unwrap();
938    let counter = Keypair::new();
939
940    let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
941    let program_id = Pubkey::try_from(program_id).unwrap();
942    let program = client.program(program_id).unwrap();
943
944    let tx = program
945        .request()
946        .accounts({1}::accounts::Initialize {{
947            payer: payer.pubkey(),
948            counter: counter.pubkey(),
949            system_program: solana_sdk_ids::system_program::id(),
950        }})
951        .args({1}::instruction::Initialize {{}})
952        .signer(&counter)
953        .send()
954        .expect("");
955
956    println!("Your transaction signature {{}}", tx);
957}}
958"#,
959                program_id,
960                name.to_snake_case(),
961            ),
962        ),
963    ]
964}
965
966/// Generate template for Mollusk Rust unit-test
967fn create_program_template_mollusk_test(name: &str, tests_path: &Path) -> Files {
968    vec![(
969        tests_path.join("test_initialize.rs"),
970        format!(
971            r#"#![cfg(feature = "test-sbf")]
972
973use {{
974    anchor_lang::{{
975        accounts::Account, solana_program::instruction::Instruction, InstructionData, Space,
976        ToAccountMetas,
977    }},
978    mollusk_svm::{{program::keyed_account_for_system_program, result::Check, Mollusk}},
979    solana_account::Account as SolanaAccount,
980    solana_pubkey::Pubkey,
981}};
982
983#[test]
984fn test_initialize() {{
985    let program_id = {0}::id();
986    let mollusk = Mollusk::new(&program_id, "{0}");
987
988    let payer = Pubkey::new_unique();
989    let counter = Pubkey::new_unique();
990
991    let instruction = Instruction::new_with_bytes(
992        program_id,
993        &{0}::instruction::Initialize {{}}.data(),
994        {0}::accounts::Initialize {{
995            payer,
996            counter,
997            system_program: solana_sdk_ids::system_program::id(),
998        }}
999        .to_account_metas(None),
1000    );
1001
1002    let counter_space = <Account<{0}::state::Counter> as Space>::INIT_SPACE;
1003    let accounts = vec![
1004        (
1005            payer,
1006            SolanaAccount::new(1_000_000_000, 0, &solana_sdk_ids::system_program::id()),
1007        ),
1008        (counter, SolanaAccount::default()),
1009        keyed_account_for_system_program(),
1010    ];
1011
1012    let result = mollusk.process_and_validate_instruction(
1013        &instruction,
1014        &accounts,
1015        &[Check::success()],
1016    );
1017
1018    let counter_account = result
1019        .resulting_accounts
1020        .iter()
1021        .find(|(pk, _)| *pk == counter)
1022        .map(|(_, a)| a)
1023        .expect("counter account");
1024    assert_eq!(counter_account.data.len(), counter_space);
1025    let counter_state: &{0}::state::Counter = bytemuck::from_bytes(&counter_account.data[8..]);
1026    assert_eq!(counter_state.count, 0);
1027    assert_eq!(counter_state.authority, payer);
1028}}
1029"#,
1030            name.to_snake_case(),
1031        ),
1032    )]
1033}
1034
1035/// Generate template for LiteSVM Rust unit-test
1036fn create_program_template_litesvm_test(name: &str, tests_path: &Path) -> Files {
1037    vec![(
1038        tests_path.join("test_initialize.rs"),
1039        format!(
1040            r#"
1041use {{
1042    anchor_lang::{{
1043        accounts::Account, bytemuck, programs::System,
1044        solana_program::instruction::Instruction, Id, InstructionData, Space, ToAccountMetas,
1045    }},
1046    anchor_v2_testing::{{Keypair, LiteSVM, Message, Signer, VersionedMessage, VersionedTransaction}},
1047}};
1048
1049#[test]
1050fn test_initialize() {{
1051    let program_id = {0}::id();
1052    let payer = Keypair::new();
1053    let counter = Keypair::new();
1054
1055    // `svm()` is `LiteSVM::new()` by default. When this crate is built
1056    // with `--features profile` (which `anchor test --profile` and
1057    // `anchor debugger` set automatically), it also installs the
1058    // register-tracing callback that writes per-test SBF traces under
1059    // `target/anchor-v2-profile/`. The cfg switch lives inside
1060    // `anchor-v2-testing` so test code stays clean either way.
1061    let mut svm = anchor_v2_testing::svm();
1062    let bytes = include_bytes!("../../../target/deploy/{0}.so");
1063    svm.add_program(program_id, bytes).unwrap();
1064    svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
1065
1066    let instruction = Instruction::new_with_bytes(
1067        program_id,
1068        &{0}::instruction::Initialize {{}}.data(),
1069        {0}::accounts::Initialize {{
1070            payer: payer.pubkey(),
1071            counter: counter.pubkey(),
1072            system_program: System::id(),
1073        }}
1074        .to_account_metas(None),
1075    );
1076
1077    let blockhash = svm.latest_blockhash();
1078    let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
1079    let tx = VersionedTransaction::try_new(
1080        VersionedMessage::Legacy(msg),
1081        &[&payer, &counter],
1082    )
1083    .unwrap();
1084
1085    let res = svm.send_transaction(tx);
1086    assert!(res.is_ok(), "send_transaction failed: {{:?}}", res);
1087
1088    // Verify the counter account was initialized. Size comes from the same
1089    // `Space::INIT_SPACE` expression the `init` constraint allocates with,
1090    // so the assertion doesn't rot if `Counter` gains fields. The payload
1091    // tail is a `Pod` struct, so we cast directly and read fields by name
1092    // instead of hand-slicing bytes.
1093    let account = svm.get_account(&counter.pubkey()).expect("counter account");
1094    assert_eq!(account.data.len(), <Account<{0}::state::Counter> as Space>::INIT_SPACE);
1095    let counter_state: &{0}::state::Counter = bytemuck::from_bytes(&account.data[8..]);
1096    assert_eq!(counter_state.count, 0);
1097    assert_eq!(counter_state.authority, payer.pubkey());
1098}}
1099"#,
1100            name.to_snake_case(),
1101        ),
1102    )]
1103}