1use crate::error::CodegenError;
6use crate::ir::*;
7use crate::GeneratedProject;
8
9#[derive(Default)]
11pub struct RustGenerator {
12 events: Vec<Event>,
14 signer_params: std::collections::HashSet<String>,
16 internal_functions: std::collections::HashSet<String>,
18 in_helper_function: bool,
20}
21
22impl RustGenerator {
23 pub fn new() -> Self {
24 Self {
25 events: Vec::new(),
26 signer_params: std::collections::HashSet::new(),
27 internal_functions: std::collections::HashSet::new(),
28 in_helper_function: false,
29 }
30 }
31
32 pub fn generate(
34 &mut self,
35 programs: &[SolanaProgram],
36 ) -> Result<GeneratedProject, CodegenError> {
37 if programs.is_empty() {
38 return Err(CodegenError::MissingElement(
39 "No deployable contracts found (abstract contracts cannot be deployed)".to_string(),
40 ));
41 }
42
43 let program = programs.last().unwrap();
46
47 self.events = program.events.clone();
49
50 self.internal_functions.clear();
52 for instruction in &program.instructions {
53 if !instruction.is_public {
54 self.internal_functions
55 .insert(to_snake_case(&instruction.name));
56 }
57 }
58
59 let lib_rs = self.generate_lib_rs(program)?;
60 let state_rs = self.generate_state_rs(program)?;
61 let instructions_rs = self.generate_instructions_rs(program)?;
62 let error_rs = self.generate_error_rs(program)?;
63 let events_rs = self.generate_events_rs(program)?;
64 let anchor_toml = self.generate_anchor_toml(program);
65 let cargo_toml = self.generate_cargo_toml(program);
66
67 let mut ts_gen = crate::ts_gen::TypeScriptGenerator::new();
69 let client_ts = ts_gen.generate(program)?;
70
71 let mut test_gen = crate::test_gen::TestGenerator::new();
73 let tests_ts = test_gen.generate(program)?;
74
75 let mut idl_gen = crate::idl_gen::IdlGenerator::new();
77 let idl_json = idl_gen.generate(program)?;
78
79 let package_json = self.generate_package_json(program);
81
82 let readme = self.generate_readme(program);
84 let gitignore = self.generate_gitignore();
85
86 let rust_tests = self.generate_rust_tests(program)?;
88 let has_tests = !program.tests.is_empty();
89
90 Ok(GeneratedProject {
91 lib_rs,
92 state_rs,
93 instructions_rs,
94 error_rs,
95 events_rs,
96 anchor_toml,
97 cargo_toml,
98 client_ts,
99 tests_ts,
100 idl_json,
101 package_json,
102 readme,
103 gitignore,
104 rust_tests,
105 has_tests,
106 })
107 }
108
109 fn generate_rust_tests(&self, program: &SolanaProgram) -> Result<String, CodegenError> {
111 if program.tests.is_empty() {
112 return Ok(String::new());
113 }
114
115 let mut output = String::new();
116 output.push_str("//! Generated tests from SolScript #[test] functions\n\n");
117 output.push_str("#[cfg(test)]\n");
118 output.push_str("mod solscript_tests {\n");
119 output.push_str(" use super::*;\n\n");
120
121 for test in &program.tests {
122 let test_name = to_snake_case(&test.name);
123
124 if let Some(expected_msg) = &test.should_fail {
126 if expected_msg.is_empty() {
127 output.push_str(&format!(
128 " #[test]\n #[should_panic]\n fn {}() {{\n",
129 test_name
130 ));
131 } else {
132 output.push_str(&format!(
133 " #[test]\n #[should_panic(expected = \"{}\")]\n fn {}() {{\n",
134 expected_msg, test_name
135 ));
136 }
137 } else {
138 output.push_str(&format!(" #[test]\n fn {}() {{\n", test_name));
139 }
140
141 for stmt in &test.body {
143 let stmt_code = self.generate_statement(stmt, 2)?;
144 output.push_str(&stmt_code);
145 }
146
147 output.push_str(" }\n\n");
148 }
149
150 output.push_str("}\n");
151 Ok(output)
152 }
153
154 fn generate_lib_rs(&mut self, program: &SolanaProgram) -> Result<String, CodegenError> {
155 let name = to_snake_case(&program.name);
156 let uses_token = program.instructions.iter().any(|i| i.uses_token_program);
157
158 let mut imports = String::from("use anchor_lang::prelude::*;\n");
159 if uses_token {
160 imports.push_str("use anchor_spl::token::CpiContext;\n");
161 }
162
163 let helper_fns = self.generate_helper_functions(program)?;
165
166 Ok(format!(
167 r#"//! Generated by SolScript compiler
168//! Contract: {}
169
170{}
171mod state;
172mod instructions;
173mod error;
174mod events;
175
176pub use state::*;
177pub use instructions::*;
178pub use error::*;
179// Events are accessed via events:: prefix to avoid name collisions
180
181declare_id!("11111111111111111111111111111111");
182
183{}
184
185#[program]
186pub mod {} {{
187 use super::*;
188
189{}
190}}
191"#,
192 program.name,
193 imports,
194 helper_fns,
195 name,
196 self.generate_instruction_handlers(program)?
197 ))
198 }
199
200 fn generate_helper_functions(
201 &mut self,
202 program: &SolanaProgram,
203 ) -> Result<String, CodegenError> {
204 let mut helpers = String::new();
205
206 for instruction in &program.instructions {
207 if !instruction.is_public {
208 helpers.push_str(&self.generate_helper_function(instruction, program)?);
209 helpers.push('\n');
210 }
211 }
212
213 Ok(helpers)
214 }
215
216 fn generate_helper_function(
217 &mut self,
218 instruction: &Instruction,
219 program: &SolanaProgram,
220 ) -> Result<String, CodegenError> {
221 let name = to_snake_case(&instruction.name);
222
223 let params: Vec<String> = instruction
225 .params
226 .iter()
227 .map(|p| format!("{}: {}", to_snake_case(&p.name), self.type_to_rust(&p.ty)))
228 .collect();
229
230 let state_type = format!("&mut crate::state::{}State", to_pascal_case(&program.name));
232 let mut all_params = vec![format!("state: {}", state_type)];
233 all_params.extend(params);
234 let params_str = all_params.join(", ");
235
236 let return_type = match &instruction.returns {
238 Some(ty) => format!("Result<{}>", self.type_to_rust(ty)),
239 None => "Result<()>".to_string(),
240 };
241
242 self.in_helper_function = true;
244 let body = self.generate_helper_body(instruction, program)?;
245 self.in_helper_function = false;
246
247 Ok(format!(
248 r#"/// Internal helper function: {}
249fn {}({}) -> {} {{
250{}
251}}
252"#,
253 instruction.name, name, params_str, return_type, body
254 ))
255 }
256
257 fn generate_helper_body(
258 &mut self,
259 instruction: &Instruction,
260 _program: &SolanaProgram,
261 ) -> Result<String, CodegenError> {
262 let mut body = String::new();
263
264 for stmt in &instruction.body {
266 body.push_str(&self.generate_statement(stmt, 1)?);
267 body.push('\n');
268 }
269
270 if instruction.returns.is_none() && !body.contains("Ok(())") {
272 body.push_str(" Ok(())\n");
273 }
274
275 Ok(body)
276 }
277
278 fn generate_instruction_handlers(
279 &mut self,
280 program: &SolanaProgram,
281 ) -> Result<String, CodegenError> {
282 let mut handlers = String::new();
283
284 for instruction in &program.instructions {
285 if instruction.is_public {
287 handlers.push_str(&self.generate_instruction_handler(instruction, program)?);
288 handlers.push('\n');
289 }
290 }
291
292 Ok(handlers)
293 }
294
295 fn generate_instruction_handler(
296 &mut self,
297 instruction: &Instruction,
298 program: &SolanaProgram,
299 ) -> Result<String, CodegenError> {
300 let name = to_snake_case(&instruction.name);
301 let ctx_type = to_pascal_case(&instruction.name);
302
303 let params: Vec<String> = instruction
305 .params
306 .iter()
307 .filter(|p| !matches!(p.ty, SolanaType::Signer))
308 .map(|p| format!("{}: {}", to_snake_case(&p.name), self.type_to_rust(&p.ty)))
309 .collect();
310
311 let params_str = if params.is_empty() {
312 String::new()
313 } else {
314 format!(", {}", params.join(", "))
315 };
316
317 let return_type = match &instruction.returns {
319 Some(ty) => format!("Result<{}>", self.type_to_rust(ty)),
320 None => "Result<()>".to_string(),
321 };
322
323 let body = self.generate_instruction_body(instruction, program)?;
325
326 Ok(format!(
327 " pub fn {}(ctx: Context<{}>{}) -> {} {{\n{}\n }}\n",
328 name, ctx_type, params_str, return_type, body
329 ))
330 }
331
332 fn generate_instruction_body(
333 &mut self,
334 instruction: &Instruction,
335 program: &SolanaProgram,
336 ) -> Result<String, CodegenError> {
337 self.signer_params.clear();
339 for param in &instruction.params {
340 if matches!(param.ty, SolanaType::Signer) {
341 self.signer_params.insert(to_snake_case(¶m.name));
342 }
343 }
344
345 let mut body = String::new();
346
347 if instruction.modifiers.is_empty() {
349 for stmt in &instruction.body {
350 body.push_str(&self.generate_statement(stmt, 2)?);
351 }
352 } else {
353 for modifier_call in &instruction.modifiers {
356 if let Some(modifier_def) = program
358 .modifiers
359 .iter()
360 .find(|m| m.name == modifier_call.name)
361 {
362 for stmt in &modifier_def.body {
364 self.generate_inlined_statement(stmt, &instruction.body, 2, &mut body)?;
365 }
366 } else {
367 body.push_str(&format!(
369 " // Modifier: {} (definition not found)\n",
370 modifier_call.name
371 ));
372 for stmt in &instruction.body {
373 body.push_str(&self.generate_statement(stmt, 2)?);
374 }
375 }
376 }
377 }
378
379 if instruction.returns.is_none() && !body.contains("Ok(") {
381 body.push_str(" Ok(())\n");
382 }
383
384 Ok(body)
385 }
386
387 fn generate_inlined_statement(
389 &self,
390 stmt: &Statement,
391 inner_body: &[Statement],
392 indent: usize,
393 output: &mut String,
394 ) -> Result<(), CodegenError> {
395 match stmt {
396 Statement::Placeholder => {
397 for inner_stmt in inner_body {
399 output.push_str(&self.generate_statement(inner_stmt, indent)?);
400 }
401 }
402 Statement::If {
403 condition,
404 then_block,
405 else_block,
406 } => {
407 let ind = " ".repeat(indent);
409 output.push_str(&format!(
410 "{}if {} {{\n",
411 ind,
412 self.generate_expression(condition)?
413 ));
414 for s in then_block {
415 self.generate_inlined_statement(s, inner_body, indent + 1, output)?;
416 }
417 if let Some(else_stmts) = else_block {
418 output.push_str(&format!("{}}} else {{\n", ind));
419 for s in else_stmts {
420 self.generate_inlined_statement(s, inner_body, indent + 1, output)?;
421 }
422 }
423 output.push_str(&format!("{}}}\n", ind));
424 }
425 _ => {
426 output.push_str(&self.generate_statement(stmt, indent)?);
428 }
429 }
430 Ok(())
431 }
432
433 fn generate_statement(&self, stmt: &Statement, indent: usize) -> Result<String, CodegenError> {
434 let ind = " ".repeat(indent);
435
436 match stmt {
437 Statement::VarDecl { name, ty, value } => {
438 let name = to_snake_case(name);
439 let ty_str = self.type_to_rust(ty);
440 match value {
441 Some(expr) => Ok(format!(
442 "{}let {}: {} = {};\n",
443 ind,
444 name,
445 ty_str,
446 self.generate_expression(expr)?
447 )),
448 None => Ok(format!(
449 "{}let {}: {} = Default::default();\n",
450 ind, name, ty_str
451 )),
452 }
453 }
454 Statement::Assign { target, value } => Ok(format!(
455 "{}{} = {};\n",
456 ind,
457 self.generate_expression(target)?,
458 self.generate_expression(value)?
459 )),
460 Statement::If {
461 condition,
462 then_block,
463 else_block,
464 } => {
465 let mut result = format!("{}if {} {{\n", ind, self.generate_expression(condition)?);
466 for s in then_block {
467 result.push_str(&self.generate_statement(s, indent + 1)?);
468 }
469 result.push_str(&format!("{}}}", ind));
470
471 if let Some(else_stmts) = else_block {
472 result.push_str(" else {\n");
473 for s in else_stmts {
474 result.push_str(&self.generate_statement(s, indent + 1)?);
475 }
476 result.push_str(&format!("{}}}", ind));
477 }
478 result.push('\n');
479 Ok(result)
480 }
481 Statement::While { condition, body } => {
482 let mut result =
483 format!("{}while {} {{\n", ind, self.generate_expression(condition)?);
484 for s in body {
485 result.push_str(&self.generate_statement(s, indent + 1)?);
486 }
487 result.push_str(&format!("{}}}\n", ind));
488 Ok(result)
489 }
490 Statement::For {
491 init,
492 condition,
493 update,
494 body,
495 } => {
496 let mut result = String::new();
497
498 if let Some(init_stmt) = init {
500 result.push_str(&self.generate_statement(init_stmt, indent)?);
501 }
502
503 let cond = match condition {
504 Some(c) => self.generate_expression(c)?,
505 None => "true".to_string(),
506 };
507
508 result.push_str(&format!("{}while {} {{\n", ind, cond));
509
510 for s in body {
511 result.push_str(&self.generate_statement(s, indent + 1)?);
512 }
513
514 if let Some(upd) = update {
515 result.push_str(&format!("{} {};\n", ind, self.generate_expression(upd)?));
516 }
517
518 result.push_str(&format!("{}}}\n", ind));
519 Ok(result)
520 }
521 Statement::Return(expr) => match expr {
522 Some(e) => Ok(format!("{}Ok({})\n", ind, self.generate_expression(e)?)),
523 None => Ok(format!("{}Ok(())\n", ind)),
524 },
525 Statement::Emit { event, args } => {
526 let event_def = self.events.iter().find(|e| e.name == *event);
528 let args_str: Vec<String> = args
529 .iter()
530 .enumerate()
531 .map(|(i, a)| {
532 let val = self.generate_expression(a)?;
533 let field_name = event_def
534 .and_then(|e| e.fields.get(i))
535 .map(|f| to_snake_case(&f.name))
536 .unwrap_or_else(|| format!("field{}", i));
537 Ok(format!("{}: {}", field_name, val))
538 })
539 .collect::<Result<Vec<_>, _>>()?;
540 Ok(format!(
541 "{}emit!(events::{} {{ {} }});\n",
542 ind,
543 to_pascal_case(event),
544 args_str.join(", ")
545 ))
546 }
547 Statement::Require {
548 condition,
549 message: _,
550 } => Ok(format!(
551 "{}require!({}, CustomError::RequireFailed);\n",
552 ind,
553 self.generate_expression(condition)?
554 )),
555 Statement::RevertWithError {
556 error_name,
557 args: _,
558 } => {
559 Ok(format!(
563 "{}return Err(error!(CustomError::{}));\n",
564 ind,
565 to_pascal_case(error_name)
566 ))
567 }
568 Statement::Delete(target) => {
569 if let Expression::MappingAccess { account_name, .. } = target {
571 Ok(format!(
574 "{}// PDA {} closed via `close = signer` constraint\n",
575 ind,
576 to_snake_case(account_name)
577 ))
578 } else {
579 let target_expr = self.generate_expression(target)?;
581 Ok(format!("{}{} = Default::default();\n", ind, target_expr))
582 }
583 }
584 Statement::Selfdestruct { .. } => {
585 Ok(format!(
589 "{}// State account will be closed, rent sent to recipient\n",
590 ind
591 ))
592 }
593 Statement::Expr(expr) => Ok(format!("{}{};\n", ind, self.generate_expression(expr)?)),
594 Statement::Placeholder => {
595 Ok(String::new())
598 }
599 }
600 }
601
602 fn generate_expression(&self, expr: &Expression) -> Result<String, CodegenError> {
603 match expr {
604 Expression::Literal(lit) => self.generate_literal(lit),
605 Expression::Var(name) => {
606 let snake_name = to_snake_case(name);
607 if self.signer_params.contains(&snake_name) {
609 Ok(format!("ctx.accounts.{}.key()", snake_name))
610 } else {
611 Ok(snake_name)
612 }
613 }
614 Expression::StateAccess(field) => {
615 if self.in_helper_function {
616 Ok(format!("state.{}", to_snake_case(field)))
617 } else {
618 Ok(format!("ctx.accounts.state.{}", to_snake_case(field)))
619 }
620 }
621 Expression::MappingAccess {
622 mapping_name: _,
623 keys: _,
624 account_name,
625 } => {
626 Ok(format!(
628 "ctx.accounts.{}.value",
629 to_snake_case(account_name)
630 ))
631 }
632 Expression::MsgSender => Ok("ctx.accounts.signer.key()".to_string()),
633 Expression::MsgValue => Ok("0u64 /* msg.value not supported */".to_string()),
634 Expression::BlockTimestamp => Ok("Clock::get()?.unix_timestamp as u64".to_string()),
635 Expression::ClockSlot => Ok("Clock::get()?.slot".to_string()),
637 Expression::ClockEpoch => Ok("Clock::get()?.epoch".to_string()),
638 Expression::ClockUnixTimestamp => Ok("Clock::get()?.unix_timestamp".to_string()),
639 Expression::RentMinimumBalance { data_len } => {
641 let len_str = self.generate_expression(data_len)?;
642 Ok(format!(
643 "Rent::get()?.minimum_balance({} as usize)",
644 len_str
645 ))
646 }
647 Expression::RentIsExempt { lamports, data_len } => {
648 let lamports_str = self.generate_expression(lamports)?;
649 let len_str = self.generate_expression(data_len)?;
650 Ok(format!(
651 "Rent::get()?.is_exempt({}, {} as usize)",
652 lamports_str, len_str
653 ))
654 }
655 Expression::Binary { op, left, right } => {
656 let l = self.generate_expression(left)?;
657 let r = self.generate_expression(right)?;
658 let op_str = match op {
659 BinaryOp::Add => "+",
660 BinaryOp::Sub => "-",
661 BinaryOp::Mul => "*",
662 BinaryOp::Div => "/",
663 BinaryOp::Rem => "%",
664 BinaryOp::Eq => "==",
665 BinaryOp::Ne => "!=",
666 BinaryOp::Lt => "<",
667 BinaryOp::Le => "<=",
668 BinaryOp::Gt => ">",
669 BinaryOp::Ge => ">=",
670 BinaryOp::And => "&&",
671 BinaryOp::Or => "||",
672 BinaryOp::BitAnd => "&",
673 BinaryOp::BitOr => "|",
674 BinaryOp::BitXor => "^",
675 BinaryOp::Shl => "<<",
676 BinaryOp::Shr => ">>",
677 };
678 Ok(format!("({} {} {})", l, op_str, r))
679 }
680 Expression::Unary { op, expr } => {
681 let e = self.generate_expression(expr)?;
682 let op_str = match op {
683 UnaryOp::Neg => "-",
684 UnaryOp::Not => "!",
685 UnaryOp::BitNot => "!",
686 };
687 Ok(format!("({}{})", op_str, e))
688 }
689 Expression::Call { func, args } => {
690 let func_name = to_snake_case(func);
691 let args_str: Vec<String> = args
692 .iter()
693 .map(|a| self.generate_expression(a))
694 .collect::<Result<Vec<_>, _>>()?;
695
696 if self.internal_functions.contains(&func_name) {
698 let state_arg = if self.in_helper_function {
700 "state".to_string()
701 } else {
702 "&mut ctx.accounts.state".to_string()
703 };
704 let mut all_args = vec![state_arg];
705 all_args.extend(args_str);
706 Ok(format!("{}({})?", func_name, all_args.join(", ")))
707 } else {
708 Ok(format!("{}({})", func_name, args_str.join(", ")))
709 }
710 }
711 Expression::MethodCall {
712 receiver,
713 method,
714 args,
715 } => {
716 if method == "__assign__" && args.len() == 1 {
718 let target = self.generate_expression(receiver)?;
719 let value = self.generate_expression(&args[0])?;
720 return Ok(format!("{} = {}", target, value));
721 }
722
723 let recv = self.generate_expression(receiver)?;
724 let args_str: Vec<String> = args
725 .iter()
726 .map(|a| self.generate_expression(a))
727 .collect::<Result<Vec<_>, _>>()?;
728 Ok(format!(
729 "{}.{}({})",
730 recv,
731 to_snake_case(method),
732 args_str.join(", ")
733 ))
734 }
735 Expression::CpiCall {
736 program,
737 interface_name,
738 method,
739 args,
740 } => {
741 let prog = self.generate_expression(program)?;
742 let args_str: Vec<String> = args
743 .iter()
744 .map(|a| self.generate_expression(a))
745 .collect::<Result<Vec<_>, _>>()?;
746
747 let method_snake = to_snake_case(method);
750
751 let mut data_parts = Vec::new();
753 data_parts.push(format!(
754 "let discriminator = anchor_lang::solana_program::hash::hash(b\"global:{}\").to_bytes();",
755 method_snake
756 ));
757 data_parts.push("let mut data = discriminator[..8].to_vec();".to_string());
758
759 for arg in &args_str {
761 data_parts.push(format!(
762 "AnchorSerialize::serialize(&({}), &mut data).unwrap();",
763 arg
764 ));
765 }
766
767 let mut account_metas = Vec::new();
770 let mut account_infos = Vec::new();
771 for arg in args_str.iter() {
772 account_metas.push(format!("AccountMeta::new({}, false)", arg));
774 account_infos.push(format!("/* account_info for {} */", arg));
775 }
776
777 let accounts_vec = if account_metas.is_empty() {
778 "vec![]".to_string()
779 } else {
780 format!("vec![{}]", account_metas.join(", "))
781 };
782
783 Ok(format!(
784 r#"{{
785 use anchor_lang::prelude::*;
786 // CPI to {interface_name}.{method}
787 let cpi_program = {prog};
788
789 // Build instruction data with Anchor discriminator
790 {data_code}
791
792 // Build the instruction
793 let ix = anchor_lang::solana_program::instruction::Instruction {{
794 program_id: cpi_program,
795 accounts: {accounts},
796 data,
797 }};
798
799 // Execute CPI
800 // Note: You may need to add the appropriate account_infos based on your context
801 anchor_lang::solana_program::program::invoke(
802 &ix,
803 &[cpi_program.to_account_info()],
804 )?
805 }}"#,
806 interface_name = interface_name,
807 method = method,
808 prog = prog,
809 data_code = data_parts.join("\n "),
810 accounts = accounts_vec,
811 ))
812 }
813 Expression::InterfaceCast {
814 interface_name: _,
815 program_id,
816 } => {
817 self.generate_expression(program_id)
820 }
821 Expression::TokenTransfer {
822 from,
823 to,
824 authority,
825 amount,
826 } => {
827 let from_str = self.generate_expression(from)?;
828 let to_str = self.generate_expression(to)?;
829 let auth_str = self.generate_expression(authority)?;
830 let amt_str = self.generate_expression(amount)?;
831 Ok(format!(
834 r#"{{
835 let cpi_accounts = anchor_spl::token::Transfer {{
836 from: ctx.accounts.{}.to_account_info(),
837 to: ctx.accounts.{}.to_account_info(),
838 authority: ctx.accounts.{}.to_account_info(),
839 }};
840 let cpi_program = ctx.accounts.token_program.to_account_info();
841 anchor_spl::token::transfer(CpiContext::new(cpi_program, cpi_accounts), {} as u64)?
842 }}"#,
843 to_snake_case(&from_str),
844 to_snake_case(&to_str),
845 to_snake_case(&auth_str),
846 amt_str
847 ))
848 }
849 Expression::TokenMint {
850 mint,
851 to,
852 authority,
853 amount,
854 } => {
855 let mint_str = self.generate_expression(mint)?;
856 let to_str = self.generate_expression(to)?;
857 let auth_str = self.generate_expression(authority)?;
858 let amt_str = self.generate_expression(amount)?;
859 Ok(format!(
860 r#"{{
861 let cpi_accounts = anchor_spl::token::MintTo {{
862 mint: ctx.accounts.{}.to_account_info(),
863 to: ctx.accounts.{}.to_account_info(),
864 authority: ctx.accounts.{}.to_account_info(),
865 }};
866 let cpi_program = ctx.accounts.token_program.to_account_info();
867 anchor_spl::token::mint_to(CpiContext::new(cpi_program, cpi_accounts), {} as u64)?
868 }}"#,
869 to_snake_case(&mint_str),
870 to_snake_case(&to_str),
871 to_snake_case(&auth_str),
872 amt_str
873 ))
874 }
875 Expression::TokenBurn {
876 from,
877 mint,
878 authority,
879 amount,
880 } => {
881 let from_str = self.generate_expression(from)?;
882 let mint_str = self.generate_expression(mint)?;
883 let auth_str = self.generate_expression(authority)?;
884 let amt_str = self.generate_expression(amount)?;
885 Ok(format!(
886 r#"{{
887 let cpi_accounts = anchor_spl::token::Burn {{
888 from: ctx.accounts.{}.to_account_info(),
889 mint: ctx.accounts.{}.to_account_info(),
890 authority: ctx.accounts.{}.to_account_info(),
891 }};
892 let cpi_program = ctx.accounts.token_program.to_account_info();
893 anchor_spl::token::burn(CpiContext::new(cpi_program, cpi_accounts), {} as u64)?
894 }}"#,
895 to_snake_case(&from_str),
896 to_snake_case(&mint_str),
897 to_snake_case(&auth_str),
898 amt_str
899 ))
900 }
901 Expression::SolTransfer { to, amount } => {
902 let to_str = self.generate_expression(to)?;
903 let amt_str = self.generate_expression(amount)?;
904 Ok(format!(
907 r#"{{
908 // Validate recipient matches the intended destination
909 require!(ctx.accounts.recipient.key() == {to_str}, CustomError::InvalidRecipient);
910 let cpi_accounts = anchor_lang::system_program::Transfer {{
911 from: ctx.accounts.signer.to_account_info(),
912 to: ctx.accounts.recipient.to_account_info(),
913 }};
914 let cpi_ctx = anchor_lang::prelude::CpiContext::new(
915 ctx.accounts.system_program.to_account_info(),
916 cpi_accounts
917 );
918 anchor_lang::system_program::transfer(cpi_ctx, {amt_str} as u64)?
919 }}"#
920 ))
921 }
922 Expression::GetATA { owner, mint } => {
923 let owner_str = self.generate_expression(owner)?;
924 let mint_str = self.generate_expression(mint)?;
925 Ok(format!(
926 "anchor_spl::associated_token::get_associated_token_address(&{}, &{})",
927 owner_str, mint_str
928 ))
929 }
930 Expression::Index { expr, index } => {
931 let e = self.generate_expression(expr)?;
932 let i = self.generate_expression(index)?;
933 Ok(format!("{}[{} as usize]", e, i))
935 }
936 Expression::Field { expr, field } => {
937 let e = self.generate_expression(expr)?;
938 if field == "length" {
940 Ok(format!("({}.len() as u128)", e))
941 } else {
942 Ok(format!("{}.{}", e, to_snake_case(field)))
943 }
944 }
945 Expression::Ternary {
946 condition,
947 then_expr,
948 else_expr,
949 } => {
950 let c = self.generate_expression(condition)?;
951 let t = self.generate_expression(then_expr)?;
952 let e = self.generate_expression(else_expr)?;
953 Ok(format!("if {} {{ {} }} else {{ {} }}", c, t, e))
954 }
955 Expression::Assert { condition, message } => {
956 let c = self.generate_expression(condition)?;
957 if let Some(msg) = message {
958 Ok(format!("assert!({}, \"{}\")", c, msg))
959 } else {
960 Ok(format!("assert!({})", c))
961 }
962 }
963 Expression::AssertEq {
964 left,
965 right,
966 message,
967 } => {
968 let l = self.generate_expression(left)?;
969 let r = self.generate_expression(right)?;
970 if let Some(msg) = message {
971 Ok(format!("assert_eq!({}, {}, \"{}\")", l, r, msg))
972 } else {
973 Ok(format!("assert_eq!({}, {})", l, r))
974 }
975 }
976 Expression::AssertNe {
977 left,
978 right,
979 message,
980 } => {
981 let l = self.generate_expression(left)?;
982 let r = self.generate_expression(right)?;
983 if let Some(msg) = message {
984 Ok(format!("assert_ne!({}, {}, \"{}\")", l, r, msg))
985 } else {
986 Ok(format!("assert_ne!({}, {})", l, r))
987 }
988 }
989 Expression::AssertGt {
990 left,
991 right,
992 message,
993 } => {
994 let l = self.generate_expression(left)?;
995 let r = self.generate_expression(right)?;
996 if let Some(msg) = message {
997 Ok(format!("assert!({} > {}, \"{}\")", l, r, msg))
998 } else {
999 Ok(format!("assert!({} > {})", l, r))
1000 }
1001 }
1002 Expression::AssertGe {
1003 left,
1004 right,
1005 message,
1006 } => {
1007 let l = self.generate_expression(left)?;
1008 let r = self.generate_expression(right)?;
1009 if let Some(msg) = message {
1010 Ok(format!("assert!({} >= {}, \"{}\")", l, r, msg))
1011 } else {
1012 Ok(format!("assert!({} >= {})", l, r))
1013 }
1014 }
1015 Expression::AssertLt {
1016 left,
1017 right,
1018 message,
1019 } => {
1020 let l = self.generate_expression(left)?;
1021 let r = self.generate_expression(right)?;
1022 if let Some(msg) = message {
1023 Ok(format!("assert!({} < {}, \"{}\")", l, r, msg))
1024 } else {
1025 Ok(format!("assert!({} < {})", l, r))
1026 }
1027 }
1028 Expression::AssertLe {
1029 left,
1030 right,
1031 message,
1032 } => {
1033 let l = self.generate_expression(left)?;
1034 let r = self.generate_expression(right)?;
1035 if let Some(msg) = message {
1036 Ok(format!("assert!({} <= {}, \"{}\")", l, r, msg))
1037 } else {
1038 Ok(format!("assert!({} <= {})", l, r))
1039 }
1040 }
1041 }
1042 }
1043
1044 fn generate_literal(&self, lit: &Literal) -> Result<String, CodegenError> {
1045 match lit {
1046 Literal::Bool(b) => Ok(b.to_string()),
1047 Literal::Int(n) => Ok(format!("{}i128", n)),
1048 Literal::Uint(n) => Ok(format!("{}u128", n)),
1049 Literal::String(s) => Ok(format!("\"{}\"", s.replace('\"', "\\\""))),
1050 Literal::Pubkey(s) => {
1051 Ok(format!("Pubkey::default() /* {} */", s))
1053 }
1054 Literal::ZeroAddress => {
1055 Ok("Pubkey::default()".to_string())
1057 }
1058 Literal::ZeroBytes(n) => {
1059 Ok(format!("[0u8; {}]", n))
1061 }
1062 }
1063 }
1064
1065 fn generate_state_rs(&self, program: &SolanaProgram) -> Result<String, CodegenError> {
1066 let mut content = String::from(
1067 r#"//! Program state definitions
1068
1069use anchor_lang::prelude::*;
1070
1071"#,
1072 );
1073
1074 for enum_def in &program.enums {
1076 content.push_str("#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, Default)]\n");
1077 content.push_str(&format!("pub enum {} {{\n", to_pascal_case(&enum_def.name)));
1078 for (i, variant) in enum_def.variants.iter().enumerate() {
1079 if i == 0 {
1080 content.push_str(" #[default]\n");
1081 }
1082 content.push_str(&format!(" {},\n", to_pascal_case(variant)));
1083 }
1084 content.push_str("}\n\n");
1085 }
1086
1087 for struct_def in &program.structs {
1089 content.push_str("#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)]\n");
1090 content.push_str(&format!(
1091 "pub struct {} {{\n",
1092 to_pascal_case(&struct_def.name)
1093 ));
1094 for field in &struct_def.fields {
1095 content.push_str(&format!(
1096 " pub {}: {},\n",
1097 to_snake_case(&field.name),
1098 self.type_to_rust(&field.ty)
1099 ));
1100 }
1101 content.push_str("}\n\n");
1102 }
1103
1104 content.push_str("#[account]\n");
1106 content.push_str("#[derive(InitSpace)]\n");
1107 content.push_str(&format!(
1108 "pub struct {}State {{\n",
1109 to_pascal_case(&program.name)
1110 ));
1111
1112 for field in &program.state.fields {
1113 if let Some(max_len_attr) = self.get_max_len_attribute(&field.ty) {
1115 content.push_str(&format!(" {}\n", max_len_attr));
1116 }
1117 content.push_str(&format!(
1118 " pub {}: {},\n",
1119 to_snake_case(&field.name),
1120 self.type_to_rust(&field.ty)
1121 ));
1122 }
1123
1124 content.push_str("}\n\n");
1125
1126 for mapping in &program.mappings {
1128 let struct_name = format!("{}Entry", to_pascal_case(&mapping.name));
1129 let innermost_ty = self.innermost_value_type(&mapping.value_ty);
1131 let value_type = self.type_to_rust(&innermost_ty);
1132 let key_type = self.type_to_rust(&mapping.key_ty);
1133
1134 let key_max_len = self.get_max_len_attribute(&mapping.key_ty);
1136 let value_max_len = self.get_max_len_attribute(&innermost_ty);
1137
1138 content.push_str(&format!(
1139 "/// PDA account for {} mapping entries\n#[account]\n#[derive(InitSpace)]\npub struct {} {{\n",
1140 mapping.name,
1141 struct_name,
1142 ));
1143
1144 if let Some(attr) = key_max_len {
1146 content.push_str(&format!(" {}\n", attr));
1147 }
1148 content.push_str(&format!(
1149 " /// The key for this entry\n pub key: {},\n",
1150 key_type
1151 ));
1152
1153 if let Some(attr) = value_max_len {
1155 content.push_str(&format!(" {}\n", attr));
1156 }
1157 content.push_str(&format!(
1158 " /// The value stored at this key\n pub value: {},\n",
1159 value_type
1160 ));
1161
1162 content.push_str("}\n\n");
1163 }
1164
1165 Ok(content)
1166 }
1167
1168 fn get_max_len_attribute(&self, ty: &SolanaType) -> Option<String> {
1171 match ty {
1172 SolanaType::String => Some("#[max_len(200)]".to_string()),
1173 SolanaType::Bytes => Some("#[max_len(1000)]".to_string()),
1174 SolanaType::Vec(elem) => {
1175 if self.get_max_len_attribute(elem).is_some() {
1178 let inner_len = match elem.as_ref() {
1180 SolanaType::String => 200,
1181 SolanaType::Bytes => 1000,
1182 _ => 100,
1183 };
1184 Some(format!("#[max_len(100, {})]", inner_len))
1185 } else {
1186 Some("#[max_len(100)]".to_string())
1187 }
1188 }
1189 SolanaType::Option(inner) => self.get_max_len_attribute(inner),
1190 _ => None, }
1192 }
1193
1194 fn generate_instructions_rs(&self, program: &SolanaProgram) -> Result<String, CodegenError> {
1195 let uses_token = program
1197 .instructions
1198 .iter()
1199 .filter(|i| i.is_public)
1200 .any(|i| i.uses_token_program);
1201
1202 let mut content =
1203 String::from("//! Instruction account contexts\n\nuse anchor_lang::prelude::*;\n");
1204
1205 if uses_token {
1206 content.push_str("use anchor_spl::token::Token;\n");
1207 }
1208
1209 content.push_str("use crate::state::*;\n\n");
1210
1211 for instruction in &program.instructions {
1213 if instruction.is_public {
1214 content.push_str(&self.generate_context_struct(instruction, program)?);
1215 content.push('\n');
1216 }
1217 }
1218
1219 Ok(content)
1220 }
1221
1222 fn generate_context_struct(
1223 &self,
1224 instruction: &Instruction,
1225 program: &SolanaProgram,
1226 ) -> Result<String, CodegenError> {
1227 let name = to_pascal_case(&instruction.name);
1228 let state_name = format!("{}State", to_pascal_case(&program.name));
1229
1230 let mut seed_params: Vec<(&String, &SolanaType)> = Vec::new();
1232 for access in &instruction.mapping_accesses {
1233 for key_expr in &access.key_exprs {
1234 self.collect_seed_params(key_expr, instruction, &mut seed_params);
1235 }
1236 }
1237
1238 let mut content = String::new();
1239 content.push_str("#[derive(Accounts)]\n");
1240
1241 if !seed_params.is_empty() {
1243 let params_str: Vec<String> = seed_params
1244 .iter()
1245 .map(|(name, ty)| format!("{}: {}", to_snake_case(name), self.type_to_rust(ty)))
1246 .collect();
1247 content.push_str(&format!("#[instruction({})]\n", params_str.join(", ")));
1248 }
1249
1250 content.push_str(&format!("pub struct {}<'info> {{\n", name));
1251
1252 if instruction.name == "initialize" {
1254 content.push_str(&format!(
1255 r#" #[account(
1256 init,
1257 payer = signer,
1258 space = 8 + {}::INIT_SPACE
1259 )]
1260 pub state: Account<'info, {}>,
1261"#,
1262 state_name, state_name
1263 ));
1264 } else if instruction.is_view {
1265 content.push_str(&format!(" pub state: Account<'info, {}>,\n", state_name));
1266 } else if instruction.closes_state {
1267 content.push_str(&format!(
1269 " #[account(mut, close = signer)]\n pub state: Account<'info, {}>,\n",
1270 state_name
1271 ));
1272 } else {
1273 content.push_str(&format!(
1274 " #[account(mut)]\n pub state: Account<'info, {}>,\n",
1275 state_name
1276 ));
1277 }
1278
1279 content.push_str(" #[account(mut)]\n");
1281 content.push_str(" pub signer: Signer<'info>,\n");
1282
1283 for param in &instruction.params {
1285 if matches!(param.ty, SolanaType::Signer) {
1286 content.push_str(&format!(
1287 " pub {}: Signer<'info>,\n",
1288 to_snake_case(¶m.name)
1289 ));
1290 }
1291 }
1292
1293 for access in &instruction.mapping_accesses {
1295 let entry_type = format!("{}Entry", to_pascal_case(&access.mapping_name));
1296 let account_name = to_snake_case(&access.account_name);
1297
1298 let key_seeds: Vec<String> = access
1300 .key_exprs
1301 .iter()
1302 .map(|k| self.generate_key_seed_expr(k))
1303 .collect::<Result<Vec<_>, _>>()?;
1304 let seeds_str = key_seeds
1305 .iter()
1306 .map(|s| format!("{}.as_ref()", s))
1307 .collect::<Vec<_>>()
1308 .join(", ");
1309
1310 if access.should_close {
1311 content.push_str(&format!(
1313 r#" #[account(
1314 mut,
1315 close = signer,
1316 seeds = [b"{}", {}],
1317 bump
1318 )]
1319 pub {}: Account<'info, {}>,
1320"#,
1321 to_snake_case(&access.mapping_name),
1322 seeds_str,
1323 account_name,
1324 entry_type
1325 ));
1326 } else if access.is_write {
1327 content.push_str(&format!(
1329 r#" #[account(
1330 init_if_needed,
1331 payer = signer,
1332 space = 8 + {}::INIT_SPACE,
1333 seeds = [b"{}", {}],
1334 bump
1335 )]
1336 pub {}: Account<'info, {}>,
1337"#,
1338 entry_type,
1339 to_snake_case(&access.mapping_name),
1340 seeds_str,
1341 account_name,
1342 entry_type
1343 ));
1344 } else {
1345 content.push_str(&format!(
1347 r#" #[account(
1348 seeds = [b"{}", {}],
1349 bump
1350 )]
1351 pub {}: Account<'info, {}>,
1352"#,
1353 to_snake_case(&access.mapping_name),
1354 seeds_str,
1355 account_name,
1356 entry_type
1357 ));
1358 }
1359 }
1360
1361 if instruction.uses_sol_transfer {
1364 content.push_str(
1365 " /// CHECK: Recipient account for SOL transfer, validated by the caller\n",
1366 );
1367 content.push_str(" #[account(mut)]\n");
1368 content.push_str(" pub recipient: UncheckedAccount<'info>,\n");
1369 }
1370
1371 let needs_system_program = instruction.name == "initialize"
1373 || instruction.mapping_accesses.iter().any(|a| a.is_write)
1374 || instruction.is_payable
1375 || instruction.uses_sol_transfer;
1376 if needs_system_program {
1377 content.push_str(" pub system_program: Program<'info, System>,\n");
1378 }
1379
1380 if instruction.uses_token_program {
1382 content.push_str(" pub token_program: Program<'info, Token>,\n");
1383 }
1384
1385 content.push_str("}\n");
1386
1387 Ok(content)
1388 }
1389
1390 fn generate_key_seed_expr(&self, key_expr: &Expression) -> Result<String, CodegenError> {
1392 match key_expr {
1393 Expression::MsgSender => Ok("signer.key()".to_string()),
1395 Expression::Var(name) => Ok(to_snake_case(name)),
1396 Expression::Literal(Literal::Pubkey(s)) => Ok(format!("Pubkey::default() /* {} */", s)),
1397 Expression::Literal(Literal::ZeroAddress) => Ok("Pubkey::default()".to_string()),
1398 Expression::Literal(Literal::ZeroBytes(n)) => Ok(format!("[0u8; {}]", n)),
1399 Expression::StateAccess(field) => {
1400 Ok(format!("state.{}", to_snake_case(field)))
1402 }
1403 _ => {
1404 let expr_str = self.generate_expression(key_expr)?;
1407 Ok(expr_str.replace("ctx.accounts.", ""))
1409 }
1410 }
1411 }
1412
1413 fn collect_seed_params<'a>(
1415 &self,
1416 key_expr: &'a Expression,
1417 instruction: &'a Instruction,
1418 params: &mut Vec<(&'a String, &'a SolanaType)>,
1419 ) {
1420 match key_expr {
1421 Expression::Var(name) => {
1422 if let Some(param) = instruction.params.iter().find(|p| &p.name == name) {
1424 if !params.iter().any(|(n, _)| *n == name) {
1426 params.push((¶m.name, ¶m.ty));
1427 }
1428 }
1429 }
1430 Expression::MethodCall { receiver, args, .. } => {
1431 self.collect_seed_params(receiver, instruction, params);
1432 for arg in args {
1433 self.collect_seed_params(arg, instruction, params);
1434 }
1435 }
1436 Expression::Binary { left, right, .. } => {
1437 self.collect_seed_params(left, instruction, params);
1438 self.collect_seed_params(right, instruction, params);
1439 }
1440 _ => {}
1441 }
1442 }
1443
1444 fn generate_error_rs(&self, program: &SolanaProgram) -> Result<String, CodegenError> {
1445 let mut content = String::from(
1446 r#"//! Custom error definitions
1447
1448use anchor_lang::prelude::*;
1449
1450#[error_code]
1451pub enum CustomError {
1452 #[msg("Requirement failed")]
1453 RequireFailed,
1454 #[msg("Invalid recipient account")]
1455 InvalidRecipient,
1456"#,
1457 );
1458
1459 for error in &program.errors {
1461 content.push_str(&format!(
1462 " #[msg(\"{}\")]\n {},\n",
1463 error.name,
1464 to_pascal_case(&error.name)
1465 ));
1466 }
1467
1468 content.push_str("}\n");
1469
1470 Ok(content)
1471 }
1472
1473 fn generate_events_rs(&self, program: &SolanaProgram) -> Result<String, CodegenError> {
1474 let mut content = String::from(
1475 r#"//! Event definitions
1476
1477use anchor_lang::prelude::*;
1478
1479"#,
1480 );
1481
1482 for event in &program.events {
1483 content.push_str("#[event]\n");
1484 content.push_str(&format!("pub struct {} {{\n", to_pascal_case(&event.name)));
1485
1486 for field in &event.fields {
1487 content.push_str(&format!(
1490 " pub {}: {},\n",
1491 to_snake_case(&field.name),
1492 self.type_to_rust(&field.ty)
1493 ));
1494 }
1495
1496 content.push_str("}\n\n");
1497 }
1498
1499 Ok(content)
1500 }
1501
1502 fn generate_anchor_toml(&self, program: &SolanaProgram) -> String {
1503 let name = to_snake_case(&program.name);
1504 format!(
1505 r#"[features]
1506seeds = false
1507skip-lint = false
1508
1509[programs.localnet]
1510{} = "11111111111111111111111111111111"
1511
1512[registry]
1513url = "https://api.apr.dev"
1514
1515[provider]
1516cluster = "localnet"
1517wallet = "~/.config/solana/id.json"
1518
1519[scripts]
1520test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
1521"#,
1522 name
1523 )
1524 }
1525
1526 fn generate_cargo_toml(&self, program: &SolanaProgram) -> String {
1527 let name = to_snake_case(&program.name);
1528 let uses_token = program.instructions.iter().any(|i| i.uses_token_program);
1529
1530 let mut deps = String::from(
1531 "anchor-lang = { version = \"0.32.0\", features = [\"init-if-needed\"] }\n",
1532 );
1533 if uses_token {
1534 deps.push_str("anchor-spl = \"0.32.0\"\n");
1535 }
1536
1537 format!(
1538 r#"[package]
1539name = "{}"
1540version = "0.1.0"
1541description = "Generated by SolScript compiler"
1542edition = "2021"
1543
1544[lib]
1545crate-type = ["cdylib", "lib"]
1546name = "{}"
1547
1548[features]
1549no-entrypoint = []
1550no-idl = []
1551no-log-ix-name = []
1552cpi = ["no-entrypoint"]
1553default = []
1554
1555[dependencies]
1556{}
1557"#,
1558 name, name, deps
1559 )
1560 }
1561
1562 fn type_to_rust(&self, ty: &SolanaType) -> String {
1563 match ty {
1564 SolanaType::U8 => "u8".to_string(),
1565 SolanaType::U16 => "u16".to_string(),
1566 SolanaType::U32 => "u32".to_string(),
1567 SolanaType::U64 => "u64".to_string(),
1568 SolanaType::U128 => "u128".to_string(),
1569 SolanaType::I8 => "i8".to_string(),
1570 SolanaType::I16 => "i16".to_string(),
1571 SolanaType::I32 => "i32".to_string(),
1572 SolanaType::I64 => "i64".to_string(),
1573 SolanaType::I128 => "i128".to_string(),
1574 SolanaType::Bool => "bool".to_string(),
1575 SolanaType::Pubkey => "Pubkey".to_string(),
1576 SolanaType::Signer => "Pubkey".to_string(), SolanaType::String => "String".to_string(),
1578 SolanaType::Bytes => "Vec<u8>".to_string(),
1579 SolanaType::FixedBytes(n) => format!("[u8; {}]", n),
1580 SolanaType::Array(elem, size) => format!("[{}; {}]", self.type_to_rust(elem), size),
1581 SolanaType::Vec(elem) => format!("Vec<{}>", self.type_to_rust(elem)),
1582 SolanaType::Option(inner) => format!("Option<{}>", self.type_to_rust(inner)),
1583 SolanaType::Mapping(_, _) => "/* Mapping - use PDAs */".to_string(),
1584 SolanaType::Custom(name) => to_pascal_case(name),
1585 }
1586 }
1587
1588 fn innermost_value_type(&self, ty: &SolanaType) -> SolanaType {
1591 match ty {
1592 SolanaType::Mapping(_, value_ty) => self.innermost_value_type(value_ty),
1593 other => other.clone(),
1594 }
1595 }
1596
1597 fn generate_package_json(&self, program: &SolanaProgram) -> String {
1598 let name = to_snake_case(&program.name);
1599 format!(
1600 r#"{{
1601 "name": "{}-client",
1602 "version": "0.1.0",
1603 "description": "Generated client for {} Solana program",
1604 "main": "app/client.ts",
1605 "scripts": {{
1606 "test": "anchor test",
1607 "build": "anchor build",
1608 "deploy": "anchor deploy"
1609 }},
1610 "dependencies": {{
1611 "@coral-xyz/anchor": "^0.32.0",
1612 "@solana/web3.js": "^1.95.0"
1613 }},
1614 "devDependencies": {{
1615 "@types/chai": "^4.3.0",
1616 "@types/mocha": "^10.0.0",
1617 "chai": "^4.3.0",
1618 "mocha": "^10.2.0",
1619 "ts-mocha": "^10.0.0",
1620 "typescript": "^5.0.0"
1621 }}
1622}}
1623"#,
1624 name, program.name
1625 )
1626 }
1627
1628 fn generate_readme(&self, program: &SolanaProgram) -> String {
1629 let name = &program.name;
1630 let _snake_name = to_snake_case(name);
1631
1632 let public_fns: Vec<&str> = program
1634 .instructions
1635 .iter()
1636 .filter(|i| i.is_public)
1637 .map(|i| i.name.as_str())
1638 .collect();
1639
1640 let fn_list = public_fns
1641 .iter()
1642 .map(|f| format!("- `{}`", to_snake_case(f)))
1643 .collect::<Vec<_>>()
1644 .join("\n");
1645
1646 format!(
1647 r#"# {} Solana Program
1648
1649Generated by [SolScript](https://github.com/cryptuon/solscript) compiler.
1650
1651## Overview
1652
1653This is an Anchor-based Solana program with a TypeScript client.
1654
1655## Project Structure
1656
1657```
1658.
1659├── programs/
1660│ └── solscript_program/
1661│ └── src/
1662│ ├── lib.rs # Main program entry
1663│ ├── state.rs # Account state definitions
1664│ ├── instructions.rs # Instruction contexts
1665│ ├── error.rs # Custom errors
1666│ └── events.rs # Event definitions
1667├── app/
1668│ └── client.ts # TypeScript client
1669├── tests/
1670│ └── program.test.ts # Anchor tests
1671├── target/
1672│ └── idl/
1673│ └── program.json # Anchor IDL
1674├── Anchor.toml
1675├── Cargo.toml
1676└── package.json
1677```
1678
1679## Available Instructions
1680
1681{}
1682
1683## Getting Started
1684
1685### Prerequisites
1686
1687- [Rust](https://www.rust-lang.org/tools/install)
1688- [Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools)
1689- [Anchor](https://www.anchor-lang.com/docs/installation)
1690- [Node.js](https://nodejs.org/)
1691
1692### Build
1693
1694```bash
1695anchor build
1696```
1697
1698### Test
1699
1700```bash
1701anchor test
1702```
1703
1704### Deploy
1705
1706```bash
1707anchor deploy
1708```
1709
1710## Usage
1711
1712See `app/client.ts` for the TypeScript client implementation.
1713
1714```typescript
1715import {{ {}Client }} from './app/client';
1716
1717// Initialize client with provider
1718const client = new {}Client(provider);
1719
1720// Call instructions...
1721```
1722"#,
1723 name,
1724 fn_list,
1725 to_pascal_case(name),
1726 to_pascal_case(name)
1727 )
1728 }
1729
1730 fn generate_gitignore(&self) -> String {
1731 r#"# Anchor
1732target/
1733.anchor/
1734node_modules/
1735
1736# Rust
1737Cargo.lock
1738**/*.rs.bk
1739
1740# IDE
1741.idea/
1742.vscode/
1743*.swp
1744*.swo
1745
1746# OS
1747.DS_Store
1748Thumbs.db
1749
1750# Solana
1751test-ledger/
1752.env
1753
1754# TypeScript
1755dist/
1756*.js
1757*.d.ts
1758*.map
1759!anchor.js
1760"#
1761 .to_string()
1762 }
1763}
1764
1765fn to_snake_case(s: &str) -> String {
1767 let mut result = String::new();
1768 let mut prev_upper = false;
1769
1770 for (i, c) in s.chars().enumerate() {
1771 if c.is_uppercase() {
1772 if i > 0 && !prev_upper {
1773 result.push('_');
1774 }
1775 result.push(c.to_lowercase().next().unwrap());
1776 prev_upper = true;
1777 } else {
1778 result.push(c);
1779 prev_upper = false;
1780 }
1781 }
1782
1783 result
1784}
1785
1786fn to_pascal_case(s: &str) -> String {
1787 let mut result = String::new();
1788 let mut capitalize_next = true;
1789
1790 for c in s.chars() {
1791 if c == '_' {
1792 capitalize_next = true;
1793 } else if capitalize_next {
1794 result.push(c.to_uppercase().next().unwrap());
1795 capitalize_next = false;
1796 } else {
1797 result.push(c);
1798 }
1799 }
1800
1801 result
1802}