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 }