anchor_cli/
rust_template.rs

1use crate::{
2    config::ProgramWorkspace, create_files, override_or_create_files, Files, PackageManager,
3    VERSION,
4};
5use anyhow::Result;
6use clap::{Parser, ValueEnum};
7use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
8use solana_sdk::{
9    pubkey::Pubkey,
10    signature::{read_keypair_file, write_keypair_file, Keypair},
11    signer::Signer,
12};
13use std::{
14    fmt::Write as _,
15    fs::{self, File},
16    io::Write as _,
17    path::Path,
18    process::Stdio,
19};
20
21const ANCHOR_MSRV: &str = "1.89.0";
22
23/// Program initialization template
24#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
25pub enum ProgramTemplate {
26    /// Program with a single `lib.rs` file
27    #[default]
28    Single,
29    /// Program with multiple files for instructions, state...
30    Multiple,
31}
32
33/// Create a program from the given name and template.
34pub fn create_program(name: &str, template: ProgramTemplate, with_mollusk: bool) -> Result<()> {
35    let program_path = Path::new("programs").join(name);
36    let common_files = vec![
37        ("Cargo.toml".into(), workspace_manifest().into()),
38        ("rust-toolchain.toml".into(), rust_toolchain_toml()),
39        (
40            program_path.join("Cargo.toml"),
41            cargo_toml(name, with_mollusk),
42        ),
43        // Note: Xargo.toml is no longer needed for modern Solana builds using SBF
44    ];
45
46    let template_files = match template {
47        ProgramTemplate::Single => create_program_template_single(name, &program_path),
48        ProgramTemplate::Multiple => create_program_template_multiple(name, &program_path),
49    };
50
51    create_files(&[common_files, template_files].concat())
52}
53
54/// Helper to create a rust-toolchain.toml at the workspace root
55fn rust_toolchain_toml() -> String {
56    format!(
57        r#"[toolchain]
58channel = "{msrv}"
59components = ["rustfmt","clippy"]
60profile = "minimal"
61"#,
62        msrv = ANCHOR_MSRV
63    )
64}
65
66/// Create a program with a single `lib.rs` file.
67fn create_program_template_single(name: &str, program_path: &Path) -> Files {
68    vec![(
69        program_path.join("src").join("lib.rs"),
70        format!(
71            r#"use anchor_lang::prelude::*;
72
73declare_id!("{}");
74
75#[program]
76pub mod {} {{
77    use super::*;
78
79    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
80        msg!("Greetings from: {{:?}}", ctx.program_id);
81        Ok(())
82    }}
83}}
84
85#[derive(Accounts)]
86pub struct Initialize {{}}
87"#,
88            get_or_create_program_id(name),
89            name.to_snake_case(),
90        ),
91    )]
92}
93
94/// Create a program with multiple files for instructions, state...
95fn create_program_template_multiple(name: &str, program_path: &Path) -> Files {
96    let src_path = program_path.join("src");
97    vec![
98        (
99            src_path.join("lib.rs"),
100            format!(
101                r#"pub mod constants;
102pub mod error;
103pub mod instructions;
104pub mod state;
105
106use anchor_lang::prelude::*;
107
108pub use constants::*;
109pub use instructions::*;
110pub use state::*;
111
112declare_id!("{}");
113
114#[program]
115pub mod {} {{
116    use super::*;
117
118    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
119        initialize::handler(ctx)
120    }}
121}}
122"#,
123                get_or_create_program_id(name),
124                name.to_snake_case(),
125            ),
126        ),
127        (
128            src_path.join("constants.rs"),
129            r#"use anchor_lang::prelude::*;
130
131#[constant]
132pub const SEED: &str = "anchor";
133"#
134            .into(),
135        ),
136        (
137            src_path.join("error.rs"),
138            r#"use anchor_lang::prelude::*;
139
140#[error_code]
141pub enum ErrorCode {
142    #[msg("Custom error message")]
143    CustomError,
144}
145"#
146            .into(),
147        ),
148        (
149            src_path.join("instructions").join("mod.rs"),
150            r#"pub mod initialize;
151
152pub use initialize::*;
153"#
154            .into(),
155        ),
156        (
157            src_path.join("instructions").join("initialize.rs"),
158            r#"use anchor_lang::prelude::*;
159
160#[derive(Accounts)]
161pub struct Initialize {}
162
163pub fn handler(ctx: Context<Initialize>) -> Result<()> {
164    msg!("Greetings from: {:?}", ctx.program_id);
165    Ok(())
166}
167"#
168            .into(),
169        ),
170        (src_path.join("state").join("mod.rs"), r#""#.into()),
171    ]
172}
173
174const fn workspace_manifest() -> &'static str {
175    r#"[workspace]
176members = [
177    "programs/*"
178]
179resolver = "2"
180
181[profile.release]
182overflow-checks = true
183lto = "fat"
184codegen-units = 1
185[profile.release.build-override]
186opt-level = 3
187incremental = false
188codegen-units = 1
189"#
190}
191
192fn cargo_toml(name: &str, with_mollusk: bool) -> String {
193    let test_sbf_feature = if with_mollusk { r#"test-sbf = []"# } else { "" };
194    let dev_dependencies = if with_mollusk {
195        r#"
196[dev-dependencies]
197mollusk-svm = "~0.4"
198"#
199    } else {
200        ""
201    };
202
203    format!(
204        r#"[package]
205name = "{0}"
206version = "0.1.0"
207description = "Created with Anchor"
208edition = "2021"
209
210[lib]
211crate-type = ["cdylib", "lib"]
212name = "{1}"
213
214[features]
215default = []
216cpi = ["no-entrypoint"]
217no-entrypoint = []
218no-idl = []
219no-log-ix-name = []
220idl-build = ["anchor-lang/idl-build"]
221anchor-debug = []
222custom-heap = []
223custom-panic = []
224{2}
225
226[dependencies]
227anchor-lang = "{3}"
228{4}
229
230[lints.rust]
231unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }}
232"#,
233        name,
234        name.to_snake_case(),
235        test_sbf_feature,
236        VERSION,
237        dev_dependencies,
238    )
239}
240
241/// Read the program keypair file or create a new one if it doesn't exist.
242pub fn get_or_create_program_id(name: &str) -> Pubkey {
243    let keypair_path = Path::new("target")
244        .join("deploy")
245        .join(format!("{}-keypair.json", name.to_snake_case()));
246
247    read_keypair_file(&keypair_path)
248        .unwrap_or_else(|_| {
249            let keypair = Keypair::new();
250            write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
251            keypair
252        })
253        .pubkey()
254}
255
256pub fn credentials(token: &str) -> String {
257    format!(
258        r#"[registry]
259token = "{token}"
260"#
261    )
262}
263
264pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
265    format!(
266        r#"
267const anchor = require('@coral-xyz/anchor');
268
269// Deploy script defined by the user.
270const userScript = require("{script_path}");
271
272async function main() {{
273    const connection = new anchor.web3.Connection(
274      "{cluster_url}",
275      anchor.AnchorProvider.defaultOptions().commitment
276    );
277    const wallet = anchor.Wallet.local();
278    const provider = new anchor.AnchorProvider(connection, wallet);
279
280    // Run the user's deploy script.
281    userScript(provider);
282}}
283main();
284"#,
285    )
286}
287
288pub fn deploy_ts_script_host(cluster_url: &str, script_path: &str) -> String {
289    format!(
290        r#"import * as anchor from '@coral-xyz/anchor';
291
292// Deploy script defined by the user.
293const userScript = require("{script_path}");
294
295async function main() {{
296    const connection = new anchor.web3.Connection(
297      "{cluster_url}",
298      anchor.AnchorProvider.defaultOptions().commitment
299    );
300    const wallet = anchor.Wallet.local();
301    const provider = new anchor.AnchorProvider(connection, wallet);
302
303    // Run the user's deploy script.
304    userScript(provider);
305}}
306main();
307"#,
308    )
309}
310
311pub fn deploy_script() -> &'static str {
312    r#"// Migrations are an early feature. Currently, they're nothing more than this
313// single deploy script that's invoked from the CLI, injecting a provider
314// configured from the workspace's Anchor.toml.
315
316const anchor = require("@coral-xyz/anchor");
317
318module.exports = async function (provider) {
319  // Configure client to use the provider.
320  anchor.setProvider(provider);
321
322  // Add your deploy script here.
323};
324"#
325}
326
327pub fn ts_deploy_script() -> &'static str {
328    r#"// Migrations are an early feature. Currently, they're nothing more than this
329// single deploy script that's invoked from the CLI, injecting a provider
330// configured from the workspace's Anchor.toml.
331
332import * as anchor from "@coral-xyz/anchor";
333
334module.exports = async function (provider: anchor.AnchorProvider) {
335  // Configure client to use the provider.
336  anchor.setProvider(provider);
337
338  // Add your deploy script here.
339};
340"#
341}
342
343pub fn mocha(name: &str) -> String {
344    format!(
345        r#"const anchor = require("@coral-xyz/anchor");
346
347describe("{}", () => {{
348  // Configure the client to use the local cluster.
349  anchor.setProvider(anchor.AnchorProvider.env());
350
351  it("Is initialized!", async () => {{
352    // Add your test here.
353    const program = anchor.workspace.{};
354    const tx = await program.methods.initialize().rpc();
355    console.log("Your transaction signature", tx);
356  }});
357}});
358"#,
359        name,
360        name.to_lower_camel_case(),
361    )
362}
363
364pub fn jest(name: &str) -> String {
365    format!(
366        r#"const anchor = require("@coral-xyz/anchor");
367
368describe("{}", () => {{
369  // Configure the client to use the local cluster.
370  anchor.setProvider(anchor.AnchorProvider.env());
371
372  it("Is initialized!", async () => {{
373    // Add your test here.
374    const program = anchor.workspace.{};
375    const tx = await program.methods.initialize().rpc();
376    console.log("Your transaction signature", tx);
377  }});
378}});
379"#,
380        name,
381        name.to_lower_camel_case(),
382    )
383}
384
385pub fn package_json(jest: bool, license: String) -> String {
386    if jest {
387        format!(
388            r#"{{
389  "license": "{license}",
390  "scripts": {{
391    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
392    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
393  }},
394  "dependencies": {{
395    "@coral-xyz/anchor": "^{VERSION}"
396  }},
397  "devDependencies": {{
398    "jest": "^29.0.3",
399    "prettier": "^2.6.2"
400  }}
401}}
402    "#
403        )
404    } else {
405        format!(
406            r#"{{
407  "license": "{license}",
408  "scripts": {{
409    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
410    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
411  }},
412  "dependencies": {{
413    "@coral-xyz/anchor": "^{VERSION}"
414  }},
415  "devDependencies": {{
416    "chai": "^4.3.4",
417    "mocha": "^9.0.3",
418    "prettier": "^2.6.2"
419  }}
420}}
421"#
422        )
423    }
424}
425
426pub fn ts_package_json(jest: bool, license: String) -> String {
427    if jest {
428        format!(
429            r#"{{
430  "license": "{license}",
431  "scripts": {{
432    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
433    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
434  }},
435  "dependencies": {{
436    "@coral-xyz/anchor": "^{VERSION}"
437  }},
438  "devDependencies": {{
439    "@types/bn.js": "^5.1.0",
440    "@types/jest": "^29.0.3",
441    "jest": "^29.0.3",
442    "prettier": "^2.6.2",
443    "ts-jest": "^29.0.2",
444    "typescript": "^5.7.3"
445  }}
446}}
447"#
448        )
449    } else {
450        format!(
451            r#"{{
452  "license": "{license}",
453  "scripts": {{
454    "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
455    "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
456  }},
457  "dependencies": {{
458    "@coral-xyz/anchor": "^{VERSION}"
459  }},
460  "devDependencies": {{
461    "chai": "^4.3.4",
462    "mocha": "^9.0.3",
463    "ts-mocha": "^10.0.0",
464    "@types/bn.js": "^5.1.0",
465    "@types/chai": "^4.3.0",
466    "@types/mocha": "^9.0.0",
467    "typescript": "^5.7.3",
468    "prettier": "^2.6.2"
469  }}
470}}
471"#
472        )
473    }
474}
475
476pub fn ts_mocha(name: &str) -> String {
477    format!(
478        r#"import * as anchor from "@coral-xyz/anchor";
479import {{ Program }} from "@coral-xyz/anchor";
480import {{ {} }} from "../target/types/{}";
481
482describe("{}", () => {{
483  // Configure the client to use the local cluster.
484  anchor.setProvider(anchor.AnchorProvider.env());
485
486  const program = anchor.workspace.{} as Program<{}>;
487
488  it("Is initialized!", async () => {{
489    // Add your test here.
490    const tx = await program.methods.initialize().rpc();
491    console.log("Your transaction signature", tx);
492  }});
493}});
494"#,
495        name.to_pascal_case(),
496        name.to_snake_case(),
497        name,
498        name.to_lower_camel_case(),
499        name.to_pascal_case(),
500    )
501}
502
503pub fn ts_jest(name: &str) -> String {
504    format!(
505        r#"import * as anchor from "@coral-xyz/anchor";
506import {{ Program }} from "@coral-xyz/anchor";
507import {{ {} }} from "../target/types/{}";
508
509describe("{}", () => {{
510  // Configure the client to use the local cluster.
511  anchor.setProvider(anchor.AnchorProvider.env());
512
513  const program = anchor.workspace.{} as Program<{}>;
514
515  it("Is initialized!", async () => {{
516    // Add your test here.
517    const tx = await program.methods.initialize().rpc();
518    console.log("Your transaction signature", tx);
519  }});
520}});
521"#,
522        name.to_pascal_case(),
523        name.to_snake_case(),
524        name,
525        name.to_lower_camel_case(),
526        name.to_pascal_case(),
527    )
528}
529
530pub fn ts_config(jest: bool) -> &'static str {
531    if jest {
532        r#"{
533  "compilerOptions": {
534    "types": ["jest"],
535    "typeRoots": ["./node_modules/@types"],
536    "lib": ["es2015"],
537    "module": "commonjs",
538    "target": "es6",
539    "esModuleInterop": true
540  }
541}
542"#
543    } else {
544        r#"{
545  "compilerOptions": {
546    "types": ["mocha", "chai"],
547    "typeRoots": ["./node_modules/@types"],
548    "lib": ["es2015"],
549    "module": "commonjs",
550    "target": "es6",
551    "esModuleInterop": true
552  }
553}
554"#
555    }
556}
557
558pub fn git_ignore() -> &'static str {
559    r#".anchor
560.DS_Store
561target
562**/*.rs.bk
563node_modules
564test-ledger
565.yarn
566"#
567}
568
569pub fn prettier_ignore() -> &'static str {
570    r#".anchor
571.DS_Store
572target
573node_modules
574dist
575build
576test-ledger
577"#
578}
579
580pub fn node_shell(
581    cluster_url: &str,
582    wallet_path: &str,
583    programs: Vec<ProgramWorkspace>,
584) -> Result<String> {
585    let mut eval_string = format!(
586        r#"
587const anchor = require('@coral-xyz/anchor');
588const web3 = anchor.web3;
589const PublicKey = anchor.web3.PublicKey;
590const Keypair = anchor.web3.Keypair;
591
592const __wallet = new anchor.Wallet(
593  Keypair.fromSecretKey(
594    Buffer.from(
595      JSON.parse(
596        require('fs').readFileSync(
597          "{wallet_path}",
598          {{
599            encoding: "utf-8",
600          }},
601        ),
602      ),
603    ),
604  ),
605);
606const __connection = new web3.Connection("{cluster_url}", "processed");
607const provider = new anchor.AnchorProvider(__connection, __wallet, {{
608  commitment: "processed",
609  preflightcommitment: "processed",
610}});
611anchor.setProvider(provider);
612"#,
613    );
614
615    for program in programs {
616        write!(
617            &mut eval_string,
618            r#"
619anchor.workspace.{} = new anchor.Program({}, provider);
620"#,
621            program.name.to_lower_camel_case(),
622            serde_json::to_string(&program.idl)?,
623        )?;
624    }
625
626    Ok(eval_string)
627}
628
629/// Test initialization template
630#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
631pub enum TestTemplate {
632    /// Generate template for Mocha unit-test
633    #[default]
634    Mocha,
635    /// Generate template for Jest unit-test
636    Jest,
637    /// Generate template for Rust unit-test
638    Rust,
639    /// Generate template for Mollusk Rust unit-test
640    Mollusk,
641}
642
643impl TestTemplate {
644    pub fn get_test_script(&self, js: bool, pkg_manager: &PackageManager) -> String {
645        let pkg_manager_exec_cmd = match pkg_manager {
646            PackageManager::Yarn => "yarn run",
647            PackageManager::NPM => "npx",
648            PackageManager::PNPM => "pnpm exec",
649            PackageManager::Bun => "bunx",
650        };
651
652        match &self {
653            Self::Mocha => {
654                if js {
655                    format!("{pkg_manager_exec_cmd} mocha -t 1000000 tests/")
656                } else {
657                    format!(
658                        r#"{pkg_manager_exec_cmd} ts-mocha -p ./tsconfig.json -t 1000000 "tests/**/*.ts""#
659                    )
660                }
661            }
662            Self::Jest => {
663                if js {
664                    format!("{pkg_manager_exec_cmd} jest")
665                } else {
666                    format!("{pkg_manager_exec_cmd} jest --preset ts-jest")
667                }
668            }
669            Self::Rust => "cargo test".to_owned(),
670            Self::Mollusk => "cargo test-sbf".to_owned(),
671        }
672    }
673
674    pub fn create_test_files(&self, project_name: &str, js: bool, program_id: &str) -> Result<()> {
675        match self {
676            Self::Mocha => {
677                // Build the test suite.
678                fs::create_dir_all("tests")?;
679
680                if js {
681                    let mut test = File::create(format!("tests/{}.js", &project_name))?;
682                    test.write_all(mocha(project_name).as_bytes())?;
683                } else {
684                    let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
685                    mocha.write_all(ts_mocha(project_name).as_bytes())?;
686                }
687            }
688            Self::Jest => {
689                // Build the test suite.
690                fs::create_dir_all("tests")?;
691
692                let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
693                test.write_all(jest(project_name).as_bytes())?;
694            }
695            Self::Rust => {
696                // Do not initialize git repo
697                let exit = std::process::Command::new("cargo")
698                    .arg("new")
699                    .arg("--vcs")
700                    .arg("none")
701                    .arg("--lib")
702                    .arg("tests")
703                    .stderr(Stdio::inherit())
704                    .output()
705                    .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
706                if !exit.status.success() {
707                    eprintln!("'cargo new --lib tests' failed");
708                    std::process::exit(exit.status.code().unwrap_or(1));
709                }
710
711                let mut files = Vec::new();
712                let tests_path = Path::new("tests");
713                files.extend(vec![(
714                    tests_path.join("Cargo.toml"),
715                    tests_cargo_toml(project_name),
716                )]);
717                files.extend(create_program_template_rust_test(
718                    project_name,
719                    tests_path,
720                    program_id,
721                ));
722                override_or_create_files(&files)?;
723            }
724            Self::Mollusk => {
725                // Build the test suite.
726                let tests_path_str = format!("programs/{}/tests", &project_name);
727                let tests_path = Path::new(&tests_path_str);
728                fs::create_dir_all(tests_path)?;
729
730                let mut files = Vec::new();
731                files.extend(create_program_template_mollusk_test(
732                    project_name,
733                    tests_path,
734                ));
735                override_or_create_files(&files)?;
736            }
737        }
738
739        Ok(())
740    }
741}
742
743pub fn tests_cargo_toml(name: &str) -> String {
744    format!(
745        r#"[package]
746name = "tests"
747version = "0.1.0"
748description = "Created with Anchor"
749edition = "2021"
750rust-version = "{msrv}"
751
752[dependencies]
753anchor-client = "{version}"
754{name} = {{ version = "0.1.0", path = "../programs/{name}" }}
755"#,
756        msrv = ANCHOR_MSRV,
757        version = VERSION,
758        name = name,
759    )
760}
761
762/// Generate template for Rust unit-test
763fn create_program_template_rust_test(name: &str, tests_path: &Path, program_id: &str) -> Files {
764    let src_path = tests_path.join("src");
765    vec![
766        (
767            src_path.join("lib.rs"),
768            r#"#[cfg(test)]
769mod test_initialize;
770"#
771            .into(),
772        ),
773        (
774            src_path.join("test_initialize.rs"),
775            format!(
776                r#"use std::str::FromStr;
777
778use anchor_client::{{
779    solana_sdk::{{
780        commitment_config::CommitmentConfig, pubkey::Pubkey, signature::read_keypair_file,
781    }},
782    Client, Cluster,
783}};
784
785#[test]
786fn test_initialize() {{
787    let program_id = "{0}";
788    let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
789    let payer = read_keypair_file(&anchor_wallet).unwrap();
790
791    let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
792    let program_id = Pubkey::from_str(program_id).unwrap();
793    let program = client.program(program_id).unwrap();
794
795    let tx = program
796        .request()
797        .accounts({1}::accounts::Initialize {{}})
798        .args({1}::instruction::Initialize {{}})
799        .send()
800        .expect("");
801
802    println!("Your transaction signature {{}}", tx);
803}}
804"#,
805                program_id,
806                name.to_snake_case(),
807            ),
808        ),
809    ]
810}
811
812/// Generate template for Mollusk Rust unit-test
813fn create_program_template_mollusk_test(name: &str, tests_path: &Path) -> Files {
814    vec![(
815        tests_path.join("test_initialize.rs"),
816        format!(
817            r#"#![cfg(feature = "test-sbf")]
818
819use {{
820    anchor_lang::{{solana_program::instruction::Instruction, InstructionData, ToAccountMetas}},
821    mollusk_svm::{{result::Check, Mollusk}},
822}};
823
824#[test]
825fn test_initialize() {{
826    let program_id = {0}::id();
827
828    let mollusk = Mollusk::new(&program_id, "{0}");
829
830    let instruction = Instruction::new_with_bytes(
831        program_id,
832        &{0}::instruction::Initialize {{}}.data(),
833        {0}::accounts::Initialize {{}}.to_account_metas(None),
834    );
835
836    mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);
837}}
838"#,
839            name.to_snake_case(),
840        ),
841    )]
842}