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";
22const ANCHOR_V2_TEMPLATE_VERSION: &str = "2.0.0";
23
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
26pub enum AnchorVersion {
27 #[default]
29 V1,
30 V2,
32}
33
34#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
36pub enum ProgramTemplate {
37 Single,
39 #[default]
41 Multiple,
42}
43
44pub fn create_program(
46 name: &str,
47 template: ProgramTemplate,
48 test_template: Option<&TestTemplate>,
49 anchor_version: AnchorVersion,
50) -> Result<()> {
51 let program_path = Path::new("programs").join(name);
52 let lib_rs_path = program_path.join("src").join("lib.rs");
53 let common_files = vec![
54 ("Cargo.toml".into(), workspace_manifest()),
55 ("rust-toolchain.toml".into(), rust_toolchain_toml()),
56 (
57 program_path.join("Cargo.toml"),
58 cargo_toml(name, test_template, anchor_version),
59 ),
60 (lib_rs_path.clone(), "".into()),
64 ];
66
67 create_files(&common_files)?;
68
69 let target_path = crate::target_dir()?;
70
71 fs::remove_file(&lib_rs_path)?;
73
74 let template_files = match template {
75 ProgramTemplate::Single => {
76 println!(
77 "Note: Using single-file template. For better code organization and \
78 maintainability, consider using --template multiple (default)."
79 );
80 create_program_template_single(name, &program_path, target_path, anchor_version)
81 }
82 ProgramTemplate::Multiple => {
83 create_program_template_multiple(name, &program_path, target_path, anchor_version)
84 }
85 };
86
87 create_files(&template_files)
88}
89
90fn rust_toolchain_toml() -> String {
92 format!(
93 r#"[toolchain]
94channel = "{ANCHOR_MSRV}"
95components = ["rustfmt","clippy"]
96profile = "minimal"
97"#
98 )
99}
100
101fn create_program_template_single(
103 name: &str,
104 program_path: &Path,
105 target_path: &Path,
106 anchor_version: AnchorVersion,
107) -> Files {
108 match anchor_version {
109 AnchorVersion::V1 => create_program_template_single_v1(name, program_path, target_path),
110 AnchorVersion::V2 => create_program_template_single_v2(name, program_path, target_path),
111 }
112}
113
114fn create_program_template_single_v1(name: &str, program_path: &Path, target_path: &Path) -> Files {
115 vec![(
116 program_path.join("src").join("lib.rs"),
117 format!(
118 r#"use anchor_lang::prelude::*;
119
120declare_id!("{}");
121
122#[program]
123pub mod {} {{
124 use super::*;
125
126 pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
127 ctx.accounts.counter.count = 0;
128 ctx.accounts.counter.authority = ctx.accounts.payer.key();
129
130 let cpi_accounts = anchor_lang::system_program::Transfer {{
131 from: ctx.accounts.payer.to_account_info(),
132 to: ctx.accounts.counter.to_account_info(),
133 }};
134 let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
135 anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
136
137 msg!("Hello, world! Counter initialized");
138 Ok(())
139 }}
140
141 pub fn increment(ctx: Context<Increment>) -> Result<()> {{
142 require_keys_eq!(
143 ctx.accounts.counter.authority,
144 ctx.accounts.authority.key(),
145 ErrorCode::Unauthorized,
146 );
147 require!(
148 ctx.accounts.counter.count < MAX_COUNT,
149 ErrorCode::CounterOverflow,
150 );
151
152 ctx.accounts.counter.count += 1;
153 msg!("Hello, world! Counter is now {{}}", ctx.accounts.counter.count);
154 Ok(())
155 }}
156}}
157
158#[derive(Accounts)]
159pub struct Initialize<'info> {{
160 #[account(mut)]
161 pub payer: Signer<'info>,
162 #[account(
163 init,
164 payer = payer,
165 space = 8 + Counter::INIT_SPACE,
166 seeds = [COUNTER_SEED],
167 bump
168 )]
169 pub counter: Account<'info, Counter>,
170 pub system_program: Program<'info, System>,
171}}
172
173#[derive(Accounts)]
174pub struct Increment<'info> {{
175 #[account(mut, seeds = [COUNTER_SEED], bump)]
176 pub counter: Account<'info, Counter>,
177 pub authority: Signer<'info>,
178}}
179
180pub mod constants {{
181 use super::*;
182
183 #[constant]
184 pub const COUNTER_SEED: &[u8] = b"counter";
185
186 #[constant]
187 pub const HELLO_WORLD_LAMPORTS: u64 = 1;
188
189 #[constant]
190 pub const MAX_COUNT: u64 = 10;
191}}
192
193pub mod error {{
194 use super::*;
195
196 #[error_code]
197 pub enum ErrorCode {{
198 #[msg("Only the counter authority can update this counter")]
199 Unauthorized,
200 #[msg("Counter has reached the maximum value")]
201 CounterOverflow,
202 }}
203}}
204
205pub mod state {{
206 use super::*;
207
208 #[account]
209 #[derive(InitSpace)]
210 pub struct Counter {{
211 pub count: u64,
212 pub authority: Pubkey,
213 }}
214}}
215
216use constants::*;
217use error::ErrorCode;
218use state::Counter;
219"#,
220 get_or_create_program_id(name, target_path),
221 name.to_snake_case(),
222 ),
223 )]
224}
225
226fn create_program_template_single_v2(name: &str, program_path: &Path, target_path: &Path) -> Files {
227 vec![(
228 program_path.join("src").join("lib.rs"),
229 format!(
230 r#"use anchor_lang_v2::prelude::*;
231
232declare_id!("{}");
233
234#[program]
235pub mod {} {{
236 use super::*;
237
238 pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {{
239 ctx.accounts.counter.count = 0;
240 ctx.accounts.counter.authority = *ctx.accounts.payer.address();
241 msg!("Counter initialized");
242 Ok(())
243 }}
244}}
245
246pub mod state {{
247 use super::*;
248
249 #[account]
250 pub struct Counter {{
251 pub count: u64,
252 pub authority: Address,
253 }}
254}}
255
256use state::Counter;
257
258#[derive(Accounts)]
259pub struct Initialize {{
260 #[account(mut)]
261 pub payer: Signer,
262 #[account(init, payer = payer)]
263 pub counter: Account<Counter>,
264 pub system_program: Program<System>,
265}}
266"#,
267 get_or_create_program_id(name, target_path),
268 name.to_snake_case(),
269 ),
270 )]
271}
272
273fn create_program_template_multiple(
275 name: &str,
276 program_path: &Path,
277 target_path: &Path,
278 anchor_version: AnchorVersion,
279) -> Files {
280 match anchor_version {
281 AnchorVersion::V1 => create_program_template_multiple_v1(name, program_path, target_path),
282 AnchorVersion::V2 => create_program_template_multiple_v2(name, program_path, target_path),
283 }
284}
285
286fn create_program_template_multiple_v1(
287 name: &str,
288 program_path: &Path,
289 target_path: &Path,
290) -> Files {
291 let src_path = program_path.join("src");
292 vec![
293 (
294 src_path.join("lib.rs"),
295 format!(
296 r#"pub mod constants;
297pub mod error;
298pub mod instructions;
299pub mod state;
300
301use anchor_lang::prelude::*;
302
303pub use constants::*;
304pub use instructions::*;
305pub use state::*;
306
307declare_id!("{}");
308
309#[program]
310pub mod {} {{
311 use super::*;
312
313 pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
314 crate::instructions::initialize::handle_initialize(ctx)
315 }}
316
317 pub fn increment(ctx: Context<Increment>) -> Result<()> {{
318 crate::instructions::increment::handle_increment(ctx)
319 }}
320}}
321"#,
322 get_or_create_program_id(name, target_path),
323 name.to_snake_case(),
324 ),
325 ),
326 (
327 src_path.join("constants.rs"),
328 r#"use anchor_lang::prelude::*;
329
330#[constant]
331pub const COUNTER_SEED: &[u8] = b"counter";
332
333#[constant]
334pub const HELLO_WORLD_LAMPORTS: u64 = 1;
335
336#[constant]
337pub const MAX_COUNT: u64 = 10;
338"#
339 .into(),
340 ),
341 (
342 src_path.join("error.rs"),
343 r#"use anchor_lang::prelude::*;
344
345#[error_code]
346pub enum ErrorCode {
347 #[msg("Only the counter authority can update this counter")]
348 Unauthorized,
349 #[msg("Counter has reached the maximum value")]
350 CounterOverflow,
351}
352"#
353 .into(),
354 ),
355 (
356 src_path.join("instructions.rs"),
357 r#"pub mod initialize;
358pub mod increment;
359
360pub use initialize::*;
361pub use increment::*;
362"#
363 .into(),
364 ),
365 (
366 src_path.join("instructions").join("initialize.rs"),
367 r#"use anchor_lang::prelude::*;
368
369use crate::{constants::*, state::Counter};
370
371#[derive(Accounts)]
372pub struct Initialize<'info> {
373 #[account(mut)]
374 pub payer: Signer<'info>,
375 #[account(
376 init,
377 payer = payer,
378 space = 8 + Counter::INIT_SPACE,
379 seeds = [COUNTER_SEED],
380 bump
381 )]
382 pub counter: Account<'info, Counter>,
383 pub system_program: Program<'info, System>,
384}
385
386pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
387 ctx.accounts.counter.count = 0;
388 ctx.accounts.counter.authority = ctx.accounts.payer.key();
389
390 let cpi_accounts = anchor_lang::system_program::Transfer {
391 from: ctx.accounts.payer.to_account_info(),
392 to: ctx.accounts.counter.to_account_info(),
393 };
394 let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
395 anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
396
397 msg!("Hello, world! Counter initialized");
398 Ok(())
399}
400"#
401 .into(),
402 ),
403 (
404 src_path.join("instructions").join("increment.rs"),
405 r#"use anchor_lang::prelude::*;
406
407use crate::{constants::*, error::ErrorCode, state::Counter};
408
409#[derive(Accounts)]
410pub struct Increment<'info> {
411 #[account(mut, seeds = [COUNTER_SEED], bump)]
412 pub counter: Account<'info, Counter>,
413 pub authority: Signer<'info>,
414}
415
416pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
417 require_keys_eq!(
418 ctx.accounts.counter.authority,
419 ctx.accounts.authority.key(),
420 ErrorCode::Unauthorized,
421 );
422 require!(
423 ctx.accounts.counter.count < MAX_COUNT,
424 ErrorCode::CounterOverflow,
425 );
426
427 ctx.accounts.counter.count += 1;
428 msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
429 Ok(())
430}
431"#
432 .into(),
433 ),
434 (
435 src_path.join("state.rs"),
436 r#"use anchor_lang::prelude::*;
437
438#[account]
439#[derive(InitSpace)]
440pub struct Counter {
441 pub count: u64,
442 pub authority: Pubkey,
443}
444"#
445 .into(),
446 ),
447 ]
448}
449
450fn create_program_template_multiple_v2(
451 name: &str,
452 program_path: &Path,
453 target_path: &Path,
454) -> Files {
455 let src_path = program_path.join("src");
456 vec![
457 (
458 src_path.join("lib.rs"),
459 format!(
460 r#"pub mod constants;
461pub mod error;
462pub mod instructions;
463pub mod state;
464
465use anchor_lang_v2::prelude::*;
466
467pub use instructions::*;
468
469declare_id!("{}");
470
471#[program]
472pub mod {} {{
473 use super::*;
474
475 pub fn initialize(ctx: &mut Context<Initialize>) -> Result<()> {{
476 initialize::handler(ctx)
477 }}
478}}
479"#,
480 get_or_create_program_id(name, target_path),
481 name.to_snake_case(),
482 ),
483 ),
484 (
485 src_path.join("constants.rs"),
486 r#"use anchor_lang_v2::prelude::*;
487
488#[constant]
489pub const SEED: &str = "anchor";
490"#
491 .into(),
492 ),
493 (
494 src_path.join("error.rs"),
495 r#"use anchor_lang_v2::prelude::*;
496
497#[error_code]
498pub enum ErrorCode {
499 #[msg("Custom error message")]
500 CustomError,
501}
502"#
503 .into(),
504 ),
505 (
506 src_path.join("instructions.rs"),
507 r#"pub mod initialize;
508
509pub use initialize::*;
510"#
511 .into(),
512 ),
513 (
514 src_path.join("instructions").join("initialize.rs"),
515 r#"use anchor_lang_v2::prelude::*;
516
517use crate::state::Counter;
518
519#[derive(Accounts)]
520pub struct Initialize {
521 #[account(mut)]
522 pub payer: Signer,
523 #[account(init, payer = payer)]
524 pub counter: Account<Counter>,
525 pub system_program: Program<System>,
526}
527
528pub fn handler(ctx: &mut Context<Initialize>) -> Result<()> {
529 ctx.accounts.counter.count = 0;
530 ctx.accounts.counter.authority = *ctx.accounts.payer.address();
531 msg!("Counter initialized");
532 Ok(())
533}
534"#
535 .into(),
536 ),
537 (
538 src_path.join("state.rs"),
539 r#"use anchor_lang_v2::prelude::*;
540
541#[account]
542pub struct Counter {
543 pub count: u64,
544 pub authority: Address,
545}
546"#
547 .into(),
548 ),
549 ]
550}
551
552fn workspace_manifest() -> String {
553 format!(
554 r#"[workspace]
555members = [
556 "programs/*"
557]
558resolver = "2"
559
560[workspace.package]
561edition = "2021"
562rust-version = "{ANCHOR_MSRV}"
563
564[profile.release]
565overflow-checks = true
566lto = "fat"
567codegen-units = 1
568[profile.release.build-override]
569opt-level = 3
570incremental = false
571codegen-units = 1
572"#
573 )
574}
575
576fn cargo_toml(
577 name: &str,
578 test_template: Option<&TestTemplate>,
579 anchor_version: AnchorVersion,
580) -> String {
581 match anchor_version {
582 AnchorVersion::V1 => cargo_toml_v1(name, test_template),
583 AnchorVersion::V2 => cargo_toml_v2(name, test_template),
584 }
585}
586
587fn cargo_toml_v1(name: &str, test_template: Option<&TestTemplate>) -> String {
588 let template_features = match test_template {
589 Some(TestTemplate::Mollusk) => r#"test-sbf = []"#,
590 _ => "",
591 };
592 let dev_dependencies = match test_template {
593 Some(TestTemplate::Mollusk) => {
594 r#"
595[dev-dependencies]
596mollusk-svm = "~0.10"
597solana-account = "3"
598solana-pubkey = "3"
599solana-sdk-ids = "3"
600"#
601 }
602 Some(TestTemplate::Litesvm) => {
603 r#"
604[dev-dependencies]
605litesvm = "0.10.0"
606solana-message = "3.0.1"
607solana-transaction = "3.0.2"
608solana-signer = "3.0.0"
609solana-keypair = "3.0.1"
610"#
611 }
612 _ => "",
613 };
614
615 format!(
616 r#"[package]
617name = "{0}"
618version = "0.1.0"
619description = "Created with Anchor"
620edition.workspace = true
621rust-version.workspace = true
622
623[lib]
624crate-type = ["cdylib", "lib"]
625name = "{1}"
626
627[features]
628default = []
629cpi = ["no-entrypoint"]
630no-entrypoint = []
631no-log-ix-name = []
632idl-build = ["anchor-lang/idl-build"]
633anchor-debug = []
634custom-heap = []
635custom-panic = []
636{2}
637
638[dependencies]
639anchor-lang = "{3}"
640{4}
641
642[lints.rust]
643unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }}
644"#,
645 name,
646 name.to_snake_case(),
647 template_features,
648 VERSION,
649 dev_dependencies,
650 )
651}
652
653fn cargo_toml_v2(name: &str, test_template: Option<&TestTemplate>) -> String {
654 let template_features = match test_template {
659 Some(TestTemplate::Mollusk) => r#"test-sbf = []"#,
660 Some(TestTemplate::Litesvm) => r#"profile = ["anchor-v2-testing/profile"]"#,
661 _ => "",
662 };
663 let dev_dependencies = match test_template {
664 Some(TestTemplate::Mollusk) => {
665 r#"
666[dev-dependencies]
667mollusk-svm = "~0.10"
668solana-account = "3"
669solana-pubkey = "4"
670solana-sdk-ids = "3"
671bytemuck = "1"
672"#
673 }
674 Some(TestTemplate::Litesvm) => {
675 r#"
676[dev-dependencies]
677anchor-v2-testing = { git = "https://github.com/solana-foundation/anchor.git", branch = "anchor-next" }
678"#
679 }
680 _ => "",
681 };
682
683 format!(
684 r#"[package]
685name = "{0}"
686version = "0.1.0"
687description = "Created with Anchor"
688edition.workspace = true
689rust-version.workspace = true
690
691[lib]
692crate-type = ["cdylib", "lib"]
693name = "{1}"
694
695[features]
696default = []
697cpi = ["no-entrypoint"]
698no-entrypoint = []
699no-log-ix-name = []
700idl-build = []
701{2}
702
703[dependencies]
704# Once anchor-lang-v2 is published to crates.io, swap to: anchor-lang-v2 = "{3}"
705anchor-lang-v2 = {{ git = "https://github.com/solana-foundation/anchor.git", branch = "anchor-next" }}
706solana-program-log = {{ version = "1.1", features = ["macro"] }}
707wincode = {{ version = "0.5", features = ["derive"] }}
708{4}
709
710[lints.rust]
711unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }}
712"#,
713 name,
714 name.to_snake_case(),
715 template_features,
716 ANCHOR_V2_TEMPLATE_VERSION,
717 dev_dependencies,
718 )
719}
720
721pub fn get_or_create_program_id(name: &str, target_path: impl AsRef<Path>) -> Pubkey {
723 let keypair_path = target_path
724 .as_ref()
725 .join("deploy")
726 .join(format!("{}-keypair.json", name.to_snake_case()));
727
728 read_keypair_file(&keypair_path)
729 .unwrap_or_else(|_| {
730 let keypair = Keypair::new();
731 write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
732 keypair
733 })
734 .pubkey()
735}
736
737pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
738 format!(
739 r#"
740const anchor = require('@anchor-lang/core');
741
742// Deploy script defined by the user.
743const userScript = require("{script_path}");
744
745async function main() {{
746 const connection = new anchor.web3.Connection(
747 "{cluster_url}",
748 anchor.AnchorProvider.defaultOptions().commitment
749 );
750 const wallet = anchor.Wallet.local();
751 const provider = new anchor.AnchorProvider(connection, wallet);
752
753 // Run the user's deploy script.
754 userScript(provider);
755}}
756main();
757"#,
758 )
759}
760
761pub fn deploy_ts_script_host(cluster_url: &str, script_path: &str) -> String {
762 format!(
763 r#"import * as anchor from '@anchor-lang/core';
764
765// Deploy script defined by the user.
766const userScript = require("{script_path}");
767
768async function main() {{
769 const connection = new anchor.web3.Connection(
770 "{cluster_url}",
771 anchor.AnchorProvider.defaultOptions().commitment
772 );
773 const wallet = anchor.Wallet.local();
774 const provider = new anchor.AnchorProvider(connection, wallet);
775
776 // Run the user's deploy script.
777 userScript(provider);
778}}
779main();
780"#,
781 )
782}
783
784pub fn deploy_script() -> &'static str {
785 r#"// Migrations are an early feature. Currently, they're nothing more than this
786// single deploy script that's invoked from the CLI, injecting a provider
787// configured from the workspace's Anchor.toml.
788
789const anchor = require("@anchor-lang/core");
790
791module.exports = async function (provider) {
792 // Configure client to use the provider.
793 anchor.setProvider(provider);
794
795 // Add your deploy script here.
796};
797"#
798}
799
800pub fn ts_deploy_script() -> &'static str {
801 r#"// Migrations are an early feature. Currently, they're nothing more than this
802// single deploy script that's invoked from the CLI, injecting a provider
803// configured from the workspace's Anchor.toml.
804
805import * as anchor from "@anchor-lang/core";
806
807module.exports = async function (provider: anchor.AnchorProvider) {
808 // Configure client to use the provider.
809 anchor.setProvider(provider);
810
811 // Add your deploy script here.
812};
813"#
814}
815
816pub fn mocha(name: &str, anchor_version: AnchorVersion) -> String {
817 match anchor_version {
818 AnchorVersion::V1 => mocha_v1(name),
819 AnchorVersion::V2 => mocha_v2(name),
820 }
821}
822
823fn mocha_v1(name: &str) -> String {
824 format!(
825 r#"const anchor = require("@anchor-lang/core");
826
827describe("{}", () => {{
828 // Configure the client to use the local cluster.
829 anchor.setProvider(anchor.AnchorProvider.env());
830
831 it("Initializes and increments a counter", async () => {{
832 const program = anchor.workspace.{};
833 const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
834 [Buffer.from("counter")],
835 program.programId
836 );
837
838 const initializeTx = await program.methods
839 .initialize()
840 .accountsPartial({{ counter }})
841 .rpc();
842 console.log("Initialize transaction signature", initializeTx);
843
844 const incrementTx = await program.methods
845 .increment()
846 .accountsPartial({{ counter }})
847 .rpc();
848 console.log("Increment transaction signature", incrementTx);
849 }});
850}});
851"#,
852 name,
853 name.to_lower_camel_case(),
854 )
855}
856
857fn mocha_v2(name: &str) -> String {
858 format!(
859 r#"const anchor = require("@anchor-lang/core");
860
861describe("{}", () => {{
862 // Configure the client to use the local cluster.
863 anchor.setProvider(anchor.AnchorProvider.env());
864
865 it("Is initialized!", async () => {{
866 // Add your test here.
867 const program = anchor.workspace.{};
868 const counter = anchor.web3.Keypair.generate();
869 const tx = await program.methods
870 .initialize()
871 .accounts({{ counter: counter.publicKey }})
872 .signers([counter])
873 .rpc();
874 console.log("Your transaction signature", tx);
875 }});
876}});
877"#,
878 name,
879 name.to_lower_camel_case(),
880 )
881}
882
883pub fn js_jest(name: &str, anchor_version: AnchorVersion) -> String {
884 match anchor_version {
885 AnchorVersion::V1 => js_jest_v1(name),
886 AnchorVersion::V2 => js_jest_v2(name),
887 }
888}
889
890fn js_jest_v1(name: &str) -> String {
891 format!(
892 r#"const anchor = require("@anchor-lang/core");
893
894describe("{}", () => {{
895 // Configure the client to use the local cluster.
896 anchor.setProvider(anchor.AnchorProvider.env());
897
898 it("Initializes and increments a counter", async () => {{
899 const program = anchor.workspace.{};
900 const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
901 [Buffer.from("counter")],
902 program.programId
903 );
904
905 const initializeTx = await program.methods
906 .initialize()
907 .accountsPartial({{ counter }})
908 .rpc();
909 console.log("Initialize transaction signature", initializeTx);
910
911 const incrementTx = await program.methods
912 .increment()
913 .accountsPartial({{ counter }})
914 .rpc();
915 console.log("Increment transaction signature", incrementTx);
916 }});
917}});
918"#,
919 name,
920 name.to_lower_camel_case(),
921 )
922}
923
924fn js_jest_v2(name: &str) -> String {
925 format!(
926 r#"const anchor = require("@anchor-lang/core");
927
928describe("{}", () => {{
929 // Configure the client to use the local cluster.
930 anchor.setProvider(anchor.AnchorProvider.env());
931
932 it("Is initialized!", async () => {{
933 // Add your test here.
934 const program = anchor.workspace.{};
935 const counter = anchor.web3.Keypair.generate();
936 const tx = await program.methods
937 .initialize()
938 .accounts({{ counter: counter.publicKey }})
939 .signers([counter])
940 .rpc();
941 console.log("Your transaction signature", tx);
942 }});
943}});
944"#,
945 name,
946 name.to_lower_camel_case(),
947 )
948}
949
950pub fn package_json(jest: bool, license: String, anchor_version: AnchorVersion) -> String {
951 match anchor_version {
952 AnchorVersion::V1 => package_json_v1(jest, license),
953 AnchorVersion::V2 => package_json_v2(jest, license),
954 }
955}
956
957fn package_json_v1(jest: bool, license: String) -> String {
958 if jest {
959 format!(
960 r#"{{
961 "license": "{license}",
962 "scripts": {{
963 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
964 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
965 }},
966 "dependencies": {{
967 "@anchor-lang/core": "^{VERSION}"
968 }},
969 "devDependencies": {{
970 "jest": "^29.0.3",
971 "prettier": "^2.6.2"
972 }},
973 "overrides": {{
974 "uuid": "^9.0.1"
975 }},
976 "resolutions": {{
977 "uuid": "^9.0.1"
978 }},
979 "pnpm": {{
980 "overrides": {{
981 "uuid": "^9.0.1"
982 }}
983 }}
984}}
985 "#
986 )
987 } else {
988 format!(
989 r#"{{
990 "license": "{license}",
991 "scripts": {{
992 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
993 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
994 }},
995 "dependencies": {{
996 "@anchor-lang/core": "^{VERSION}"
997 }},
998 "devDependencies": {{
999 "chai": "^4.3.4",
1000 "mocha": "^9.0.3",
1001 "prettier": "^2.6.2"
1002 }}
1003}}
1004"#
1005 )
1006 }
1007}
1008
1009fn package_json_v2(jest: bool, license: String) -> String {
1011 if jest {
1012 format!(
1013 r#"{{
1014 "license": "{license}",
1015 "scripts": {{
1016 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
1017 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
1018 }},
1019 "dependencies": {{
1020 "@anchor-lang/core": "^1.0.0"
1021 }},
1022 "devDependencies": {{
1023 "jest": "^30.3.0",
1024 "prettier": "^3.8.3"
1025 }},
1026 "overrides": {{
1027 "uuid": "^9.0.1"
1028 }},
1029 "resolutions": {{
1030 "uuid": "^9.0.1"
1031 }},
1032 "pnpm": {{
1033 "overrides": {{
1034 "uuid": "^9.0.1"
1035 }}
1036 }}
1037}}
1038 "#
1039 )
1040 } else {
1041 format!(
1042 r#"{{
1043 "license": "{license}",
1044 "scripts": {{
1045 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
1046 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
1047 }},
1048 "dependencies": {{
1049 "@anchor-lang/core": "^1.0.0"
1050 }},
1051 "devDependencies": {{
1052 "chai": "^4.5.0",
1053 "mocha": "^11.7.5",
1054 "prettier": "^3.8.3"
1055 }}
1056}}
1057"#
1058 )
1059 }
1060}
1061
1062pub fn ts_package_json(jest: bool, license: String, anchor_version: AnchorVersion) -> String {
1063 match anchor_version {
1064 AnchorVersion::V1 => ts_package_json_v1(jest, license),
1065 AnchorVersion::V2 => ts_package_json_v2(jest, license),
1066 }
1067}
1068
1069fn ts_package_json_v1(jest: bool, license: String) -> String {
1070 if jest {
1071 format!(
1072 r#"{{
1073 "license": "{license}",
1074 "scripts": {{
1075 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
1076 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
1077 }},
1078 "dependencies": {{
1079 "@anchor-lang/core": "^{VERSION}"
1080 }},
1081 "devDependencies": {{
1082 "@types/bn.js": "^5.1.0",
1083 "@types/jest": "^29.0.3",
1084 "jest": "^29.0.3",
1085 "prettier": "^2.6.2",
1086 "ts-jest": "^29.0.2",
1087 "typescript": "^5.7.3"
1088 }},
1089 "overrides": {{
1090 "uuid": "^9.0.1"
1091 }},
1092 "resolutions": {{
1093 "uuid": "^9.0.1"
1094 }},
1095 "pnpm": {{
1096 "overrides": {{
1097 "uuid": "^9.0.1"
1098 }}
1099 }}
1100}}
1101"#
1102 )
1103 } else {
1104 format!(
1105 r#"{{
1106 "license": "{license}",
1107 "scripts": {{
1108 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
1109 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
1110 }},
1111 "dependencies": {{
1112 "@anchor-lang/core": "^{VERSION}"
1113 }},
1114 "devDependencies": {{
1115 "chai": "^4.3.4",
1116 "mocha": "^9.0.3",
1117 "ts-mocha": "^10.0.0",
1118 "@types/bn.js": "^5.1.0",
1119 "@types/chai": "^4.3.0",
1120 "@types/mocha": "^9.0.0",
1121 "typescript": "^5.7.3",
1122 "prettier": "^2.6.2"
1123 }}
1124}}
1125"#
1126 )
1127 }
1128}
1129
1130fn ts_package_json_v2(jest: bool, license: String) -> String {
1132 if jest {
1133 format!(
1134 r#"{{
1135 "license": "{license}",
1136 "scripts": {{
1137 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
1138 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
1139 }},
1140 "dependencies": {{
1141 "@anchor-lang/core": "^1.0.0"
1142 }},
1143 "devDependencies": {{
1144 "@types/bn.js": "^5.2.0",
1145 "@types/jest": "^30.0.0",
1146 "jest": "^30.3.0",
1147 "prettier": "^3.8.3",
1148 "ts-jest": "^29.4.9",
1149 "typescript": "^5.9.3"
1150 }},
1151 "overrides": {{
1152 "uuid": "^9.0.1"
1153 }},
1154 "resolutions": {{
1155 "uuid": "^9.0.1"
1156 }},
1157 "pnpm": {{
1158 "overrides": {{
1159 "uuid": "^9.0.1"
1160 }}
1161 }}
1162}}
1163"#
1164 )
1165 } else {
1166 format!(
1167 r#"{{
1168 "license": "{license}",
1169 "scripts": {{
1170 "lint:fix": "prettier */*.js \"*/**/*{{.js,.ts}}\" -w",
1171 "lint": "prettier */*.js \"*/**/*{{.js,.ts}}\" --check"
1172 }},
1173 "dependencies": {{
1174 "@anchor-lang/core": "^1.0.0"
1175 }},
1176 "devDependencies": {{
1177 "chai": "^4.5.0",
1178 "mocha": "^11.7.5",
1179 "ts-mocha": "^11.1.0",
1180 "ts-node": "^10.9.2",
1181 "@types/bn.js": "^5.2.0",
1182 "@types/chai": "^4.3.0",
1183 "@types/mocha": "^10.0.10",
1184 "@types/node": "^25.6.0",
1185 "typescript": "^5.9.3",
1186 "prettier": "^3.8.3"
1187 }}
1188}}
1189"#
1190 )
1191 }
1192}
1193
1194pub fn ts_mocha(name: &str, anchor_version: AnchorVersion) -> String {
1195 match anchor_version {
1196 AnchorVersion::V1 => ts_mocha_v1(name),
1197 AnchorVersion::V2 => ts_mocha_v2(name),
1198 }
1199}
1200
1201fn ts_mocha_v1(name: &str) -> String {
1202 format!(
1203 r#"import * as anchor from "@anchor-lang/core";
1204import {{ Program }} from "@anchor-lang/core";
1205import {{ {} }} from "../target/types/{}";
1206
1207describe("{}", () => {{
1208 // Configure the client to use the local cluster.
1209 anchor.setProvider(anchor.AnchorProvider.env());
1210
1211 const program = anchor.workspace.{} as Program<{}>;
1212
1213 it("Initializes and increments a counter", async () => {{
1214 const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
1215 [Buffer.from("counter")],
1216 program.programId
1217 );
1218
1219 const initializeTx = await program.methods
1220 .initialize()
1221 .accountsPartial({{ counter }})
1222 .rpc();
1223 console.log("Initialize transaction signature", initializeTx);
1224
1225 const incrementTx = await program.methods
1226 .increment()
1227 .accountsPartial({{ counter }})
1228 .rpc();
1229 console.log("Increment transaction signature", incrementTx);
1230 }});
1231}});
1232"#,
1233 name.to_pascal_case(),
1234 name.to_snake_case(),
1235 name,
1236 name.to_lower_camel_case(),
1237 name.to_pascal_case(),
1238 )
1239}
1240
1241fn ts_mocha_v2(name: &str) -> String {
1242 format!(
1243 r#"import * as anchor from "@anchor-lang/core";
1244import {{ Program }} from "@anchor-lang/core";
1245import {{ {} }} from "../target/types/{}";
1246
1247describe("{}", () => {{
1248 // Configure the client to use the local cluster.
1249 anchor.setProvider(anchor.AnchorProvider.env());
1250
1251 const program = anchor.workspace.{} as Program<{}>;
1252
1253 it("Is initialized!", async () => {{
1254 // Add your test here.
1255 const counter = anchor.web3.Keypair.generate();
1256 const tx = await program.methods
1257 .initialize()
1258 .accounts({{ counter: counter.publicKey }})
1259 .signers([counter])
1260 .rpc();
1261 console.log("Your transaction signature", tx);
1262 }});
1263}});
1264"#,
1265 name.to_pascal_case(),
1266 name.to_snake_case(),
1267 name,
1268 name.to_lower_camel_case(),
1269 name.to_pascal_case(),
1270 )
1271}
1272
1273pub fn ts_jest(name: &str, anchor_version: AnchorVersion) -> String {
1274 match anchor_version {
1275 AnchorVersion::V1 => ts_jest_v1(name),
1276 AnchorVersion::V2 => ts_jest_v2(name),
1277 }
1278}
1279
1280fn ts_jest_v1(name: &str) -> String {
1281 format!(
1282 r#"import * as anchor from "@anchor-lang/core";
1283import {{ Program }} from "@anchor-lang/core";
1284import {{ {} }} from "../target/types/{}";
1285
1286describe("{}", () => {{
1287 // Configure the client to use the local cluster.
1288 anchor.setProvider(anchor.AnchorProvider.env());
1289
1290 const program = anchor.workspace.{} as Program<{}>;
1291
1292 it("Initializes and increments a counter", async () => {{
1293 const [counter] = anchor.web3.PublicKey.findProgramAddressSync(
1294 [Buffer.from("counter")],
1295 program.programId
1296 );
1297
1298 const initializeTx = await program.methods
1299 .initialize()
1300 .accountsPartial({{ counter }})
1301 .rpc();
1302 console.log("Initialize transaction signature", initializeTx);
1303
1304 const incrementTx = await program.methods
1305 .increment()
1306 .accountsPartial({{ counter }})
1307 .rpc();
1308 console.log("Increment transaction signature", incrementTx);
1309 }});
1310}});
1311"#,
1312 name.to_pascal_case(),
1313 name.to_snake_case(),
1314 name,
1315 name.to_lower_camel_case(),
1316 name.to_pascal_case(),
1317 )
1318}
1319
1320fn ts_jest_v2(name: &str) -> String {
1321 format!(
1322 r#"import * as anchor from "@anchor-lang/core";
1323import {{ Program }} from "@anchor-lang/core";
1324import {{ {} }} from "../target/types/{}";
1325
1326describe("{}", () => {{
1327 // Configure the client to use the local cluster.
1328 anchor.setProvider(anchor.AnchorProvider.env());
1329
1330 const program = anchor.workspace.{} as Program<{}>;
1331
1332 it("Is initialized!", async () => {{
1333 // Add your test here.
1334 const counter = anchor.web3.Keypair.generate();
1335 const tx = await program.methods
1336 .initialize()
1337 .accounts({{ counter: counter.publicKey }})
1338 .signers([counter])
1339 .rpc();
1340 console.log("Your transaction signature", tx);
1341 }});
1342}});
1343"#,
1344 name.to_pascal_case(),
1345 name.to_snake_case(),
1346 name,
1347 name.to_lower_camel_case(),
1348 name.to_pascal_case(),
1349 )
1350}
1351
1352pub fn ts_config(jest: bool) -> &'static str {
1353 if jest {
1354 r#"{
1355 "compilerOptions": {
1356 "types": ["jest"],
1357 "typeRoots": ["./node_modules/@types"],
1358 "lib": ["es2015"],
1359 "module": "commonjs",
1360 "target": "es6",
1361 "esModuleInterop": true
1362 }
1363}
1364"#
1365 } else {
1366 r#"{
1367 "compilerOptions": {
1368 "types": ["mocha", "chai"],
1369 "typeRoots": ["./node_modules/@types"],
1370 "lib": ["es2015"],
1371 "module": "commonjs",
1372 "target": "es6",
1373 "esModuleInterop": true
1374 }
1375}
1376"#
1377 }
1378}
1379
1380pub fn git_ignore() -> &'static str {
1381 r#".anchor
1382.DS_Store
1383target
1384**/*.rs.bk
1385node_modules
1386test-ledger
1387.yarn
1388.surfpool
1389"#
1390}
1391
1392pub fn prettier_ignore() -> &'static str {
1393 r#".anchor
1394.DS_Store
1395target
1396node_modules
1397dist
1398build
1399test-ledger
1400"#
1401}
1402
1403pub fn node_shell(
1404 cluster_url: &str,
1405 wallet_path: &str,
1406 programs: Vec<ProgramWorkspace>,
1407) -> Result<String> {
1408 let mut eval_string = format!(
1409 r#"
1410const anchor = require('@anchor-lang/core');
1411const web3 = anchor.web3;
1412const PublicKey = anchor.web3.PublicKey;
1413const Keypair = anchor.web3.Keypair;
1414
1415const __wallet = new anchor.Wallet(
1416 Keypair.fromSecretKey(
1417 Buffer.from(
1418 JSON.parse(
1419 require('fs').readFileSync(
1420 "{wallet_path}",
1421 {{
1422 encoding: "utf-8",
1423 }},
1424 ),
1425 ),
1426 ),
1427 ),
1428);
1429const __connection = new web3.Connection("{cluster_url}", "processed");
1430const provider = new anchor.AnchorProvider(__connection, __wallet, {{
1431 commitment: "processed",
1432 preflightcommitment: "processed",
1433}});
1434anchor.setProvider(provider);
1435"#,
1436 );
1437
1438 for program in programs {
1439 write!(
1440 &mut eval_string,
1441 r#"
1442anchor.workspace.{} = new anchor.Program({}, provider);
1443"#,
1444 program.name.to_lower_camel_case(),
1445 serde_json::to_string(&program.idl)?,
1446 )?;
1447 }
1448
1449 Ok(eval_string)
1450}
1451
1452#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, AbsolutePath)]
1454pub enum TestTemplate {
1455 Mocha,
1457 Jest,
1459 Rust,
1461 Mollusk,
1463 #[default]
1465 Litesvm,
1466}
1467
1468impl TestTemplate {
1469 pub fn uses_node(&self) -> bool {
1470 matches!(self, Self::Mocha | Self::Jest)
1471 }
1472
1473 pub fn get_test_script(&self, js: bool, pkg_manager: Option<&PackageManager>) -> String {
1474 let pkg_manager_exec_cmd = match pkg_manager {
1475 Some(PackageManager::Yarn) => "yarn run",
1476 Some(PackageManager::NPM) => "npx",
1477 Some(PackageManager::PNPM) => "pnpm exec",
1478 Some(PackageManager::Bun) => "bunx",
1479 None => "",
1480 };
1481
1482 match &self {
1483 Self::Mocha => {
1484 if js {
1485 format!("{pkg_manager_exec_cmd} mocha -t 1000000 tests/")
1486 } else {
1487 format!(
1488 r#"{pkg_manager_exec_cmd} ts-mocha -p ./tsconfig.json -t 1000000 "tests/**/*.ts""#
1489 )
1490 }
1491 }
1492 Self::Jest => {
1493 if js {
1494 format!("{pkg_manager_exec_cmd} jest")
1495 } else {
1496 format!("{pkg_manager_exec_cmd} jest --preset ts-jest")
1497 }
1498 }
1499 Self::Rust | Self::Litesvm => "cargo test".to_owned(),
1500 Self::Mollusk => "cargo test-sbf".to_owned(),
1501 }
1502 }
1503
1504 pub fn create_test_files(
1505 &self,
1506 project_name: &str,
1507 js: bool,
1508 program_id: &str,
1509 anchor_version: AnchorVersion,
1510 ) -> Result<()> {
1511 match self {
1512 Self::Mocha => {
1513 fs::create_dir_all("tests")?;
1515
1516 if js {
1517 let mut test = File::create(format!("tests/{}.js", &project_name))?;
1518 test.write_all(mocha(project_name, anchor_version).as_bytes())?;
1519 } else {
1520 let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
1521 mocha.write_all(ts_mocha(project_name, anchor_version).as_bytes())?;
1522 }
1523 }
1524 Self::Jest => {
1525 fs::create_dir_all("tests")?;
1527
1528 if js {
1529 let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
1530 test.write_all(js_jest(project_name, anchor_version).as_bytes())?;
1531 } else {
1532 let mut test = File::create(format!("tests/{}.test.ts", &project_name))?;
1533 test.write_all(ts_jest(project_name, anchor_version).as_bytes())?;
1534 }
1535 }
1536 Self::Rust => {
1537 let exit = std::process::Command::new("cargo")
1539 .arg("new")
1540 .arg("--vcs")
1541 .arg("none")
1542 .arg("--lib")
1543 .arg("tests")
1544 .stderr(Stdio::inherit())
1545 .output()
1546 .map_err(|e| anyhow::format_err!("{}", e))?;
1547 if !exit.status.success() {
1548 eprintln!("'cargo new --lib tests' failed");
1549 std::process::exit(exit.status.code().unwrap_or(1));
1550 }
1551
1552 let mut files = Vec::new();
1553 let tests_path = Path::new("tests");
1554 files.extend(vec![(
1555 tests_path.join("Cargo.toml"),
1556 tests_cargo_toml(project_name, anchor_version),
1557 )]);
1558 files.extend(create_program_template_rust_test(
1559 project_name,
1560 tests_path,
1561 program_id,
1562 anchor_version,
1563 ));
1564 override_or_create_files(&files)?;
1565 }
1566 Self::Mollusk => {
1567 let tests_path_str = format!("programs/{}/tests", &project_name);
1569 let tests_path = Path::new(&tests_path_str);
1570 fs::create_dir_all(tests_path)?;
1571
1572 let mut files = Vec::new();
1573 files.extend(create_program_template_mollusk_test(
1574 project_name,
1575 tests_path,
1576 anchor_version,
1577 ));
1578 override_or_create_files(&files)?;
1579 }
1580
1581 Self::Litesvm => {
1582 let tests_path_str = format!("programs/{}/tests", &project_name);
1583 let tests_path = Path::new(&tests_path_str);
1584 fs::create_dir_all(tests_path)?;
1585 let mut files = Vec::new();
1586 files.extend(create_program_template_litesvm_test(
1587 project_name,
1588 tests_path,
1589 anchor_version,
1590 ));
1591 override_or_create_files(&files)?;
1592 }
1593 }
1594
1595 Ok(())
1596 }
1597}
1598
1599pub fn tests_cargo_toml(name: &str, anchor_version: AnchorVersion) -> String {
1600 match anchor_version {
1601 AnchorVersion::V1 => tests_cargo_toml_v1(name),
1602 AnchorVersion::V2 => tests_cargo_toml_v2(name),
1603 }
1604}
1605
1606fn tests_cargo_toml_v1(name: &str) -> String {
1607 format!(
1608 r#"[package]
1609name = "tests"
1610version = "0.1.0"
1611description = "Created with Anchor"
1612edition = "2021"
1613rust-version = "{ANCHOR_MSRV}"
1614
1615[dependencies]
1616anchor-client = "{VERSION}"
1617{name} = {{ version = "0.1.0", path = "../programs/{name}" }}
1618solana-keypair = "3.0.0"
1619solana-pubkey = "3.0.0"
1620solana-sdk-ids = "3"
1621solana-signer = "3"
1622"#
1623 )
1624}
1625
1626fn tests_cargo_toml_v2(name: &str) -> String {
1627 format!(
1628 r#"[package]
1629name = "tests"
1630version = "0.1.0"
1631description = "Created with Anchor"
1632edition = "2021"
1633rust-version = "{ANCHOR_MSRV}"
1634
1635[dependencies]
1636# Once anchor-client v2 is published to crates.io, swap to: anchor-client = "{ANCHOR_V2_TEMPLATE_VERSION}"
1637anchor-client = {{ git = "https://github.com/solana-foundation/anchor.git", branch = "anchor-next" }}
1638{name} = {{ version = "0.1.0", path = "../programs/{name}" }}
1639solana-keypair = "3.0.0"
1640solana-pubkey = "3.0.0"
1641solana-sdk-ids = "3"
1642solana-signer = "3"
1643"#
1644 )
1645}
1646
1647fn create_program_template_rust_test(
1649 name: &str,
1650 tests_path: &Path,
1651 program_id: &str,
1652 anchor_version: AnchorVersion,
1653) -> Files {
1654 match anchor_version {
1655 AnchorVersion::V1 => create_program_template_rust_test_v1(name, tests_path, program_id),
1656 AnchorVersion::V2 => create_program_template_rust_test_v2(name, tests_path, program_id),
1657 }
1658}
1659
1660fn create_program_template_rust_test_v1(name: &str, tests_path: &Path, program_id: &str) -> Files {
1661 let src_path = tests_path.join("src");
1662 vec![
1663 (
1664 src_path.join("lib.rs"),
1665 r#"#[cfg(test)]
1666mod test_initialize;
1667"#
1668 .into(),
1669 ),
1670 (
1671 src_path.join("test_initialize.rs"),
1672 format!(
1673 r#"use anchor_client::{{
1674 CommitmentConfig,
1675 Client, Cluster,
1676}};
1677use solana_keypair::read_keypair_file;
1678use solana_pubkey::Pubkey;
1679use solana_signer::Signer;
1680
1681#[test]
1682fn test_initialize() {{
1683 let program_id = "{0}";
1684 let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
1685 let payer = read_keypair_file(&anchor_wallet).unwrap();
1686
1687 let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
1688 let program_id = Pubkey::try_from(program_id).unwrap();
1689 let program = client.program(program_id).unwrap();
1690 let counter = Pubkey::find_program_address(
1691 &[{1}::constants::COUNTER_SEED],
1692 &program_id,
1693 )
1694 .0;
1695
1696 let initialize_tx = program
1697 .request()
1698 .accounts({1}::accounts::Initialize {{
1699 payer: payer.pubkey(),
1700 counter,
1701 system_program: solana_sdk_ids::system_program::id(),
1702 }})
1703 .args({1}::instruction::Initialize {{}})
1704 .send()
1705 .expect("");
1706
1707 println!("Initialize transaction signature {{}}", initialize_tx);
1708
1709 let increment_tx = program
1710 .request()
1711 .accounts({1}::accounts::Increment {{
1712 counter,
1713 authority: payer.pubkey(),
1714 }})
1715 .args({1}::instruction::Increment {{}})
1716 .send()
1717 .expect("");
1718
1719 println!("Increment transaction signature {{}}", increment_tx);
1720}}
1721"#,
1722 program_id,
1723 name.to_snake_case(),
1724 ),
1725 ),
1726 ]
1727}
1728
1729fn create_program_template_rust_test_v2(name: &str, tests_path: &Path, program_id: &str) -> Files {
1730 let src_path = tests_path.join("src");
1731 vec![
1732 (
1733 src_path.join("lib.rs"),
1734 r#"#[cfg(test)]
1735mod test_initialize;
1736"#
1737 .into(),
1738 ),
1739 (
1740 src_path.join("test_initialize.rs"),
1741 format!(
1742 r#"use anchor_client::{{
1743 CommitmentConfig,
1744 Client, Cluster,
1745}};
1746use solana_keypair::{{read_keypair_file, Keypair}};
1747use solana_pubkey::Pubkey;
1748use solana_signer::Signer;
1749
1750#[test]
1751fn test_initialize() {{
1752 let program_id = "{0}";
1753 let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
1754 let payer = read_keypair_file(&anchor_wallet).unwrap();
1755 let counter = Keypair::new();
1756
1757 let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
1758 let program_id = Pubkey::try_from(program_id).unwrap();
1759 let program = client.program(program_id).unwrap();
1760
1761 let tx = program
1762 .request()
1763 .accounts({1}::accounts::Initialize {{
1764 payer: payer.pubkey(),
1765 counter: counter.pubkey(),
1766 system_program: solana_sdk_ids::system_program::id(),
1767 }})
1768 .args({1}::instruction::Initialize {{}})
1769 .signer(&counter)
1770 .send()
1771 .expect("");
1772
1773 println!("Your transaction signature {{}}", tx);
1774}}
1775"#,
1776 program_id,
1777 name.to_snake_case(),
1778 ),
1779 ),
1780 ]
1781}
1782
1783fn create_program_template_mollusk_test(
1785 name: &str,
1786 tests_path: &Path,
1787 anchor_version: AnchorVersion,
1788) -> Files {
1789 match anchor_version {
1790 AnchorVersion::V1 => create_program_template_mollusk_test_v1(name, tests_path),
1791 AnchorVersion::V2 => create_program_template_mollusk_test_v2(name, tests_path),
1792 }
1793}
1794
1795fn create_program_template_mollusk_test_v1(name: &str, tests_path: &Path) -> Files {
1796 vec![(
1797 tests_path.join("test_initialize.rs"),
1798 format!(
1799 r#"#![cfg(feature = "test-sbf")]
1800
1801use {{
1802 anchor_lang::{{
1803 solana_program::instruction::Instruction, AccountDeserialize, InstructionData,
1804 Space, ToAccountMetas,
1805 }},
1806 mollusk_svm::{{program::keyed_account_for_system_program, result::Check, Mollusk}},
1807 solana_account::Account as SolanaAccount,
1808 solana_pubkey::Pubkey,
1809}};
1810
1811#[test]
1812fn test_initialize() {{
1813 let program_id = {0}::id();
1814 let mollusk = Mollusk::new(&program_id, "{0}");
1815 let payer = Pubkey::new_unique();
1816 let counter = Pubkey::find_program_address(
1817 &[{0}::constants::COUNTER_SEED],
1818 &program_id,
1819 )
1820 .0;
1821
1822 let instruction = Instruction::new_with_bytes(
1823 program_id,
1824 &{0}::instruction::Initialize {{}}.data(),
1825 {0}::accounts::Initialize {{
1826 payer,
1827 counter,
1828 system_program: solana_sdk_ids::system_program::id(),
1829 }}
1830 .to_account_metas(None),
1831 );
1832
1833 let accounts = vec![
1834 (
1835 payer,
1836 SolanaAccount::new(1_000_000_000, 0, &solana_sdk_ids::system_program::id()),
1837 ),
1838 (counter, SolanaAccount::default()),
1839 keyed_account_for_system_program(),
1840 ];
1841
1842 let result = mollusk.process_and_validate_instruction(
1843 &instruction,
1844 &accounts,
1845 &[Check::success()],
1846 );
1847
1848 let payer_account = result
1849 .resulting_accounts
1850 .iter()
1851 .find(|(pk, _)| *pk == payer)
1852 .map(|(_, a)| a.clone())
1853 .expect("payer account");
1854 let counter_account = result
1855 .resulting_accounts
1856 .iter()
1857 .find(|(pk, _)| *pk == counter)
1858 .map(|(_, a)| a.clone())
1859 .expect("counter account");
1860 assert_eq!(
1861 counter_account.data.len(),
1862 8 + {0}::state::Counter::INIT_SPACE
1863 );
1864 let mut data: &[u8] = &counter_account.data;
1865 let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
1866 assert_eq!(counter_state.count, 0);
1867 assert_eq!(counter_state.authority, payer);
1868
1869 let instruction = Instruction::new_with_bytes(
1870 program_id,
1871 &{0}::instruction::Increment {{}}.data(),
1872 {0}::accounts::Increment {{
1873 counter,
1874 authority: payer,
1875 }}
1876 .to_account_metas(None),
1877 );
1878 let accounts = vec![(counter, counter_account), (payer, payer_account)];
1879
1880 let result = mollusk.process_and_validate_instruction(
1881 &instruction,
1882 &accounts,
1883 &[Check::success()],
1884 );
1885
1886 let counter_account = result
1887 .resulting_accounts
1888 .iter()
1889 .find(|(pk, _)| *pk == counter)
1890 .map(|(_, a)| a)
1891 .expect("counter account");
1892 let mut data: &[u8] = &counter_account.data;
1893 let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
1894 assert_eq!(counter_state.count, 1);
1895 assert_eq!(counter_state.authority, payer);
1896}}
1897"#,
1898 name.to_snake_case(),
1899 ),
1900 )]
1901}
1902
1903fn create_program_template_mollusk_test_v2(name: &str, tests_path: &Path) -> Files {
1904 vec![(
1905 tests_path.join("test_initialize.rs"),
1906 format!(
1907 r#"#![cfg(feature = "test-sbf")]
1908
1909use {{
1910 anchor_lang_v2::{{
1911 accounts::Account, solana_program::instruction::Instruction, InstructionData, Space,
1912 ToAccountMetas,
1913 }},
1914 mollusk_svm::{{program::keyed_account_for_system_program, result::Check, Mollusk}},
1915 solana_account::Account as SolanaAccount,
1916 solana_pubkey::Pubkey,
1917}};
1918
1919#[test]
1920fn test_initialize() {{
1921 let program_id = {0}::id();
1922 let mollusk = Mollusk::new(&program_id, "{0}");
1923
1924 let payer = Pubkey::new_unique();
1925 let counter = Pubkey::new_unique();
1926
1927 let instruction = Instruction::new_with_bytes(
1928 program_id,
1929 &{0}::instruction::Initialize {{}}.data(),
1930 {0}::accounts::Initialize {{
1931 payer,
1932 counter,
1933 system_program: solana_sdk_ids::system_program::id(),
1934 }}
1935 .to_account_metas(None),
1936 );
1937
1938 let counter_space = <Account<{0}::state::Counter> as Space>::INIT_SPACE;
1939 let accounts = vec![
1940 (
1941 payer,
1942 SolanaAccount::new(1_000_000_000, 0, &solana_sdk_ids::system_program::id()),
1943 ),
1944 (counter, SolanaAccount::default()),
1945 keyed_account_for_system_program(),
1946 ];
1947
1948 let result = mollusk.process_and_validate_instruction(
1949 &instruction,
1950 &accounts,
1951 &[Check::success()],
1952 );
1953
1954 let counter_account = result
1955 .resulting_accounts
1956 .iter()
1957 .find(|(pk, _)| *pk == counter)
1958 .map(|(_, a)| a)
1959 .expect("counter account");
1960 assert_eq!(counter_account.data.len(), counter_space);
1961 let counter_state: &{0}::state::Counter = bytemuck::from_bytes(&counter_account.data[8..]);
1962 assert_eq!(counter_state.count, 0);
1963 assert_eq!(counter_state.authority, payer);
1964}}
1965"#,
1966 name.to_snake_case(),
1967 ),
1968 )]
1969}
1970
1971fn create_program_template_litesvm_test(
1973 name: &str,
1974 tests_path: &Path,
1975 anchor_version: AnchorVersion,
1976) -> Files {
1977 match anchor_version {
1978 AnchorVersion::V1 => create_program_template_litesvm_test_v1(name, tests_path),
1979 AnchorVersion::V2 => create_program_template_litesvm_test_v2(name, tests_path),
1980 }
1981}
1982
1983fn create_program_template_litesvm_test_v1(name: &str, tests_path: &Path) -> Files {
1984 vec![(
1985 tests_path.join("test_initialize.rs"),
1986 format!(
1987 r#"
1988use {{
1989 anchor_lang::{{
1990 prelude::Pubkey,
1991 solana_program::{{instruction::Instruction, system_program}},
1992 AccountDeserialize, InstructionData, ToAccountMetas,
1993 }},
1994 litesvm::LiteSVM,
1995 solana_keypair::Keypair,
1996 solana_message::{{Message, VersionedMessage}},
1997 solana_signer::Signer,
1998 solana_transaction::versioned::VersionedTransaction,
1999}};
2000
2001#[test]
2002fn test_initialize() {{
2003 let program_id = {0}::id();
2004 let payer = Keypair::new();
2005 let counter = Pubkey::find_program_address(
2006 &[{0}::constants::COUNTER_SEED],
2007 &program_id,
2008 )
2009 .0;
2010 let mut svm = LiteSVM::new();
2011 let bytes = include_bytes!(concat!(
2012 env!("CARGO_TARGET_TMPDIR"),
2013 "/../deploy/{0}.so"
2014 ));
2015 svm.add_program(program_id, bytes).unwrap();
2016 svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
2017
2018 let instruction = Instruction::new_with_bytes(
2019 program_id,
2020 &{0}::instruction::Initialize {{}}.data(),
2021 {0}::accounts::Initialize {{
2022 payer: payer.pubkey(),
2023 counter,
2024 system_program: system_program::ID,
2025 }}
2026 .to_account_metas(None),
2027 );
2028
2029 let blockhash = svm.latest_blockhash();
2030 let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
2031 let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
2032
2033 let res = svm.send_transaction(tx);
2034 assert!(res.is_ok());
2035
2036 let counter_account = svm.get_account(&counter).unwrap();
2037 let mut data: &[u8] = &counter_account.data;
2038 let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
2039 assert_eq!(counter_state.count, 0);
2040 assert_eq!(counter_state.authority, payer.pubkey());
2041
2042 let instruction = Instruction::new_with_bytes(
2043 program_id,
2044 &{0}::instruction::Increment {{}}.data(),
2045 {0}::accounts::Increment {{
2046 counter,
2047 authority: payer.pubkey(),
2048 }}
2049 .to_account_metas(None),
2050 );
2051
2052 let blockhash = svm.latest_blockhash();
2053 let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
2054 let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
2055
2056 let res = svm.send_transaction(tx);
2057 assert!(res.is_ok());
2058
2059 let counter_account = svm.get_account(&counter).unwrap();
2060 let mut data: &[u8] = &counter_account.data;
2061 let counter_state = {0}::state::Counter::try_deserialize(&mut data).unwrap();
2062 assert_eq!(counter_state.count, 1);
2063 assert_eq!(counter_state.authority, payer.pubkey());
2064}}
2065"#,
2066 name.to_snake_case(),
2067 ),
2068 )]
2069}
2070
2071fn create_program_template_litesvm_test_v2(name: &str, tests_path: &Path) -> Files {
2072 vec![(
2073 tests_path.join("test_initialize.rs"),
2074 format!(
2075 r#"
2076use {{
2077 anchor_lang_v2::{{
2078 accounts::Account, bytemuck, programs::System,
2079 solana_program::instruction::Instruction, Id, InstructionData, Space, ToAccountMetas,
2080 }},
2081 anchor_v2_testing::{{Keypair, Message, Signer, VersionedMessage, VersionedTransaction}},
2082}};
2083
2084#[test]
2085fn test_initialize() {{
2086 let program_id = {0}::id();
2087 let payer = Keypair::new();
2088 let counter = Keypair::new();
2089
2090 // `svm()` is `LiteSVM::new()` by default. When this crate is built
2091 // with `--features profile` (which `anchor test --profile` and
2092 // `anchor debugger` set automatically), it also installs the
2093 // register-tracing callback that writes per-test SBF traces under
2094 // `target/anchor-v2-profile/`. The cfg switch lives inside
2095 // `anchor-v2-testing` so test code stays clean either way.
2096 let mut svm = anchor_v2_testing::svm();
2097 let bytes = include_bytes!("../../../target/deploy/{0}.so");
2098 svm.add_program(program_id, bytes).unwrap();
2099 svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
2100
2101 let instruction = Instruction::new_with_bytes(
2102 program_id,
2103 &{0}::instruction::Initialize {{}}.data(),
2104 {0}::accounts::Initialize {{
2105 payer: payer.pubkey(),
2106 counter: counter.pubkey(),
2107 system_program: System::id(),
2108 }}
2109 .to_account_metas(None),
2110 );
2111
2112 let blockhash = svm.latest_blockhash();
2113 let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
2114 let tx = VersionedTransaction::try_new(
2115 VersionedMessage::Legacy(msg),
2116 &[&payer, &counter],
2117 )
2118 .unwrap();
2119
2120 let res = svm.send_transaction(tx);
2121 assert!(res.is_ok(), "send_transaction failed: {{:?}}", res);
2122
2123 // Verify the counter account was initialized. Size comes from the same
2124 // `Space::INIT_SPACE` expression the `init` constraint allocates with,
2125 // so the assertion doesn't rot if `Counter` gains fields. The payload
2126 // tail is a `Pod` struct, so we cast directly and read fields by name
2127 // instead of hand-slicing bytes.
2128 let account = svm.get_account(&counter.pubkey()).expect("counter account");
2129 assert_eq!(account.data.len(), <Account<{0}::state::Counter> as Space>::INIT_SPACE);
2130 let counter_state: &{0}::state::Counter = bytemuck::from_bytes(&account.data[8..]);
2131 assert_eq!(counter_state.count, 0);
2132 assert_eq!(counter_state.authority, payer.pubkey());
2133}}
2134"#,
2135 name.to_snake_case(),
2136 ),
2137 )]
2138}
2139
2140#[cfg(test)]
2141mod tests {
2142 use super::*;
2143
2144 #[test]
2145 fn v1_templates_keep_legacy_anchor_lang_shape() {
2146 let manifest = cargo_toml("counter", Some(&TestTemplate::Litesvm), AnchorVersion::V1);
2147 assert!(manifest.contains("anchor-lang ="));
2148 assert!(manifest.contains("litesvm = \"0.10.0\""));
2149 assert!(!manifest.contains("anchor-lang-v2"));
2150
2151 let test = ts_mocha("counter", AnchorVersion::V1);
2152 assert!(test.contains("[Buffer.from(\"counter\")]"));
2153 assert!(test.contains(".accountsPartial({ counter })"));
2154 assert!(!test.contains("counter: counter.publicKey"));
2155 }
2156
2157 #[test]
2158 fn v2_templates_use_anchor_next_counter_shape() {
2159 let manifest = cargo_toml("counter", Some(&TestTemplate::Litesvm), AnchorVersion::V2);
2160 assert!(manifest.contains("anchor-lang-v2 = { git = "));
2161 assert!(manifest.contains("profile = [\"anchor-v2-testing/profile\"]"));
2162 assert!(manifest.contains("anchor-v2-testing = { git = "));
2163
2164 let test = ts_mocha("counter", AnchorVersion::V2);
2165 assert!(test.contains("const counter = anchor.web3.Keypair.generate();"));
2166 assert!(test.contains("counter: counter.publicKey"));
2167 }
2168}