1use {
2 crate::{
3 config::{GlobalConfig, GlobalDefaults, UiConfig},
4 error::CliResult,
5 toolchain,
6 },
7 dialoguer::{theme::ColorfulTheme, Input, Select},
8 serde::Serialize,
9 std::{fmt, fs, path::Path},
10};
11
12#[derive(Debug, Clone, Copy)]
17enum Toolchain {
18 Solana,
19 Upstream,
20}
21
22impl fmt::Display for Toolchain {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Toolchain::Solana => write!(f, "solana"),
26 Toolchain::Upstream => write!(f, "upstream"),
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy)]
32enum Framework {
33 None,
34 Mollusk,
35 QuasarSVMWeb3js,
36 QuasarSVMKit,
37 QuasarSVMRust,
38}
39
40impl Framework {
41 fn has_typescript(&self) -> bool {
42 matches!(self, Framework::QuasarSVMWeb3js | Framework::QuasarSVMKit)
43 }
44
45 fn is_kit(&self) -> bool {
46 matches!(self, Framework::QuasarSVMKit)
47 }
48
49 fn has_rust_tests(&self) -> bool {
50 matches!(self, Framework::Mollusk | Framework::QuasarSVMRust)
51 }
52}
53
54impl fmt::Display for Framework {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Framework::None => write!(f, "none"),
58 Framework::Mollusk => write!(f, "mollusk"),
59 Framework::QuasarSVMWeb3js => write!(f, "quasarsvm-web3js"),
60 Framework::QuasarSVMKit => write!(f, "quasarsvm-kit"),
61 Framework::QuasarSVMRust => write!(f, "quasarsvm-rust"),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy)]
67enum Template {
68 Minimal,
69 Full,
70}
71
72impl fmt::Display for Template {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Template::Minimal => write!(f, "minimal"),
76 Template::Full => write!(f, "full"),
77 }
78 }
79}
80
81#[derive(Serialize)]
86struct QuasarToml {
87 project: QuasarProject,
88 toolchain: QuasarToolchain,
89 testing: QuasarTesting,
90}
91
92#[derive(Serialize)]
93struct QuasarProject {
94 name: String,
95}
96
97#[derive(Serialize)]
98struct QuasarToolchain {
99 #[serde(rename = "type")]
100 toolchain_type: String,
101}
102
103#[derive(Serialize)]
104struct QuasarTesting {
105 framework: String,
106}
107
108fn print_banner() {
113 use std::io::{self, IsTerminal, Write};
114
115 let stdout = io::stdout();
116 if !stdout.is_terminal() {
117 println!("\n Quasar\n Build programs that execute at the speed of light\n");
118 return;
119 }
120
121 use std::{thread, time::Duration};
122
123 ctrlc::set_handler(move || {
125 print!("\x1b[?25h");
126 std::process::exit(130);
127 })
128 .ok();
129
130 let mut out = stdout.lock();
131 write!(out, "\x1b[?25l").ok();
132
133 let w: usize = 70;
134 let h: usize = 11; let n_frames: usize = 22;
136 let nebula_w: f32 = 30.0; #[rustfmt::skip]
140 let figlet: [&str; 7] = [
141 " ██████╗ ██╗ ██╗ █████╗ ███████╗ █████╗ ██████╗ ",
142 "██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗",
143 "██║ ██║██║ ██║███████║███████╗███████║██████╔╝",
144 "██║▄▄ ██║██║ ██║██╔══██║╚════██║██╔══██║██╔══██╗",
145 "╚██████╔╝╚██████╔╝██║ ██║███████║██║ ██║██║ ██║",
146 " ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝",
147 "",
148 ];
149 let fig: Vec<Vec<char>> = figlet.iter().map(|l| l.chars().collect()).collect();
150 let fig_w = fig.iter().map(|l| l.len()).max().unwrap_or(0);
151 let fig_off = w.saturating_sub(fig_w) / 2;
152
153 let tagline = "Build programs that execute at the speed of light";
154 let tag_chars: Vec<char> = tagline.chars().collect();
155 let tag_off = w.saturating_sub(tag_chars.len()) / 2;
156
157 let byline = "by blueshift.gg";
158 let by_chars: Vec<char> = byline.chars().collect();
159 let by_off = w.saturating_sub(by_chars.len()) / 2;
160
161 writeln!(out).ok();
163 for _ in 0..h {
164 writeln!(out).ok();
165 }
166 out.flush().ok();
167
168 for frame in 0..n_frames {
169 write!(out, "\x1b[{h}A").ok();
170 let is_final = frame == n_frames - 1;
171
172 let t = frame as f32 / (n_frames - 2).max(1) as f32;
174 let edge = -nebula_w + t * (w as f32 + nebula_w * 2.0);
175
176 #[allow(clippy::needless_range_loop)]
177 for li in 0..h {
178 write!(out, "\x1b[2K ").ok();
179
180 if is_final {
181 match li {
183 1..=7 => {
184 let row = &fig[li - 1];
185 for _ in 0..fig_off {
186 write!(out, " ").ok();
187 }
188 for &ch in row.iter() {
189 if ch != ' ' {
190 write!(out, "\x1b[36m{ch}\x1b[0m").ok();
191 } else {
192 write!(out, " ").ok();
193 }
194 }
195 }
196 9 => {
197 for _ in 0..tag_off {
198 write!(out, " ").ok();
199 }
200 write!(out, "\x1b[1m{tagline}\x1b[0m").ok();
201 }
202 10 => {
203 for _ in 0..by_off {
204 write!(out, " ").ok();
205 }
206 write!(out, "\x1b[90mby \x1b[36mblueshift.gg\x1b[0m").ok();
207 }
208 _ => {}
209 }
210 } else {
211 for ci in 0..w {
213 let dist = ci as f32 - edge;
214
215 let text_ch = match li {
217 1..=7 if ci >= fig_off && ci - fig_off < fig_w => {
218 fig[li - 1].get(ci - fig_off).copied().unwrap_or(' ')
219 }
220 9 if ci >= tag_off && ci - tag_off < tag_chars.len() => {
221 tag_chars[ci - tag_off]
222 }
223 10 if ci >= by_off && ci - by_off < by_chars.len() => by_chars[ci - by_off],
224 _ => ' ',
225 };
226
227 if dist < -nebula_w {
228 write_text_char(&mut out, text_ch, li, ci, by_off);
230 } else if dist < nebula_w {
231 let blend = (dist + nebula_w) / (nebula_w * 2.0);
233 let intensity = 1.0 - (dist.abs() / nebula_w);
234 let d = aurora_density(ci, li, frame) * intensity;
235
236 if blend < 0.3 && text_ch != ' ' {
237 write_text_char(&mut out, text_ch, li, ci, by_off);
239 } else {
240 write_nebula_char(&mut out, d);
241 }
242 } else {
243 write!(out, " ").ok();
245 }
246 }
247 }
248 writeln!(out).ok();
249 }
250 out.flush().ok();
251
252 if !is_final {
253 thread::sleep(Duration::from_millis(55));
254 }
255 }
256
257 write!(out, "\x1b[?25h").ok();
258 writeln!(out).ok();
259 out.flush().ok();
260}
261
262fn write_text_char(
263 out: &mut impl std::io::Write,
264 ch: char,
265 line: usize,
266 col: usize,
267 by_off: usize,
268) {
269 if ch == ' ' {
270 write!(out, " ").ok();
271 } else {
272 match line {
273 1..=7 => {
274 write!(out, "\x1b[36m{ch}\x1b[0m").ok();
275 }
276 9 => {
277 write!(out, "\x1b[1m{ch}\x1b[0m").ok();
278 }
279 10 => {
280 if col - by_off < 3 {
281 write!(out, "\x1b[90m{ch}\x1b[0m").ok();
282 } else {
283 write!(out, "\x1b[36m{ch}\x1b[0m").ok();
284 }
285 }
286 _ => {
287 write!(out, " ").ok();
288 }
289 };
290 }
291}
292
293fn write_nebula_char(out: &mut impl std::io::Write, d: f32) {
294 if d < 0.10 {
295 write!(out, " ").ok();
296 } else if d < 0.25 {
297 write!(out, "\x1b[38;2;15;25;85m░\x1b[0m").ok();
298 } else if d < 0.42 {
299 write!(out, "\x1b[38;2;30;55;145m░\x1b[0m").ok();
300 } else if d < 0.60 {
301 write!(out, "\x1b[38;2;50;95;200m▒\x1b[0m").ok();
302 } else if d < 0.78 {
303 write!(out, "\x1b[38;2;75;140;235m▓\x1b[0m").ok();
304 } else {
305 write!(out, "\x1b[38;2;100;170;255m█\x1b[0m").ok();
306 }
307}
308
309fn aurora_density(col: usize, line: usize, frame: usize) -> f32 {
311 let c = col as f32;
312 let l = line as f32;
313 let f = frame as f32;
314
315 let w1 = ((c - f * 5.0) / 8.0 + l * 0.35).sin();
316 let w2 = ((c - f * 3.5) / 5.5 - l * 0.25).sin() * 0.45;
317 let w3 = ((c - f * 7.0) / 12.0 + l * 0.15).sin() * 0.3;
318
319 ((w1 + w2 + w3 + 1.5) / 3.5).clamp(0.0, 1.0)
320}
321
322fn color(code: u8, s: &str) -> String {
327 crate::style::color(code, s)
328}
329
330fn bold(s: &str) -> String {
331 crate::style::bold(s)
332}
333
334fn dim(s: &str) -> String {
335 crate::style::dim(s)
336}
337
338pub fn run(
343 name: Option<String>,
344 yes: bool,
345 no_git: bool,
346 framework_override: Option<String>,
347 template_override: Option<String>,
348 toolchain_override: Option<String>,
349) -> CliResult {
350 let globals = GlobalConfig::load();
351
352 let skip_prompts = yes
355 || name.is_some()
356 || framework_override.is_some()
357 || template_override.is_some()
358 || toolchain_override.is_some();
359
360 if let Some(ref f) = framework_override {
362 if !matches!(
363 f.as_str(),
364 "none" | "mollusk" | "quasarsvm-rust" | "quasarsvm-web3js" | "quasarsvm-kit"
365 ) {
366 eprintln!(
367 " {}",
368 crate::style::fail(&format!("unknown framework: {f}"))
369 );
370 eprintln!(
371 " {}",
372 dim("valid: none, mollusk, quasarsvm-rust, quasarsvm-web3js, quasarsvm-kit")
373 );
374 std::process::exit(1);
375 }
376 }
377 if let Some(ref t) = template_override {
378 if !matches!(t.as_str(), "minimal" | "full") {
379 eprintln!(
380 " {}",
381 crate::style::fail(&format!("unknown template: {t}"))
382 );
383 eprintln!(" {}", dim("valid: minimal, full"));
384 std::process::exit(1);
385 }
386 }
387 if let Some(ref t) = toolchain_override {
388 if !matches!(t.as_str(), "solana" | "upstream") {
389 eprintln!(
390 " {}",
391 crate::style::fail(&format!("unknown toolchain: {t}"))
392 );
393 eprintln!(" {}", dim("valid: solana, upstream"));
394 std::process::exit(1);
395 }
396 }
397
398 if globals.ui.animation && !skip_prompts {
399 print_banner();
400 }
401
402 let theme = ColorfulTheme::default();
403
404 let name: String = if skip_prompts {
406 name.unwrap_or_else(|| {
407 eprintln!(
408 " {}",
409 crate::style::fail("a project name is required when using flags")
410 );
411 eprintln!(
412 " {}",
413 crate::style::dim("usage: quasar init <name> [--framework ...] [--template ...]")
414 );
415 std::process::exit(1);
416 })
417 } else {
418 let mut prompt = Input::with_theme(&theme).with_prompt("Project name");
419 if let Some(default) = name {
420 prompt = prompt.default(default);
421 }
422 prompt.interact_text().map_err(anyhow::Error::from)?
423 };
424
425 let crate_name = if name == "." {
427 std::env::current_dir()
428 .ok()
429 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
430 .unwrap_or_else(|| "my-program".to_string())
431 } else {
432 name.clone()
433 };
434
435 let toolchain_default = match toolchain_override
437 .as_deref()
438 .or(globals.defaults.toolchain.as_deref())
439 {
440 Some("upstream") => 1,
441 _ => 0,
442 };
443 let toolchain_idx = if skip_prompts {
444 toolchain_default
445 } else {
446 let toolchain_items = &[
447 "solana (cargo build-sbf)",
448 "upstream (cargo +nightly build-bpf)",
449 ];
450 Select::with_theme(&theme)
451 .with_prompt("Toolchain")
452 .items(toolchain_items)
453 .default(toolchain_default)
454 .interact()
455 .map_err(anyhow::Error::from)?
456 };
457 let toolchain = match toolchain_idx {
458 0 => Toolchain::Solana,
459 _ => Toolchain::Upstream,
460 };
461
462 if matches!(toolchain, Toolchain::Upstream) && !toolchain::has_sbpf_linker() {
464 eprintln!();
465 eprintln!(" {} sbpf-linker not found.", color(196, "\u{2718}"));
466 eprintln!();
467 eprintln!(" Install platform-tools first:");
468 eprintln!(
469 " {}",
470 bold("git clone https://github.com/anza-xyz/platform-tools")
471 );
472 eprintln!(" {}", bold("cd platform-tools"));
473 eprintln!(" {}", bold("cargo install-with-gallery"));
474 eprintln!();
475 std::process::exit(1);
476 }
477
478 let framework_default = match framework_override
480 .as_deref()
481 .or(globals.defaults.framework.as_deref())
482 {
483 Some("quasarsvm-rust") => 1,
484 Some("quasarsvm-web3js") => 2,
485 Some("quasarsvm-kit") => 3,
486 Some("mollusk") => 4,
487 Some("none") => 0,
488 _ => 1,
489 };
490 let framework_idx = if skip_prompts {
491 framework_default
492 } else {
493 let framework_items = &["None", "Rust", "TypeScript", "Kit", "Rust (Mollusk)"];
494 Select::with_theme(&theme)
495 .with_prompt("Testing framework")
496 .items(framework_items)
497 .default(framework_default)
498 .interact()
499 .map_err(anyhow::Error::from)?
500 };
501 let framework = match framework_idx {
502 0 => Framework::None,
503 1 => Framework::QuasarSVMRust,
504 2 => Framework::QuasarSVMWeb3js,
505 3 => Framework::QuasarSVMKit,
506 _ => Framework::Mollusk,
507 };
508
509 let template_default = match template_override
511 .as_deref()
512 .or(globals.defaults.template.as_deref())
513 {
514 Some("full") => 1,
515 _ => 0,
516 };
517 let template_idx = if skip_prompts {
518 template_default
519 } else {
520 let template_items = &[
521 "Minimal (instruction file only)",
522 "Full (state, errors, and instruction files)",
523 ];
524 Select::with_theme(&theme)
525 .with_prompt("Template")
526 .items(template_items)
527 .default(template_default)
528 .interact()
529 .map_err(anyhow::Error::from)?
530 };
531 let template = match template_idx {
532 0 => Template::Minimal,
533 _ => Template::Full,
534 };
535
536 if skip_prompts {
537 println!();
538 println!(
539 " {} {} {} {} {}",
540 dim("Using:"),
541 bold(&toolchain.to_string()),
542 dim("+"),
543 bold(&framework.to_string()),
544 bold(&template.to_string()),
545 );
546 }
547
548 scaffold(&name, &crate_name, toolchain, framework, template)?;
549
550 if !no_git {
552 let root = Path::new(&name);
553 let already_git = if name == "." {
554 Path::new(".git").exists()
555 } else {
556 root.join(".git").exists()
557 };
558 if !already_git {
559 let _ = std::process::Command::new("git")
560 .args(["init", "--quiet"])
561 .current_dir(root)
562 .status();
563 }
564 }
565
566 let new_globals = GlobalConfig {
568 defaults: GlobalDefaults {
569 toolchain: Some(toolchain.to_string()),
570 framework: Some(framework.to_string()),
571 template: Some(template.to_string()),
572 },
573 ui: UiConfig {
574 animation: false,
575 ..globals.ui
576 },
577 };
578 let _ = new_globals.save(); println!();
582 println!(
583 " {} Created {} {}",
584 color(83, "\u{2714}"),
585 bold(&crate_name),
586 dim("project")
587 );
588 println!();
589 println!(" {}", dim("Next steps:"));
590 if name != "." {
591 println!(
592 " {} {}",
593 color(45, "\u{276f}"),
594 bold(&format!("cd {name}"))
595 );
596 }
597 println!(" {} {}", color(45, "\u{276f}"), bold("quasar build"));
598 if framework.has_rust_tests() || framework.has_typescript() {
599 println!(" {} {}", color(45, "\u{276f}"), bold("quasar test"));
600 }
601 println!();
602 println!(
603 " {} saved to {}",
604 dim("Preferences"),
605 dim(&GlobalConfig::path().display().to_string()),
606 );
607 println!();
608
609 Ok(())
610}
611
612fn scaffold(
613 dir: &str,
614 name: &str,
615 toolchain: Toolchain,
616 framework: Framework,
617 template: Template,
618) -> CliResult {
619 let root = Path::new(dir);
620
621 if dir == "." {
622 if root.join("Cargo.toml").exists() || root.join("Quasar.toml").exists() {
624 eprintln!(
625 " {}",
626 crate::style::fail("current directory already contains a project")
627 );
628 std::process::exit(1);
629 }
630 } else if root.exists() {
631 eprintln!(
632 " {}",
633 crate::style::fail(&format!("directory '{dir}' already exists"))
634 );
635 std::process::exit(1);
636 }
637
638 let src = root.join("src");
639 fs::create_dir_all(&src).map_err(anyhow::Error::from)?;
640
641 let config = QuasarToml {
643 project: QuasarProject {
644 name: name.to_string(),
645 },
646 toolchain: QuasarToolchain {
647 toolchain_type: toolchain.to_string(),
648 },
649 testing: QuasarTesting {
650 framework: framework.to_string(),
651 },
652 };
653 let toml_str = toml::to_string_pretty(&config).map_err(anyhow::Error::from)?;
654 fs::write(root.join("Quasar.toml"), toml_str).map_err(anyhow::Error::from)?;
655
656 fs::write(
658 root.join("Cargo.toml"),
659 generate_cargo_toml(name, toolchain, framework),
660 )
661 .map_err(anyhow::Error::from)?;
662
663 if matches!(toolchain, Toolchain::Upstream) {
665 let cargo_dir = root.join(".cargo");
666 fs::create_dir_all(&cargo_dir).map_err(anyhow::Error::from)?;
667 fs::write(cargo_dir.join("config.toml"), CARGO_CONFIG).map_err(anyhow::Error::from)?;
668 }
669
670 fs::write(root.join(".gitignore"), GITIGNORE).map_err(anyhow::Error::from)?;
672
673 let deploy_dir = root.join("target").join("deploy");
675 fs::create_dir_all(&deploy_dir).map_err(anyhow::Error::from)?;
676
677 let signing_key = ed25519_dalek::SigningKey::generate(&mut rand::thread_rng());
678 let program_id = bs58::encode(signing_key.verifying_key().as_bytes()).into_string();
679
680 let mut keypair_bytes = Vec::with_capacity(64);
682 keypair_bytes.extend_from_slice(signing_key.as_bytes());
683 keypair_bytes.extend_from_slice(signing_key.verifying_key().as_bytes());
684 let keypair_json = serde_json::to_string(&keypair_bytes).map_err(anyhow::Error::from)?;
685 fs::write(
686 deploy_dir.join(format!("{name}-keypair.json")),
687 &keypair_json,
688 )
689 .map_err(anyhow::Error::from)?;
690
691 let module_name = name.replace('-', "_");
693 let has_rust_tests = framework.has_rust_tests();
694 fs::write(
695 src.join("lib.rs"),
696 generate_lib_rs(&module_name, &program_id, template, has_rust_tests),
697 )
698 .map_err(anyhow::Error::from)?;
699
700 match template {
702 Template::Minimal => {
703 }
705 Template::Full => {
706 let instructions_dir = src.join("instructions");
707 fs::create_dir_all(&instructions_dir).map_err(anyhow::Error::from)?;
708 fs::write(instructions_dir.join("mod.rs"), INSTRUCTIONS_MOD)
709 .map_err(anyhow::Error::from)?;
710 fs::write(
711 instructions_dir.join("initialize.rs"),
712 INSTRUCTION_INITIALIZE,
713 )
714 .map_err(anyhow::Error::from)?;
715 fs::write(src.join("state.rs"), STATE_RS).map_err(anyhow::Error::from)?;
716 fs::write(src.join("errors.rs"), ERRORS_RS).map_err(anyhow::Error::from)?;
717 }
718 }
719
720 if framework.has_rust_tests() {
722 fs::write(
723 src.join("tests.rs"),
724 generate_tests_rs(&module_name, framework, template, toolchain),
725 )
726 .map_err(anyhow::Error::from)?;
727 }
728
729 if framework.has_typescript() {
731 let tests_dir = root.join("tests");
732 fs::create_dir_all(&tests_dir).map_err(anyhow::Error::from)?;
733
734 fs::write(
736 root.join("package.json"),
737 generate_package_json(name, framework),
738 )
739 .map_err(anyhow::Error::from)?;
740 fs::write(root.join("tsconfig.json"), TS_TEST_TSCONFIG).map_err(anyhow::Error::from)?;
741
742 fs::write(
744 tests_dir.join(format!("{}.test.ts", name)),
745 generate_test_ts(name, framework, toolchain),
746 )
747 .map_err(anyhow::Error::from)?;
748 }
749
750 Ok(())
751}
752
753fn generate_cargo_toml(name: &str, toolchain: Toolchain, framework: Framework) -> String {
758 let mut out = format!(
759 r#"[package]
760name = "{name}"
761version = "0.1.0"
762edition = "2021"
763
764[lints.rust.unexpected_cfgs]
765level = "warn"
766check-cfg = [
767 'cfg(target_os, values("solana"))',
768]
769
770[lib]
771crate-type = ["cdylib"]
772
773[features]
774alloc = []
775client = []
776debug = []
777
778[dependencies]
779quasar-lang = {{ git = "https://github.com/blueshift-gg/quasar" }}
780"#,
781 );
782
783 if matches!(toolchain, Toolchain::Solana) {
784 out.push_str("solana-instruction = { version = \"3.2.0\" }\n");
785 }
786
787 let client_dep = format!("{name}-client = {{ path = \"target/client/rust/{name}-client\" }}\n");
789
790 match framework {
791 Framework::None => {}
792 Framework::Mollusk => {
793 out.push_str(&format!(
794 r#"
795[dev-dependencies]
796{client_dep}mollusk-svm = "0.10.3"
797solana-account = {{ version = "3.4.0" }}
798solana-address = {{ version = "2.2.0", features = ["decode"] }}
799solana-instruction = {{ version = "3.2.0", features = ["bincode"] }}
800"#,
801 ));
802 }
803 Framework::QuasarSVMRust => {
804 out.push_str(&format!(
805 r#"
806[dev-dependencies]
807{client_dep}quasar-svm = {{ git = "https://github.com/blueshift-gg/quasar-svm" }}
808solana-account = {{ version = "3.4.0" }}
809solana-address = {{ version = "2.2.0", features = ["decode"] }}
810solana-instruction = {{ version = "3.2.0", features = ["bincode"] }}
811solana-pubkey = {{ version = "4.1.0" }}
812"#,
813 ));
814 }
815 Framework::QuasarSVMWeb3js | Framework::QuasarSVMKit => {
816 out.push_str(&format!(
817 r#"
818[dev-dependencies]
819{client_dep}solana-account = {{ version = "3.4.0" }}
820solana-address = {{ version = "2.2.0", features = ["decode"] }}
821solana-instruction = {{ version = "3.2.0", features = ["bincode"] }}
822"#,
823 ));
824 }
825 }
826
827 out
828}
829
830fn generate_lib_rs(
831 module_name: &str,
832 program_id: &str,
833 template: Template,
834 has_tests: bool,
835) -> String {
836 let test_mod = if has_tests {
837 "\n#[cfg(test)]\nmod tests;\n"
838 } else {
839 ""
840 };
841
842 match template {
843 Template::Minimal => {
844 format!(
845 r#"#![cfg_attr(not(test), no_std)]
846
847use quasar_lang::prelude::*;
848
849declare_id!("{program_id}");
850
851#[derive(Accounts)]
852pub struct Initialize<'info> {{
853 pub payer: &'info mut Signer,
854 pub system_program: &'info Program<System>,
855}}
856
857impl<'info> Initialize<'info> {{
858 #[inline(always)]
859 pub fn initialize(&self) -> Result<(), ProgramError> {{
860 Ok(())
861 }}
862}}
863
864#[program]
865mod {module_name} {{
866 use super::*;
867
868 #[instruction(discriminator = 0)]
869 pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> {{
870 ctx.accounts.initialize()
871 }}
872}}
873{test_mod}"#
874 )
875 }
876 Template::Full => {
877 format!(
878 r#"#![cfg_attr(not(test), no_std)]
879
880use quasar_lang::prelude::*;
881
882mod errors;
883mod instructions;
884mod state;
885use instructions::*;
886
887declare_id!("{program_id}");
888
889#[program]
890mod {module_name} {{
891 use super::*;
892
893 #[instruction(discriminator = 0)]
894 pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> {{
895 ctx.accounts.initialize()
896 }}
897}}
898{test_mod}"#
899 )
900 }
901 }
902}
903
904fn generate_package_json(name: &str, framework: Framework) -> String {
905 let solana_dep = if framework.is_kit() {
906 "\"@solana/kit\": \"^6.0.0\""
907 } else {
908 "\"@solana/web3.js\": \"github:blueshift-gg/web3.js#v2\""
909 };
910 format!(
911 r#"{{
912 "name": "{name}",
913 "version": "0.1.0",
914 "private": true,
915 "type": "commonjs",
916 "scripts": {{
917 "test": "mocha --require tsx --delay tests/*.test.ts"
918 }},
919 "dependencies": {{
920 "@blueshift-gg/quasar-svm": "^0.1",
921 {solana_dep}
922 }},
923 "devDependencies": {{
924 "@types/chai": "^5.2.0",
925 "@types/mocha": "^10.0.0",
926 "@types/node": "^22.0.0",
927 "chai": "^6.2.2",
928 "mocha": "^11.7.5",
929 "tsx": "^4.21.0",
930 "typescript": "^5.9.3"
931 }}
932}}
933"#
934 )
935}
936
937fn generate_test_ts(name: &str, framework: Framework, toolchain: Toolchain) -> String {
938 let module_name = name.replace('-', "_");
939 let class_name = crate::utils::snake_to_pascal(&module_name);
940 let so_name = match toolchain {
941 Toolchain::Upstream => format!("lib{module_name}"),
942 Toolchain::Solana => module_name.clone(),
943 };
944
945 if framework.is_kit() {
946 format!(
947 r#"import {{ generateKeyPairSigner }} from "@solana/kit";
948import {{ {class_name}Client, PROGRAM_ADDRESS }} from "../target/client/typescript/{module_name}/kit";
949import {{ describe, it, run }} from "mocha";
950import {{ QuasarSvm, createKeyedSystemAccount }} from "@blueshift-gg/quasar-svm/kit";
951import {{ readFile }} from "node:fs/promises";
952import {{ assert }} from "chai";
953
954const {class_name}Program = new {class_name}Client();
955
956describe("{class_name} Program", async () => {{
957
958 const vm = new QuasarSvm();
959 vm.addProgram(PROGRAM_ADDRESS, await readFile("target/deploy/{so_name}.so"));
960
961 const payer = await generateKeyPairSigner();
962
963 it("initializes", async () => {{
964 const initializeInstruction = {class_name}Program.createInitializeInstruction({{
965 payer: payer.address,
966 }});
967
968 const result = vm.processInstruction(initializeInstruction, [
969 createKeyedSystemAccount(payer.address),
970 ]);
971
972 assert.isTrue(result.status.ok, `initialize failed:\n${{result.logs.join("\n")}}`);
973 }});
974
975 run()
976}});
977"#
978 )
979 } else {
980 format!(
981 r#"import {{ Keypair }} from "@solana/web3.js";
982import {{ {class_name}Client }} from "../target/client/typescript/{module_name}/web3.js";
983import {{ readFile }} from "node:fs/promises";
984import {{ describe, it, run }} from "mocha";
985import {{ assert }} from "chai";
986import {{ QuasarSvm, createKeyedSystemAccount }} from "@blueshift-gg/quasar-svm/web3.js";
987
988const {class_name}Program = new {class_name}Client();
989
990describe("{class_name} Program", async () => {{
991 const vm = new QuasarSvm();
992 vm.addProgram({class_name}Client.programId, await readFile("target/deploy/{so_name}.so"));
993
994 const {{ publicKey: payer }} = await Keypair.generate();
995
996 it("initializes", async () => {{
997 const initializeInstruction = {class_name}Program.createInitializeInstruction({{
998 payer,
999 }});
1000
1001 const result = vm.processInstruction(initializeInstruction, [
1002 createKeyedSystemAccount(payer),
1003 ]);
1004
1005 assert.isTrue(result.status.ok, `initialize failed:\n${{result.logs.join("\n")}}`);
1006 }});
1007
1008 run();
1009}});
1010"#
1011 )
1012 }
1013}
1014
1015fn generate_tests_rs(
1016 module_name: &str,
1017 framework: Framework,
1018 template: Template,
1019 toolchain: Toolchain,
1020) -> String {
1021 let mut libname = module_name.to_string();
1022 if matches!(toolchain, Toolchain::Upstream) {
1023 libname = format!("lib{libname}");
1024 };
1025 let client_crate = format!("{module_name}_client");
1026
1027 match (framework, template) {
1028 (Framework::Mollusk, Template::Minimal | Template::Full) => {
1029 format!(
1030 r#"extern crate std;
1031
1032use mollusk_svm::{{program::keyed_account_for_system_program, Mollusk}};
1033use solana_account::Account;
1034use solana_address::Address;
1035use solana_instruction::Instruction;
1036
1037use {client_crate}::InitializeInstruction;
1038
1039fn setup() -> Mollusk {{
1040 Mollusk::new(&crate::ID, "target/deploy/{libname}")
1041}}
1042
1043#[test]
1044fn test_initialize() {{
1045 let mollusk = setup();
1046 let (system_program, system_program_account) = keyed_account_for_system_program();
1047
1048 let payer = Address::new_unique();
1049 let payer_account = Account::new(10_000_000_000, 0, &system_program);
1050
1051 let instruction: Instruction = InitializeInstruction {{
1052 payer,
1053 system_program,
1054 }}
1055 .into();
1056
1057 let result = mollusk.process_instruction(
1058 &instruction,
1059 &[
1060 (payer, payer_account),
1061 (system_program, system_program_account),
1062 ],
1063 );
1064
1065 assert!(
1066 result.program_result.is_ok(),
1067 "initialize failed: {{:?}}",
1068 result.program_result,
1069 );
1070}}
1071"#
1072 )
1073 }
1074 (Framework::QuasarSVMRust, Template::Minimal | Template::Full) => {
1075 format!(
1076 r#"extern crate std;
1077
1078use quasar_svm::{{Account, Instruction, Pubkey, QuasarSvm}};
1079use solana_address::Address;
1080
1081use {client_crate}::InitializeInstruction;
1082
1083fn setup() -> QuasarSvm {{
1084 let elf = include_bytes!("../target/deploy/{libname}.so");
1085 QuasarSvm::new()
1086 .with_program(&Pubkey::from(crate::ID), elf)
1087}}
1088
1089#[test]
1090fn test_initialize() {{
1091 let mut svm = setup();
1092
1093 let payer = Pubkey::new_unique();
1094
1095 let instruction: Instruction = InitializeInstruction {{
1096 payer: Address::from(payer.to_bytes()),
1097 system_program: Address::from(quasar_svm::system_program::ID.to_bytes()),
1098 }}
1099 .into();
1100
1101 let result = svm.process_instruction(
1102 &instruction,
1103 &[Account {{
1104 address: payer,
1105 lamports: 10_000_000_000,
1106 data: vec![],
1107 owner: quasar_svm::system_program::ID,
1108 executable: false,
1109 }}],
1110 );
1111
1112 result.assert_success();
1113}}
1114"#
1115 )
1116 }
1117 _ => r#"extern crate std;
1118
1119#[test]
1120fn test_initialize() {
1121 // TODO: implement test
1122}
1123"#
1124 .to_string(),
1125 }
1126}
1127
1128const GITIGNORE: &str = "\
1133# Build artifacts
1134/target
1135
1136# Lock files
1137Cargo.lock
1138package-lock.json
1139yarn.lock
1140
1141# Dependencies
1142node_modules
1143
1144# Environment
1145.env
1146.env.*
1147
1148# OS
1149.DS_Store
1150";
1151
1152const CARGO_CONFIG: &str = r#"[unstable]
1153build-std = ["core", "alloc"]
1154
1155[target.bpfel-unknown-none]
1156rustflags = [
1157"--cfg", "target_os=\"solana\"",
1158"--cfg", "feature=\"mem_unaligned\"",
1159"-C", "linker=sbpf-linker",
1160"-C", "panic=abort",
1161"-C", "relocation-model=static",
1162"-C", "link-arg=--disable-memory-builtins",
1163"-C", "link-arg=--llvm-args=--bpf-stack-size=4096",
1164"-C", "link-arg=--disable-expand-memcpy-in-order",
1165"-C", "link-arg=--export=entrypoint",
1166"-C", "target-cpu=v2",
1167]
1168[alias]
1169build-bpf = "build --release --target bpfel-unknown-none"
1170"#;
1171
1172const INSTRUCTIONS_MOD: &str = r#"mod initialize;
1173pub use initialize::*;
1174"#;
1175
1176const INSTRUCTION_INITIALIZE: &str = r#"use quasar_lang::prelude::*;
1177
1178#[derive(Accounts)]
1179pub struct Initialize<'info> {
1180 pub payer: &'info mut Signer,
1181 pub system_program: &'info Program<System>,
1182}
1183
1184impl<'info> Initialize<'info> {
1185 #[inline(always)]
1186 pub fn initialize(&self) -> Result<(), ProgramError> {
1187 Ok(())
1188 }
1189}
1190"#;
1191
1192const STATE_RS: &str = r#"use quasar_lang::prelude::*;
1193
1194#[account(discriminator = 1)]
1195pub struct MyAccount {
1196 pub authority: Address,
1197 pub value: u64,
1198}
1199"#;
1200
1201const ERRORS_RS: &str = r#"use quasar_lang::prelude::*;
1202
1203#[error_code]
1204pub enum MyError {
1205 Unauthorized,
1206}
1207"#;
1208
1209const TS_TEST_TSCONFIG: &str = r#"{
1210 "compilerOptions": {
1211 "target": "es2020",
1212 "module": "commonjs",
1213 "strict": true,
1214 "esModuleInterop": true,
1215 "skipLibCheck": true,
1216 "resolveJsonModule": true,
1217 "types": ["node", "mocha"]
1218 },
1219 "include": ["tests/*.test.ts"]
1220}
1221"#;