/ lib / src / lib.rs
lib.rs
  1  //! Gramr - A blazing-fast library for scaffolding smart contracts
  2  //! 
  3  //! This library provides the core functionality for generating Solidity and Rust/Stylus contracts,
  4  //! tests, and deployment scripts for Foundry and Cargo projects.
  5  
  6  pub mod error;
  7  pub mod foundry;
  8  pub mod generators;
  9  pub mod templates;
 10  pub mod language;
 11  pub mod project;
 12  
 13  // Re-export commonly used types
 14  pub use error::{GramrError, Result};
 15  pub use foundry::FoundryProject;
 16  pub use generators::{ContractGenerator, ScriptGenerator, TestGenerator, GenericContractGenerator, LibraryGenerator, InterfaceGenerator, AbstractContractGenerator, ConfigGenerator};
 17  pub use templates::{ContractType, TokenExtension, SolidityTemplate, StylusTemplate};
 18  pub use language::Language;
 19  pub use project::{Project, ProjectType, CargoProject};
 20  
 21  // Version info
 22  pub const VERSION: &str = env!("CARGO_PKG_VERSION");
 23  
 24  /// Builder pattern for creating contracts programmatically
 25  pub struct ContractBuilder {
 26      name: String,
 27      contract_type: ContractType,
 28      pragma: String,
 29      license: String,
 30  }
 31  
 32  impl ContractBuilder {
 33      /// Create a new contract builder
 34      pub fn new(name: impl Into<String>) -> Self {
 35          Self {
 36              name: name.into(),
 37              contract_type: ContractType::Basic,
 38              pragma: "0.8.30".to_string(),
 39              license: "UNLICENSED".to_string(),
 40          }
 41      }
 42  
 43      /// Set the contract type
 44      pub fn contract_type(mut self, contract_type: ContractType) -> Self {
 45          self.contract_type = contract_type;
 46          self
 47      }
 48  
 49      /// Set the pragma version
 50      pub fn pragma(mut self, pragma: impl Into<String>) -> Self {
 51          self.pragma = pragma.into();
 52          self
 53      }
 54  
 55      /// Set the license
 56      pub fn license(mut self, license: impl Into<String>) -> Self {
 57          self.license = license.into();
 58          self
 59      }
 60  
 61      /// Generate the contract source code
 62      pub fn build(self) -> String {
 63          let template = SolidityTemplate::new(
 64              self.name,
 65              self.contract_type,
 66              self.pragma,
 67              self.license,
 68          );
 69          template.generate_contract()
 70      }
 71  
 72      /// Generate and write the contract to a Foundry project
 73      pub fn generate(self, project: FoundryProject) -> Result<()> {
 74          let generator = ContractGenerator::new(
 75              project,
 76              self.name.clone(),
 77              self.contract_type,
 78              false,
 79              false,
 80              self.pragma,
 81              self.license,
 82          );
 83          generator.generate()
 84      }
 85  }
 86  
 87  /// Helper function to parse extensions from strings
 88  pub fn parse_extensions(extensions: &[String]) -> Result<Vec<TokenExtension>> {
 89      use templates::TokenExtension;
 90      
 91      let mut result = Vec::new();
 92      
 93      for ext in extensions {
 94          let extension = match ext.to_lowercase().as_str() {
 95              // ERC20 Extensions
 96              "permit" => TokenExtension::ERC20Permit,
 97              "burnable" => TokenExtension::ERC20Burnable,
 98              "capped" => TokenExtension::ERC20Capped,
 99              "pausable" => TokenExtension::ERC20Pausable,
100              "votes" => TokenExtension::ERC20Votes,
101              "wrapper" => TokenExtension::ERC20Wrapper,
102              "flashmint" => TokenExtension::ERC20FlashMint,
103              "temporaryapproval" => TokenExtension::ERC20TemporaryApproval,
104              "bridgeable" => TokenExtension::ERC20Bridgeable,
105              "erc1363" => TokenExtension::ERC1363,
106              "erc4626" => TokenExtension::ERC4626,
107              
108              // ERC721 Extensions  
109              "consecutive" => TokenExtension::ERC721Consecutive,
110              "uristorage" => TokenExtension::ERC721URIStorage,
111              "royalty" => TokenExtension::ERC721Royalty,
112              "enumerable" => TokenExtension::ERC721Enumerable,
113              
114              // ERC1155 Extensions
115              "supply" => TokenExtension::ERC1155Supply,
116              
117              _ => return Err(GramrError::Other(
118                  format!("Unknown extension: {}. Available extensions: permit, burnable, capped, pausable, votes, wrapper, flashmint, temporaryapproval, bridgeable, erc1363, erc4626, consecutive, uristorage, royalty, wrapper, enumerable, supply", ext)
119              )),
120          };
121          
122          result.push(extension);
123      }
124      
125      Ok(result)
126  }
127  
128  #[cfg(test)]
129  mod tests {
130      use super::*;
131  
132      #[test]
133      fn test_version_constant() {
134          assert!(!VERSION.is_empty());
135          assert!(VERSION.chars().any(|c| c.is_ascii_digit()));
136      }
137  
138      mod contract_builder_tests {
139          use super::*;
140  
141          #[test]
142          fn test_new_contract_builder() {
143              let builder = ContractBuilder::new("TestContract");
144              assert_eq!(builder.name, "TestContract");
145              assert!(matches!(builder.contract_type, ContractType::Basic));
146              assert_eq!(builder.pragma, "0.8.30");
147              assert_eq!(builder.license, "UNLICENSED");
148          }
149  
150          #[test]
151          fn test_contract_builder_chain() {
152              let builder = ContractBuilder::new("MyToken")
153                  .contract_type(ContractType::ERC20)
154                  .pragma("0.8.25")
155                  .license("MIT");
156              
157              assert_eq!(builder.name, "MyToken");
158              assert!(matches!(builder.contract_type, ContractType::ERC20));
159              assert_eq!(builder.pragma, "0.8.25");
160              assert_eq!(builder.license, "MIT");
161          }
162  
163          #[test]
164          fn test_contract_builder_with_different_types() {
165              let erc20_builder = ContractBuilder::new("Token").contract_type(ContractType::ERC20);
166              assert!(matches!(erc20_builder.contract_type, ContractType::ERC20));
167  
168              let erc721_builder = ContractBuilder::new("NFT").contract_type(ContractType::ERC721);
169              assert!(matches!(erc721_builder.contract_type, ContractType::ERC721));
170  
171              let erc1155_builder = ContractBuilder::new("Multi").contract_type(ContractType::ERC1155);
172              assert!(matches!(erc1155_builder.contract_type, ContractType::ERC1155));
173          }
174  
175          #[test]
176          fn test_build_basic_contract() {
177              let contract = ContractBuilder::new("BasicContract")
178                  .pragma("0.8.20")
179                  .license("MIT")
180                  .build();
181              
182              assert!(contract.contains("BasicContract"));
183              assert!(contract.contains("0.8.20"));
184              assert!(contract.contains("MIT"));
185              assert!(contract.contains("pragma solidity"));
186          }
187  
188          #[test]
189          fn test_build_erc20_contract() {
190              let contract = ContractBuilder::new("MyToken")
191                  .contract_type(ContractType::ERC20)
192                  .build();
193              
194              assert!(contract.contains("MyToken"));
195              assert!(contract.contains("ERC20"));
196              assert!(contract.contains("import"));
197          }
198  
199          #[test]
200          fn test_build_with_empty_name() {
201              let contract = ContractBuilder::new("")
202                  .build();
203              
204              // Should still generate valid solidity
205              assert!(contract.contains("pragma solidity"));
206          }
207  
208          #[test]
209          fn test_build_with_special_characters_in_name() {
210              let contract = ContractBuilder::new("My_Token_123")
211                  .build();
212              
213              assert!(contract.contains("My_Token_123"));
214          }
215      }
216  
217      mod parse_extensions_tests {
218          use super::*;
219  
220          #[test]
221          fn test_parse_empty_extensions() {
222              let result = parse_extensions(&[]);
223              assert!(result.is_ok());
224              assert!(result.unwrap().is_empty());
225          }
226  
227          #[test]
228          fn test_parse_single_erc20_extension() {
229              let result = parse_extensions(&["burnable".to_string()]);
230              assert!(result.is_ok());
231              let extensions = result.unwrap();
232              assert_eq!(extensions.len(), 1);
233              assert!(matches!(extensions[0], TokenExtension::ERC20Burnable));
234          }
235  
236          #[test]
237          fn test_parse_multiple_erc20_extensions() {
238              let result = parse_extensions(&[
239                  "burnable".to_string(),
240                  "pausable".to_string(),
241                  "permit".to_string(),
242              ]);
243              assert!(result.is_ok());
244              let extensions = result.unwrap();
245              assert_eq!(extensions.len(), 3);
246          }
247  
248          #[test]
249          fn test_parse_erc721_extensions() {
250              let result = parse_extensions(&[
251                  "enumerable".to_string(),
252                  "uristorage".to_string(),
253                  "royalty".to_string(),
254              ]);
255              assert!(result.is_ok());
256              let extensions = result.unwrap();
257              assert_eq!(extensions.len(), 3);
258              assert!(matches!(extensions[0], TokenExtension::ERC721Enumerable));
259              assert!(matches!(extensions[1], TokenExtension::ERC721URIStorage));
260              assert!(matches!(extensions[2], TokenExtension::ERC721Royalty));
261          }
262  
263          #[test]
264          fn test_parse_erc1155_extensions() {
265              let result = parse_extensions(&["supply".to_string()]);
266              assert!(result.is_ok());
267              let extensions = result.unwrap();
268              assert_eq!(extensions.len(), 1);
269              assert!(matches!(extensions[0], TokenExtension::ERC1155Supply));
270          }
271  
272          #[test]
273          fn test_parse_case_insensitive() {
274              let result = parse_extensions(&[
275                  "BURNABLE".to_string(),
276                  "Pausable".to_string(),
277                  "peRmIt".to_string(),
278              ]);
279              assert!(result.is_ok());
280              let extensions = result.unwrap();
281              assert_eq!(extensions.len(), 3);
282          }
283  
284          #[test]
285          fn test_parse_unknown_extension() {
286              let result = parse_extensions(&["unknown".to_string()]);
287              assert!(result.is_err());
288              
289              if let Err(GramrError::Other(msg)) = result {
290                  assert!(msg.contains("Unknown extension: unknown"));
291                  assert!(msg.contains("Available extensions:"));
292              } else {
293                  panic!("Expected GramrError::Other");
294              }
295          }
296  
297          #[test]
298          fn test_parse_mixed_valid_invalid() {
299              let result = parse_extensions(&[
300                  "burnable".to_string(),
301                  "invalid".to_string(),
302              ]);
303              assert!(result.is_err());
304          }
305  
306          #[test]
307          fn test_parse_all_erc20_extensions() {
308              let extensions = vec![
309                  "permit".to_string(),
310                  "burnable".to_string(),
311                  "capped".to_string(),
312                  "pausable".to_string(),
313                  "votes".to_string(),
314                  "wrapper".to_string(),
315                  "flashmint".to_string(),
316                  "temporaryapproval".to_string(),
317                  "bridgeable".to_string(),
318                  "erc1363".to_string(),
319                  "erc4626".to_string(),
320              ];
321              
322              let result = parse_extensions(&extensions);
323              assert!(result.is_ok());
324              assert_eq!(result.unwrap().len(), 11);
325          }
326  
327          #[test]
328          fn test_parse_duplicate_extensions() {
329              let result = parse_extensions(&[
330                  "burnable".to_string(),
331                  "burnable".to_string(),
332              ]);
333              assert!(result.is_ok());
334              let extensions = result.unwrap();
335              assert_eq!(extensions.len(), 2); // Duplicates allowed
336          }
337  
338          #[test]
339          fn test_parse_whitespace_extensions() {
340              let result = parse_extensions(&[" burnable ".to_string()]);
341              assert!(result.is_err()); // Whitespace not trimmed
342          }
343      }
344  
345      #[test]
346      fn test_module_exports() {
347          // Test that key types are properly exported
348          let _error: GramrError = GramrError::Other("test".to_string());
349          let _lang = Language::Solidity;
350          let _contract_type = ContractType::Basic;
351          let _token_ext = TokenExtension::ERC20Burnable;
352          
353          // This test ensures the public API is accessible
354          assert!(true);
355      }
356  
357      #[test]
358      fn test_contract_builder_string_conversions() {
359          let builder = ContractBuilder::new("Test".to_string())
360              .pragma("0.8.19".to_string())
361              .license("Apache-2.0".to_string());
362          
363          assert_eq!(builder.name, "Test");
364          assert_eq!(builder.pragma, "0.8.19");
365          assert_eq!(builder.license, "Apache-2.0");
366      }
367  
368      #[test]
369      fn test_contract_builder_str_conversions() {
370          let builder = ContractBuilder::new("Test")
371              .pragma("0.8.19")
372              .license("Apache-2.0");
373          
374          assert_eq!(builder.name, "Test");
375          assert_eq!(builder.pragma, "0.8.19");
376          assert_eq!(builder.license, "Apache-2.0");
377      }
378  }