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#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
25pub enum ProgramTemplate {
26 #[default]
28 Single,
29 Multiple,
31}
32
33pub 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 ];
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
54fn rust_toolchain_toml() -> String {
56 format!(
57 r#"[toolchain]
58channel = "{msrv}"
59components = ["rustfmt","clippy"]
60profile = "minimal"
61"#,
62 msrv = ANCHOR_MSRV
63 )
64}
65
66fn 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
94fn 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
241pub 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#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
631pub enum TestTemplate {
632 #[default]
634 Mocha,
635 Jest,
637 Rust,
639 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 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 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 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 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
762fn 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
812fn 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}