diff --git a/README.md b/README.md index 5f6b2f8af..75da4ea6d 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,44 @@ dotnet test ### Creating a New Smart Contract +The `nccs` CLI can scaffold a ready-to-build project (and optional tests) in one step. If you have installed the tool globally you can run `nccs`; otherwise prefix commands with `dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj --`: + +```bash +dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj -- templates +dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj -- new HelloWorld --template Basic --with-tests +cd HelloWorld +dotnet tool restore +dotnet build && dotnet tool run nccs HelloWorld.csproj +cd ../HelloWorld.UnitTests +dotnet test +``` + +Prefer a one-liner? Use the helper script: + +```bash +./scripts/new_contract_with_tests.sh HelloWorld Basic +``` +The second argument is optional (defaults to `Basic`). Available templates: `Basic`, `NEP17`, `NEP11`, `Ownable`, `Oracle`. + +To compose features, pass them after the template (or instead of it) and the helper will forward them as `--features`, for example: + +```bash +./scripts/new_contract_with_tests.sh ComboToken Basic NEP17 Ownable Oracle +``` + +Looking for more than the starter contracts? The advanced templates ship with opinionated scaffolds: +- Combine feature flags (for example `--features NEP17 Ownable Oracle`) to get an oracle-enabled token without memorising a dedicated template name. +- Need owner rotation on NFTs? `--features NEP11 Ownable` scaffolds an NEP-11 contract with owner management built in. + +You can also compose features without memorising special template names. Mix and match with `--features`: + +```bash +dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj -- \ + new ComboToken --template Basic --features NEP17 Ownable Oracle +``` + +Prefer to wire things up manually? You can still: + 1. Create a new class library project targeting .NET 9.0 or later 2. Add a reference to the Neo.SmartContract.Framework package 3. Create a class that inherits from `SmartContract` diff --git a/eng/NeoDevPack.Version.props b/eng/NeoDevPack.Version.props new file mode 100644 index 000000000..c3bf3c40e --- /dev/null +++ b/eng/NeoDevPack.Version.props @@ -0,0 +1,15 @@ + + + 3.8.1 + * + + + + $(NeoVersionPrefix) + $(NeoVersionPrefix)-$(NeoVersionSuffix) + + + + $(NeoPackageVersion) + + diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props index 0944b73f7..08961bc77 100644 --- a/examples/Directory.Build.props +++ b/examples/Directory.Build.props @@ -1,9 +1,11 @@ + + 2024 The Neo SmartContract Examples Project - 3.8.1 + $(NeoVersionPrefix) net9.0 The Neo SmartContract Examples Project neo.png @@ -19,51 +21,40 @@ enable disable false - + Library PackageReference false disable false - + true true true true true - - - true - 3.8.1 + + + false + $(NeoAnalyzerVersion) - - - - - + + + + Analyzer false - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -88,15 +79,14 @@ $(OutputPath)$(AssemblyName).manifest.json $(MSBuildThisFileDirectory)..\src\Neo.Compiler.CSharp\Neo.Compiler.CSharp.csproj - + - - - + + diff --git a/guidance/GETTING-STARTED-GUIDE.md b/guidance/GETTING-STARTED-GUIDE.md index 95b5a4df6..39bf69006 100644 --- a/guidance/GETTING-STARTED-GUIDE.md +++ b/guidance/GETTING-STARTED-GUIDE.md @@ -98,7 +98,27 @@ For the best development experience, install: Let's create a simple "Hello World" smart contract. -### Step 1: Create a New Project +### Step 1 (Recommended): Scaffold with the CLI + +```bash +# From the neo-devpack-dotnet repository root +dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj -- templates +dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj -- new HelloWorld --template Basic --with-tests + +cd HelloWorld +dotnet tool restore +dotnet build +dotnet tool run nccs HelloWorld.csproj + +cd ../HelloWorld.UnitTests +dotnet test +``` + +This creates both the contract and a MSTest project pre-wired with `Neo.SmartContract.Testing`. + +If you prefer to build things manually, follow the optional steps below. + +### Step 1 (Manual): Create a New Project ```bash # Create a new directory for your contract @@ -120,7 +140,8 @@ Edit `HelloWorldContract.csproj`: - + + ``` @@ -206,8 +227,9 @@ From your project directory, compile the contract: # Build the project first dotnet build -# Compile to NEO bytecode (from the neo-devpack-dotnet root directory) -dotnet run --project src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj -- HelloWorldContract/HelloWorldContract.csproj +# Compile to NEO bytecode with the local tool manifest +dotnet tool restore +dotnet tool run nccs HelloWorldContract.csproj ``` **Verify compilation:** @@ -721,4 +743,4 @@ public class ProxyContract : SmartContract --- -Congratulations! You've completed the Getting Started guide. You now have the foundation to build powerful smart contracts on NEO. Happy coding! \ No newline at end of file +Congratulations! You've completed the Getting Started guide. You now have the foundation to build powerful smart contracts on NEO. Happy coding! diff --git a/scripts/new_contract_with_tests.sh b/scripts/new_contract_with_tests.sh new file mode 100755 index 000000000..75df0d143 --- /dev/null +++ b/scripts/new_contract_with_tests.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +# Helper script for scaffolding a smart contract together with its MSTest project. +# Usage: +# ./scripts/new_contract_with_tests.sh MyContract [Template] +# ./scripts/new_contract_with_tests.sh MyContract [Template] [Feature...] +# Template defaults to "Basic". Available templates: Basic, NEP17, NEP11, Ownable, Oracle. +# Features can be provided as additional arguments (e.g. "NEP17 Ownable Oracle") or comma separated values. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [Template] [Feature ...]" >&2 + exit 1 +fi + +contract_name="$1" +shift + +template="Basic" +features=() + +if [[ $# -gt 0 ]]; then + template="$1" + shift + + # If the template argument looks like inline features, treat it as such + if [[ "$template" == "--features" ]]; then + template="Basic" + elif [[ "$template" == -* ]]; then + echo "Unexpected option '$template'. Pass features without flags." >&2 + exit 1 + elif [[ "$template" == *","* || "$template" == *"+"* ]]; then + features+=("$template") + template="Basic" + fi +fi + +if [[ $# -gt 0 ]]; then + features+=("$@") +fi + +feature_args=() +if [[ ${#features[@]} -gt 0 ]]; then + for feature in "${features[@]}"; do + if [[ -n "$feature" ]]; then + feature_args+=(--features "$feature") + fi + done +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" + +if [[ ${#feature_args[@]} -gt 0 ]]; then + echo "Scaffolding contract '${contract_name}' with template '${template}' and features '${features[*]}' (tests included)..." +else + echo "Scaffolding contract '${contract_name}' with template '${template}' (tests included)..." +fi +dotnet run --project "${repo_root}/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj" -- \ + new "${contract_name}" --template "${template}" "${feature_args[@]}" --with-tests + +cat < + + 2015-2025 The Neo Project The Neo Project - 3.8.1 + $(NeoVersionPrefix) The Neo Project https://github.com/neo-project/neo-devpack-dotnet MIT diff --git a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj index fb4e7bdb3..46d446c68 100644 --- a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj +++ b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj @@ -16,6 +16,7 @@ Neo.Compiler.CSharp true snupkg + $(NeoPackageVersion) @@ -24,13 +25,38 @@ + + ..\..\neo\src + + - - scfx + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index b079a2d47..a60e48ecf 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -40,19 +40,31 @@ public static int Main(string[] args) RootCommand rootCommand = new(Assembly.GetExecutingAssembly().GetCustomAttribute()!.Title); // Add the 'new' subcommand for creating contracts from templates + var featuresOption = new Option(["-f", "--features"], () => Array.Empty(), "Optional feature flags (repeat or comma separated) to compose contracts, e.g. --features NEP17 Ownable Oracle") + { + AllowMultipleArgumentsPerToken = true + }; + var newCommand = new Command("new", "Create a new smart contract from a template") { new Argument("name", "The name of the new contract"), new Option(["-t", "--template"], () => ContractTemplate.Basic, "The template to use (Basic, NEP17, NEP11, Ownable, Oracle)"), + featuresOption, new Option(["-o", "--output"], () => Environment.CurrentDirectory, "The output directory for the new contract"), new Option("--author", () => "Author", "The author of the contract"), new Option("--email", () => $"email@example.com", "The author's email"), new Option("--description", "A description of the contract"), - new Option("--force", "Overwrite existing files") + new Option("--force", "Overwrite existing files"), + new Option("--with-tests", () => false, "Generate a companion MSTest project") }; - newCommand.Handler = CommandHandler.Create(HandleNew); + + newCommand.Handler = CommandHandler.Create(HandleNew); rootCommand.AddCommand(newCommand); + var templatesCommand = new Command("templates", "List available smart contract templates"); + templatesCommand.Handler = CommandHandler.Create(HandleTemplateList); + rootCommand.AddCommand(templatesCommand); + // Add compilation arguments (make them optional for backward compatibility) var pathsArgument = new Argument("paths", "The path of the solution file, project file, project directory or source files.") { @@ -96,7 +108,7 @@ private static CompilationOptions.DebugType ParseDebug(ArgumentResult result) return ret; } - private static int HandleNew(string name, ContractTemplate template, string output, string author, string email, string? description, bool force) + private static int HandleNew(string name, ContractTemplate template, string[] features, string output, string author, string email, string? description, bool force, bool withTests) { try { @@ -145,7 +157,16 @@ private static int HandleNew(string name, ContractTemplate template, string outp } // Generate the contract from template - templateManager.GenerateContract(template, name, output, additionalReplacements); + var featureList = features?.Where(f => !string.IsNullOrWhiteSpace(f)).ToList() ?? new List(); + + if (featureList.Count > 0) + { + templateManager.GenerateContractFromFeatures(featureList, name, output, additionalReplacements, withTests); + } + else + { + templateManager.GenerateContract(template, name, output, additionalReplacements, withTests); + } return 0; } catch (Exception ex) @@ -155,6 +176,19 @@ private static int HandleNew(string name, ContractTemplate template, string outp } } + private static void HandleTemplateList() + { + var manager = new TemplateManager(); + Console.WriteLine("Available templates:\n"); + foreach (var (template, name, description) in manager.GetAvailableTemplates()) + { + Console.WriteLine($" - {template}: {name}\n {description}"); + } + Console.WriteLine(); + Console.WriteLine("Use 'nccs new --template