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#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
23pub enum ProgramTemplate {
24 #[default]
26 Single,
27 Multiple,
29}
30
31pub 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
51fn 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
79fn 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
227pub 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#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
623pub enum TestTemplate {
624 #[default]
626 Mocha,
627 Jest,
629 Rust,
631 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 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 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 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 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
766fn 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
816fn 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}