anchor_cli/
rust_template.rs

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