diff --git a/.devcontainer/devcontainer.dockerfile b/.devcontainer/devcontainer.dockerfile
index 9e9dd5489a..ea4a15c30c 100644
--- a/.devcontainer/devcontainer.dockerfile
+++ b/.devcontainer/devcontainer.dockerfile
@@ -1,6 +1,6 @@
# https://github.com/dotnet/dotnet-docker/blob/main/README.sdk.md
# https://mcr.microsoft.com/en-us/artifact/mar/dotnet/sdk/tags <-- this shows all images
-FROM mcr.microsoft.com/dotnet/sdk:9.0.102-noble
+FROM mcr.microsoft.com/dotnet/sdk:9.0.300-noble
# Install the libleveldb-dev package
RUN apt-get update && apt-get install -y libleveldb-dev
diff --git a/.editorconfig b/.editorconfig
index 4f76e265c0..6be4df71a5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -66,6 +66,8 @@ dotnet_diagnostic.IDE0251.severity = warning
dotnet_diagnostic.IDE0044.severity = warning
dotnet_diagnostic.CS1591.severity = silent
+// Use primary constructor
+csharp_style_prefer_primary_constructors = false
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = false
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4ebc00ed19..e093c7c141 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -2,6 +2,16 @@
+# Change Log
+
+
Fixes # (issue)
## Type of change
@@ -19,10 +29,10 @@ Fixes # (issue)
-- [ ] Test A
-- [ ] Test B
-
-**Test Configuration**:
+- [ ] Unit Testing
+- [ ] Run Application
+- [ ] Local Computer Tests
+- [ ] No Testing
# Checklist:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 7662948e5c..ccd1a4e80d 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -75,9 +75,11 @@ jobs:
-p:GITHUB_ACTIONS=true
- name: Remove (junk)
+ working-directory: ${{ env.DIST_DIR }}/Plugins/LevelDBStore
run: |
- rm -v -R ${{ env.DIST_DIR }}/Plugins/LevelDBStore/runtimes
- rm -v ${{ env.DIST_DIR }}/Plugins/LevelDBStore/Neo*
+ rm -v -R runtimes
+ rm -v Neo*
+ rm -v $(ls *.dll | grep -v "LevelDBStore.dll")
- name: Docker Login
run: |
diff --git a/.github/workflows/issue-metrics.yml b/.github/workflows/issue-metrics.yml
new file mode 100644
index 0000000000..5fdf092dd7
--- /dev/null
+++ b/.github/workflows/issue-metrics.yml
@@ -0,0 +1,40 @@
+name: Monthly issue metrics
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '3 2 1 * *'
+
+permissions:
+ issues: write
+ pull-requests: read
+
+jobs:
+ build:
+ name: issue metrics
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get dates for last month
+ shell: bash
+ run: |
+ # Calculate the first day of the previous month
+ first_day=$(date -d "last month" +%Y-%m-01)
+
+ # Calculate the last day of the previous month
+ last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d)
+
+ #Set an environment variable with the date range
+ echo "$first_day..$last_day"
+ echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
+
+ - name: Run issue-metrics tool
+ uses: github/issue-metrics@v3.20.1
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SEARCH_QUERY: 'repo:neo-project/neo is:issue created:${{ env.last_month }} -reason:"not planned" -reason:"duplicate"'
+
+ - name: Create issue
+ uses: peter-evans/create-issue-from-file@v5.0.1
+ with:
+ title: Monthly issue metrics report
+ token: ${{ secrets.GITHUB_TOKEN }}
+ content-filepath: ./issue_metrics.md
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e52af69fde..3e988859cb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -21,9 +21,37 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Check Format (*.cs)
run: dotnet format --verify-no-changes --verbosity diagnostic
+ Check-Vulnerable:
+ name: Scan for Vulnerable Dependencies
+ needs: [Format]
+ timeout-minutes: 15
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.2.2
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4.3.1
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Scan for Vulnerable Dependencies
+ run: dotnet list package --vulnerable --include-transitive
+
Test-Everything:
needs: [Format]
timeout-minutes: 15
@@ -37,6 +65,13 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Build (Everything)
run: dotnet build --disable-parallel -p:GITHUB_ACTIONS=true
@@ -65,6 +100,13 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key : ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Test (MacOS)
if: matrix.os == 'macos-latest'
run: |
@@ -94,7 +136,7 @@ jobs:
- name: Coveralls
if: matrix.os == 'ubuntu-latest'
- uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b
+ uses: coverallsapp/github-action@v2.3.6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
debug: false
@@ -108,11 +150,13 @@ jobs:
${{ github.workspace }}/TestResults/Neo.VM.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Json.UnitTests/coverage.info
${{ github.workspace }}/TestResults/Neo.Cryptography.MPTTrie.Tests/coverage.info
- ${{ github.workspace }}/TestResults/Neo.Network.RPC.Tests/coverage.info
+ ${{ github.workspace }}/TestResults/Neo.RpcClient.Tests/coverage.info
+ ${{ github.workspace }}/TestResults/Neo.Plugins.DBFTPlugin.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.OracleService.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.RpcServer.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.Storage.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Plugins.ApplicationLogs.Tests/coverage.info
+ ${{ github.workspace }}/TestResults/Neo.Plugins.StateService.Tests/coverage.info
${{ github.workspace }}/TestResults/Neo.Extensions.Tests/coverage.info
PublishPackage:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3b5f9dca30..07aea44733 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -102,6 +102,7 @@ jobs:
run: |
rm -v -R runtimes
rm -v Neo*
+ rm -v $(ls *.dll | grep -v "LevelDBStore.dll")
# Create the distribution directory
- name: Create Distribution Directory
diff --git a/.gitignore b/.gitignore
index a8f241fb91..b8feea9c3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -266,3 +266,5 @@ launchSettings.json
# Benchmarks
**/BenchmarkDotNet.Artifacts/
+/src/Neo.CLI/neo-cli/
+**/localnet_nodes/
\ No newline at end of file
diff --git a/.neo/README.md b/.neo/README.md
index c3738b18b9..3ec40371e2 100644
--- a/.neo/README.md
+++ b/.neo/README.md
@@ -5,9 +5,8 @@ Visit the [documentation](https://docs.neo.org/docs/en-us/index.html) to get sta
## Related projects
Code references are provided for all platform building blocks. That includes the base library, the VM, a command line application and the compiler.
-- [neo:](https://github.com/neo-project/neo/) Neo core library, contains base classes, including ledger, p2p and IO modules.
-- [neo-modules:](https://github.com/neo-project/neo-modules/) Neo modules include additional tools and plugins to be used with Neo.
-- [neo-devpack-dotnet:](https://github.com/neo-project/neo-devpack-dotnet/) These are the official tools used to convert a C# smart-contract into a *neo executable file*.
+- [neo:](https://github.com/neo-project/neo/) Neo core library, contains base classes, including ledger, Peer-to-Peer, IO, plugins and more.
+- [neo-devpack-dotnet:](https://github.com/neo-project/neo-devpack-dotnet/) These are the official tools used to convert C# smart-contract into *neo vm executable file*.
## Opening a new issue
Please feel free to create new issues to suggest features or ask questions.
diff --git a/README.md b/README.md
index a40dbcfaa7..5ea43f2368 100644
--- a/README.md
+++ b/README.md
@@ -94,14 +94,19 @@
+## Quick Start A Mainnet Node
+1. git clone https://github.com/neo-project/neo.git
+2. cd neo/src/Neo.CLI
+3. make build
## Table of Contents
1. [Overview](#overview)
2. [Project structure](#project-structure)
3. [Related projects](#related-projects)
4. [Opening a new issue](#opening-a-new-issue)
-5. [Bounty program](#bounty-program)
-6. [License](#license)
+5. [Contributing](#contributing)
+6. [Bounty program](#bounty-program)
+7. [License](#license)
## Overview
This repository is a csharp implementation of the [neo](https://neo.org) blockchain. It is jointly maintained by the neo core developers and neo global development community.
@@ -146,6 +151,57 @@ Please feel free to create new issues to suggest features or ask questions.
If you found a security issue, please refer to our [security policy](https://github.com/neo-project/neo/security/policy).
+## Contributing
+
+We welcome contributions to the Neo project! To ensure a smooth collaboration process, please follow these guidelines:
+
+### Branch Rules
+
+- **`master`** - Contains the latest stable release version. This branch reflects the current production state.
+- **`dev`** - The main development branch where all new features and improvements are integrated.
+
+### Pull Request Guidelines
+
+**Important**: All pull requests must be based on the `dev` branch, not `master`.
+
+1. **Fork the repository** and create your feature branch from `dev`:
+ ```bash
+ git checkout dev
+ git pull origin dev
+ git checkout -b feature/your-feature-name
+ ```
+
+2. **Make your changes** following the project's coding standards and conventions.
+
+3. **Test your changes** thoroughly to ensure they don't break existing functionality.
+
+4. **Commit your changes** with clear, descriptive commit messages:
+ ```bash
+ git commit -m "feat: add new feature description"
+ ```
+
+5. **Push to your fork** and create a pull request against the `dev` branch:
+ ```bash
+ git push origin feature/your-feature-name
+ ```
+
+6. **Create a Pull Request** targeting the `dev` branch with:
+ - Clear title and description
+ - Reference to any related issues
+ - Summary of changes made
+
+### Development Workflow
+
+```
+feature/bug-fix → dev → master (via release)
+```
+
+- Feature branches are merged into `dev`
+- `dev` is periodically merged into `master` for releases
+- Never create PRs directly against `master`
+
+For more detailed contribution guidelines, please check our documentation or reach out to the maintainers.
+
## Bounty program
You can be rewarded by finding security issues. Please refer to our [bounty program page](https://neo.org/bounty) for more information.
diff --git a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
index debd94430d..adc9647433 100644
--- a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
+++ b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
@@ -5,14 +5,13 @@
net9.0
Neo
enable
- true
-
+
-
+
diff --git a/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Benchmarks.Cache.cs b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Benchmarks.Cache.cs
new file mode 100644
index 0000000000..9d41c1e21c
--- /dev/null
+++ b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Benchmarks.Cache.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Benchmarks.Cache.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using BenchmarkDotNet.Attributes;
+
+namespace Neo.Cryptography.MPTTrie.Benchmarks
+{
+ public class Benchmarks_Cache
+ {
+ private readonly byte _prefix = 0x01;
+ private readonly UInt256 _hash;
+
+ public Benchmarks_Cache()
+ {
+ var randomBytes = new byte[UInt256.Length];
+ new Random(42).NextBytes(randomBytes);
+ _hash = new UInt256(randomBytes);
+ }
+
+ [Benchmark]
+ public byte[] Key_Original()
+ {
+ var buffer = new byte[UInt256.Length + 1];
+ using (var ms = new MemoryStream(buffer, true))
+ using (var writer = new BinaryWriter(ms))
+ {
+ writer.Write(_prefix);
+ _hash.Serialize(writer);
+ }
+ return buffer;
+ }
+
+ [Benchmark]
+ public byte[] Key_Optimized()
+ {
+ var buffer = new byte[UInt256.Length + 1];
+ buffer[0] = _prefix;
+ _hash.GetSpan().CopyTo(buffer.AsSpan(1));
+ return buffer;
+ }
+ }
+}
diff --git a/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Neo.Cryptography.MPTTrie.Benchmarks.csproj b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Neo.Cryptography.MPTTrie.Benchmarks.csproj
new file mode 100644
index 0000000000..07b47fa911
--- /dev/null
+++ b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Neo.Cryptography.MPTTrie.Benchmarks.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Program.cs b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Program.cs
new file mode 100644
index 0000000000..16255a17ee
--- /dev/null
+++ b/benchmarks/Neo.Cryptography.MPTTrie.Benchmarks/Program.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Program.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using BenchmarkDotNet.Running;
+
+// List all benchmarks:
+// dotnet run -c Release --framework [for example: net9.0] -- --list flat(or tree)
+// Run a specific benchmark:
+// dotnet run -c Release --framework [for example: net9.0] -- -f [benchmark name]
+// Run all benchmarks:
+// dotnet run -c Release --framework [for example: net9.0] -- -f *
+// Run all benchmarks of a class:
+// dotnet run -c Release --framework [for example: net9.0] -- -f '*Class*'
+// More options: https://benchmarkdotnet.org/articles/guides/console-args.html
+BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
diff --git a/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj b/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj
index 1d9290d19b..56eccd8683 100644
--- a/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj
+++ b/benchmarks/Neo.Extensions.Benchmarks/Neo.Extensions.Benchmarks.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs
index 743fde0933..bf6bb2a8ce 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JBoolean.cs
@@ -18,10 +18,8 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter]
public class Benchmark_JBoolean
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private JBoolean _jFalse;
- private JBoolean _jTrue;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private JBoolean _jFalse = new();
+ private JBoolean _jTrue = new(true);
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs
index 15048e4c31..305b9d12dc 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JNumber.cs
@@ -18,10 +18,8 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter]
public class Benchmark_JNumber
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private JNumber _maxInt;
- private JNumber _zero;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private JNumber _maxInt = new(JNumber.MAX_SAFE_INTEGER);
+ private JNumber _zero = new(0);
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs
index cc02300ec0..2e706435c5 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JObject.cs
@@ -18,9 +18,7 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter]
public class Benchmark_JObject
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private JObject _alice;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private JObject _alice = new();
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs
index 66307818d3..dadb8866c3 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JPath.cs
@@ -18,9 +18,7 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter]
public class Benchmark_JPath
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private JObject _json;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private JObject _json = new();
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs
index 7c4f08d7f1..3b35bbd64f 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JString.cs
@@ -18,9 +18,7 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter]
public class Benchmark_JString
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private JString _testString;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private JString _testString = new(string.Empty);
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs
index 1b8c0beddf..61fb04cf50 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonArray.cs
@@ -18,11 +18,9 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter] // Markdown 格式导出
public class Benchmark_JsonArray
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private JObject _alice;
- private JObject _bob;
- private JArray _jArray;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private JObject _alice = new();
+ private JObject _bob = new();
+ private JArray _jArray = new();
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs
index 49700a05f8..f2f2370cee 100644
--- a/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs
+++ b/benchmarks/Neo.Json.Benchmarks/Benchmark_JsonDeserialize.cs
@@ -19,9 +19,7 @@ namespace Neo.Json.Benchmarks
[MarkdownExporter] // Exporting results in Markdown format
public class Benchmark_JsonDeserialize
{
-#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
- private string _jsonString;
-#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private string _jsonString = string.Empty;
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json b/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json
index 623b59f0d9..bcc82c91d9 100644
--- a/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json
+++ b/benchmarks/Neo.Json.Benchmarks/Data/RpcTestCases.json
@@ -3628,7 +3628,7 @@
"netfee": "1272390",
"validuntilblock": 2105487,
"attributes": [],
- "cosigners": [
+ "signers": [
{
"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569",
"scopes": "CalledByEntry"
@@ -3679,7 +3679,7 @@
"netfee": "2483780",
"validuntilblock": 2105494,
"attributes": [],
- "cosigners": [
+ "signers": [
{
"account": "0x36d6200fb4c9737c7b552d2b5530ab43605c5869",
"scopes": "CalledByEntry"
@@ -3724,7 +3724,7 @@
"netfee": "2381780",
"validuntilblock": 2105500,
"attributes": [],
- "cosigners": [
+ "signers": [
{
"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569",
"scopes": "CalledByEntry"
diff --git a/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj b/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj
index 1d75612a58..6720cfc7c1 100644
--- a/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj
+++ b/benchmarks/Neo.Json.Benchmarks/Neo.Json.Benchmarks.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/benchmarks/Neo.VM.Benchmarks/InstructionBuilder/InstructionBuilder.cs b/benchmarks/Neo.VM.Benchmarks/InstructionBuilder/InstructionBuilder.cs
index 294f18684a..be1aec1e88 100644
--- a/benchmarks/Neo.VM.Benchmarks/InstructionBuilder/InstructionBuilder.cs
+++ b/benchmarks/Neo.VM.Benchmarks/InstructionBuilder/InstructionBuilder.cs
@@ -56,7 +56,7 @@ internal Instruction Push(BigInteger number)
if (number >= -1 && number <= 16) return AddInstruction(number == -1 ? VM.OpCode.PUSHM1 : VM.OpCode.PUSH0 + (byte)(int)number);
Span buffer = stackalloc byte[32];
if (!number.TryWriteBytes(buffer, out var bytesWritten, isUnsigned: false, isBigEndian: false))
- throw new ArgumentOutOfRangeException(nameof(number));
+ throw new ArgumentOutOfRangeException(nameof(number), "The `number` is too large");
var instruction = bytesWritten switch
{
1 => new Instruction
@@ -89,7 +89,7 @@ internal Instruction Push(BigInteger number)
_opCode = VM.OpCode.PUSHINT256,
_operand = PadRight(buffer, bytesWritten, 32, number.Sign < 0).ToArray()
},
- _ => throw new ArgumentOutOfRangeException($"Number too large: {bytesWritten}")
+ _ => throw new ArgumentOutOfRangeException(nameof(number), "The `number` is too large")
};
AddInstruction(instruction);
return instruction;
diff --git a/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj b/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
index 109fab93e3..e0de03af9a 100644
--- a/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
+++ b/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/benchmarks/Neo.VM.Benchmarks/VMTypes/Benchmarks_DeepCopy.cs b/benchmarks/Neo.VM.Benchmarks/VMTypes/Benchmarks_DeepCopy.cs
index 35fd23319a..508eb625b4 100644
--- a/benchmarks/Neo.VM.Benchmarks/VMTypes/Benchmarks_DeepCopy.cs
+++ b/benchmarks/Neo.VM.Benchmarks/VMTypes/Benchmarks_DeepCopy.cs
@@ -72,20 +72,10 @@ public void BenchNestedTestArrayDeepCopyWithReferenceCounter()
private static void CreateNestedArray(Array? rootArray, int depth, int elementsPerLevel = 1, IReferenceCounter? referenceCounter = null)
{
- if (depth < 0)
- {
- throw new ArgumentException("Depth must be non-negative", nameof(depth));
- }
+ ArgumentOutOfRangeException.ThrowIfNegative(depth, nameof(depth));
+ ArgumentNullException.ThrowIfNull(rootArray, nameof(rootArray));
- if (rootArray == null)
- {
- throw new ArgumentNullException(nameof(rootArray));
- }
-
- if (depth == 0)
- {
- return;
- }
+ if (depth == 0) return;
for (var i = 0; i < elementsPerLevel; i++)
{
@@ -97,20 +87,10 @@ private static void CreateNestedArray(Array? rootArray, int depth, int elementsP
private static void CreateNestedTestArray(TestArray rootArray, int depth, int elementsPerLevel = 1, IReferenceCounter? referenceCounter = null)
{
- if (depth < 0)
- {
- throw new ArgumentException("Depth must be non-negative", nameof(depth));
- }
+ ArgumentOutOfRangeException.ThrowIfNegative(depth, nameof(depth));
+ ArgumentNullException.ThrowIfNull(rootArray, nameof(rootArray));
- if (rootArray == null)
- {
- throw new ArgumentNullException(nameof(rootArray));
- }
-
- if (depth == 0)
- {
- return;
- }
+ if (depth == 0) return;
for (var i = 0; i < elementsPerLevel; i++)
{
diff --git a/docs/RestServer/Addons.md b/docs/RestServer/Addons.md
new file mode 100644
index 0000000000..b71c8a6d95
--- /dev/null
+++ b/docs/RestServer/Addons.md
@@ -0,0 +1,108 @@
+## RestServer Plugin
+In this section of you will learn how to make a `neo-cli` plugin that integrates with `RestServer`
+plugin. Lets take a look at [Example Plugin](/examples/RestServerPlugin).
+
+- No reference to `RestServer` is required.
+- Requires DotNet 7.0
+
+## Folder Structure
+```bash
+Project
+├── Controllers
+│ └── ExampleController.cs
+├── ExamplePlugin.cs
+├── ExamplePlugin.csproj
+├── Exceptions
+│ └── CustomException.cs
+└── Models
+ └── ErrorModel.cs
+```
+The only thing that is important here is the `controllers` folder. This folder is required for the `RestServer`
+plugin to register the controllers in its web server. This location is where you put all your controllers.
+
+## Controllers
+The `controller` class is the same as ASP.Net Core's. Controllers must have their attribute set
+as `[ApiController]` and inherent from `ControllerBase`.
+
+## Swagger Controller
+A `Swagger` controller uses special attributes that are set on your controller's class.
+
+**Controller Class Attributes**
+- `[Produces(MediaTypeNames.Application.Json)]` (_Required_)
+- `[Consumes(MediaTypeNames.Application.Json)]` (_Required_)
+- `[ApiExplorerSettings(GroupName = "v1")]`
+ - **GroupName** - _is which version of the API you are targeting._
+- `[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorModel))]` (_Required_)
+ - **Type** - _Must have a base class of [error](#error-class)._
+
+## Error Class
+Needs to be the same as `RestServer` of else there will be some inconsistencies
+with end users not knowing which type to use. This class can be `public` or `internal`.
+Properties `Code`, `Name` and `Message` values can be whatever you desire.
+
+**Model**
+```csharp
+public class ErrorModel
+{
+ public int Code { get; set; };
+ public string Name { get; set; };
+ public string Message { get; set; };
+}
+```
+
+## Controller Actions
+Controller actions need to have special attributes as well as code comments.
+
+- `[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]`
+
+HTTP status code `200 (OK)` is required with return type defined. You can use more than one attribute. One per HTTP status code.
+
+### Action Example
+```csharp
+[HttpGet("contracts/{hash:required}/sayHello", Name = "GetSayHello")]
+[ProducesResponseType(StatusCodes.Status204NoContent)]
+[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]
+public IActionResult GetSayHello(
+ [FromRoute(Name = "hash")]
+ UInt160 scripthash)
+{
+ if (scripthash == UInt160.Zero)
+ return NoContent();
+ return Ok($"Hello, {scripthash}");
+}
+```
+Notice that the _above_ example also returns with HTTP status code of `204 No Content`.
+This action `route` also extends the `contracts` API. Adding method `sayHello`. Routes
+can be what you like as well. But if you want to extend on any existing controller you
+must use existing routes paths.
+
+### Path(s)
+- `/api/v1/contracts/`
+- `/api/v1/ledger/`
+- `/api/v1/node/`
+- `/api/v1/tokens`
+- `/api/v1/Utils/`
+
+### Excluded Path(s)
+- `/api/v1/wallet/`
+
+_for security reasons_.
+
+### Code Comments for Swagger
+```csharp
+///
+///
+///
+///
+///
+/// Successful
+/// An error occurred. See Response for details.
+```
+
+Also note that you need to have `GenerateDocumentationFile` enabled in your
+`.csproj` file. The `xml` file that is generated; in our case would be `RestServerPlugin.xml`.
+This file gets put in same directory `Plugins/RestServerPlugin/` which is in the root of `neo-node`
+executable folder. Where you will see `neo-cli.exe`.
+
+File `RestServerPlugin.xml` will get added to `Swagger` automatically by the `RestServer`
+plugin.
diff --git a/docs/RestServer/ConfigFile.md b/docs/RestServer/ConfigFile.md
new file mode 100644
index 0000000000..1c9df0e1ad
--- /dev/null
+++ b/docs/RestServer/ConfigFile.md
@@ -0,0 +1,54 @@
+## Table
+
+| Name | Type | Description |
+| :--- | :---: | :--- |
+|**Network**|_uint32_|_Network you would like the `RestServer` to be enabled on._|
+|**BindAddress**|_string_|_Ip address of the interface you want to bind too._|
+|**Port**|_uint32_|_Port number to bind too._|
+|**KeepAliveTimeout**|_uint32_|_Time to keep the request alive, in seconds._|
+|**SslCertFile**|_string_|_Is the path and file name of a certificate file, relative to the directory that contains the node's executable files._|
+|**SslCertPassword**|_string_|_Is the password required to access the `X.509` certificate data._|
+|**TrustedAuthorities**|_StringArray_|_Tumbprints of the of the last certificate authority in the chain._|
+|**EnableBasicAuthentication**|_boolean_|_enables basic authentication._|
+|**RestUser**|_string_|_Basic authentication's `username`._|
+|**RestPass**|_string_|_Basic authentication's `password`._|
+|**EnableCors**|_boolean_|_Enables Cross-origin resource sharing (`CORS`). Note by default it enables `*` any origin._|
+|**AllowOrigins**|_StringArray_|_A list of the origins to allow. Note needs to add origins for basic auth to work with `CORS`._|
+|**DisableControllers**|_StringArray_|_A list of `controllers` to be disabled. Requires restart of the node, if changed._|
+|**EnableCompression**|_boolean_|_Enables `GZip` data compression._|
+|**CompressionLevel**|_enum_|_Compression level. Values can be `Fastest`, `Optimal`, `NoCompression` or `SmallestSize`_|
+|**EnableForwardedHeaders**|_boolean_|_Enables response/request headers for proxy forwarding. (data center usage)_|
+|**EnableSwagger**|_boolean_|_Enables `Swagger` with `Swagger UI` for the rest services._|
+|**MaxPageSize**|_uint32_|_Max page size for searches on `Ledger`/`Contracts` route._|
+|**MaxConcurrentConnections**|_int64_|_Max allow concurrent HTTP connections._|
+|**MaxInvokeGas**|_int64_|_Max gas to be invoked on the `Neo` virtual machine._|
+
+## Default "Config.json" file
+```json
+{
+ "PluginConfiguration": {
+ "Network": 860833102,
+ "BindAddress": "127.0.0.1",
+ "Port": 10339,
+ "KeepAliveTimeout": 120,
+ "SslCertFile": "",
+ "SslCertPassword": "",
+ "TrustedAuthorities": [],
+ "EnableBasicAuthentication": false,
+ "RestUser": "",
+ "RestPass": "",
+ "EnableCors": true,
+ "AllowOrigins": [],
+ "DisableControllers": [ "WalletController" ],
+ "EnableCompression": true,
+ "CompressionLevel": "SmallestSize",
+ "EnableForwardedHeaders": false,
+ "EnableSwagger": true,
+ "MaxPageSize": 50,
+ "MaxConcurrentConnections": 40,
+ "MaxTransactionFee": 10000000,
+ "MaxInvokeGas": 20000000,
+ "WalletSessionTimeout": 120
+ }
+}
+```
diff --git a/docs/RestServer/README.md b/docs/RestServer/README.md
new file mode 100644
index 0000000000..9063babae4
--- /dev/null
+++ b/docs/RestServer/README.md
@@ -0,0 +1,144 @@
+## RestServer
+In this section you will learn about `RestServer` plugin and how it works.
+
+See [config.json](ConfigFile.md) for information about the configurations.
+
+## Dependencies
+- **Microsoft.AspNetCore.JsonPatch.dll** `Required`
+- **Microsoft.AspNetCore.Mvc.NewtonsoftJson.dll** `Required`
+- **Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.dll** `Required`
+- **Microsoft.AspNetCore.Mvc.Versioning.dll** `Required`
+- **Microsoft.OpenApi.dll** `Required`
+- **Newtonsoft.Json.Bson.dll** `Required`
+- **Newtonsoft.Json.dll** `Required`
+- **System.ServiceProcess.ServiceController.dll** `Required`
+- **Microsoft.AspNetCore.Mvc.Versioning.dll** `Required`
+- **Microsoft.AspNetCore.Mvc.Versioning.dll** `Required`
+- **Microsoft.AspNetCore.Mvc.Versioning.dll** `Required`
+- **Microsoft.OpenApi.dll** `Swagger`
+- **Swashbuckle.AspNetCore.Swagger.dll** `Swagger`
+- **Swashbuckle.AspNetCore.SwaggerGen.dll** `Swagger`
+- **Swashbuckle.AspNetCore.SwaggerUI.dll** `Swagger`
+- **Swashbuckle.AspNetCore.Newtonsoft.dll** `Swagger`
+- **RestServer.xml** `Swagger UI`
+
+These files go in the same directory as the `RestServer.dll`. In neo-cli
+`plugins/RestServer/` folder.
+
+## Response Headers
+| Name | Value(s) | Description |
+| :---: | --- | :--- |
+|**server**|_neo-cli/3.6.0 RestServer/3.6.0_|_`neo-cli` and `RestServer` version._|
+
+Custom headers can be added by [Neo RestServer Plugins](Addons.md).
+
+## JSON Serializer
+`RestServer` uses custom NewtonSoft JSON Converters to serialize controller action
+responses and `route` parameters.
+
+**One Way Binding** - `Write` only.
+- `Neo.SmartContract.ContractState`
+- `Neo.SmartContract.NefFile`
+- `Neo.SmartContract.MethodToken`
+- `Neo.SmartContract.Native.TrimmedBlock`
+- `Neo.SmartContract.Manifest.ContractAbi`
+- `Neo.SmartContract.Manifest.ContractGroup`
+- `Neo.SmartContract.Manifest.ContractManifest`
+- `Neo.SmartContract.Manifest.ContractPermission`
+- `Neo.SmartContract.Manifest.ContractPermissionDescriptor`
+- `Neo.Network.P2P.Payloads.Block`
+- `Neo.Network.P2P.Payloads.Header`
+- `Neo.Network.P2P.Payloads.Signer`
+- `Neo.Network.P2P.Payloads.TransactionAttribute`
+- `Neo.Network.P2P.Payloads.Transaction`
+- `Neo.Network.P2P.Payloads.Witness`
+
+**Two Way Binding** - `Read` & `Write`
+- `System.Guid`
+- `System.ReadOnlyMemory`
+- `Neo.BigDecimal`
+- `Neo.UInt160`
+- `Neo.UInt256`
+- `Neo.Cryptography.ECC.ECPoint`
+- `Neo.VM.Types.Array`
+- `Neo.VM.Types.Boolean`
+- `Neo.VM.Types.Buffer`
+- `Neo.VM.Types.ByteString`
+- `Neo.VM.Types.Integer`
+- `Neo.VM.Types.InteropInterface`
+- `Neo.VM.Types.Null`
+- `Neo.VM.Types.Map`
+- `Neo.VM.Types.Pointer`
+- `Neo.VM.Types.StackItem`
+- `Neo.VM.Types.Struct`
+
+## Remote Endpoints
+Parametes `{hash}` can be any Neo N3 address or scripthash; `{address}` can be any Neo N3 address **only**; `{number}` and `{index}` can be any _**uint32**_.
+
+**Parameter Examples**
+- `{hash}` - _0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5_ **or** _NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc_
+- `{address}` - _NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc_
+- `{number}` - _1_
+- `{index}` - _2500000_
+
+**Paths**
+- Utils
+ - `[GET]` `/api/v1/utils/{hash}/address`
+ - `[GET]` `/api/v1/utils/{address}/scripthash`
+ - `[GET]` `/api/v1/utils/{hash}/{address}/validate`
+- Node
+ - `[GET]` `/api/v1/node/peers`
+ - `[GET]` `/api/v1/node/plugins`
+ - `[GET]` `/api/v1/node/settings`
+- Ledger
+ - `[GET]` `/api/v1/ledger/neo/accounts`
+ - `[GET]` `/api/v1/ledger/gas/accounts`
+ - `[GET]` `/api/v1/ledger/blocks?page={number}&size={number}`
+ - `[GET]` `/api/v1/ledger/blocks/height`
+ - `[GET]` `/api/v1/ledger/blocks/{index}`
+ - `[GET]` `/api/v1/ledger/blocks/{index}/header`
+ - `[GET]` `/api/v1/ledger/blocks/{index}/witness`
+ - `[GET]` `/api/v1/ledger/blocks/{index}/transactions?page={number}&size={number}`
+ - `[GET]` `/api/v1/ledger/transactions/{hash}`
+ - `[GET]` `/api/v1/ledger/transactions/{hash}/witnesses`
+ - `[GET]` `/api/v1/ledger/transactions/{hash}/signers`
+ - `[GET]` `/api/v1/ledger/transactions/{hash}/atributes`
+ - `[GET]` `/api/v1/ledger/memorypool?page={number}&size={number}`
+ - `[GET]` `/api/v1/ledger/memorypool/verified?page={number}&size={number}`
+ - `[GET]` `/api/v1/ledger/memorypool/unverified?page={number}&size={number}`
+ - `[GET]` `/api/v1/ledger/memorypool/count`
+- Tokens
+ - `[GET]` `/api/v1/tokens/balanceof/{address}`
+ - NFTs
+ - `[GET]` `/api/v1/tokens/nep-11?page={number}&size={number}`
+ - `[GET]` `/api/v1/tokens/nep-11/count`
+ - `[GET]` `/api/v1/tokens/nep-11/{hash}/balanceof/{address}`
+ - NEP-17
+ - `[GET]` `/api/v1/tokens/nep-17?page={number}&size={number}`
+ - `[GET]` `/api/v1/tokens/nep-17/count`
+ - `[GET]` `/api/v1/tokens/nep-17/{hash}/balanceof/{address}`
+- Contracts
+ - `[GET]` `/api/v1/contracts?page={number}&size={number}`
+ - `[GET]` `/api/v1/contracts/count`
+ - `[GET]` `/api/v1/contracts/{hash}`
+ - `[GET]` `/api/v1/contracts/{hash}/abi`
+ - `[GET]` `/api/v1/contracts/{hash}/manifest`
+ - `[GET]` `/api/v1/contracts/{hash}/nef`
+ - `[GET]` `/api/v1/contracts/{hash}/storage`
+- Wallet
+ - `[POST]` `/api/v1/wallet/open`
+ - `[POST]` `/api/v1/wallet/create`
+ - `[POST]` `/api/v1/wallet/{session}/address/create`
+ - `[GET]` `/api/v1/wallet/{session}/address/list`
+ - `[GET]` `/api/v1/wallet/{session}/asset/list`
+ - `[GET]` `/api/v1/wallet/{session}/balance/list`
+ - `[POST]` `/api/v1/wallet/{session}/changepassword`
+ - `[GET]` `/api/v1/wallet/{session}/close`
+ - `[GET]` `/api/v1/wallet/{session}/delete/{address}`
+ - `[GET]` `/api/v1/wallet/{session}/export/{address}`
+ - `[GET]` `/api/v1/wallet/{session}/export`
+ - `[GET]` `/api/v1/wallet/{session}/gas/unclaimed`
+ - `[GET]` `/api/v1/wallet/{session}/key/list`
+ - `[POST]` `/api/v1/wallet/{session}/import`
+ - `[POST]` `/api/v1/wallet/{session}/import/multisigaddress`
+ - `[POST]` `/api/v1/wallet/{session}/transfer`
diff --git a/docs/RestServer/RateLimiting.md b/docs/RestServer/RateLimiting.md
new file mode 100644
index 0000000000..56bfd8d099
--- /dev/null
+++ b/docs/RestServer/RateLimiting.md
@@ -0,0 +1,60 @@
+# Rate Limiting in Neo REST Server
+
+## Overview
+
+The rate limiting feature in the Neo REST Server plugin provides protection against abuse by limiting the number of requests a client can make in a given time period. This helps maintain stability, security, and performance of the REST API.
+
+## Configuration
+
+Rate limiting can be configured in the `RestServer.json` file with the following options:
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `EnableRateLimiting` | boolean | Enables or disables rate limiting |
+| `RateLimitPermitLimit` | integer | Maximum number of requests allowed in the specified time window |
+| `RateLimitWindowSeconds` | integer | The time window in seconds for rate limiting |
+| `RateLimitQueueLimit` | integer | Number of requests to queue when limit is exceeded (0 to disable queuing) |
+
+## Default Configuration
+
+```json
+{
+ "EnableRateLimiting": true,
+ "RateLimitPermitLimit": 10,
+ "RateLimitWindowSeconds": 60,
+ "RateLimitQueueLimit": 0
+}
+```
+
+By default, the configuration allows 10 requests per minute per IP address.
+
+## How It Works
+
+The REST Server uses ASP.NET Core's built-in rate limiting middleware (`Microsoft.AspNetCore.RateLimiting`) to implement a fixed window rate limiter. This means:
+
+1. Requests are tracked based on the client's IP address
+2. A fixed time window (configured by `RateLimitWindowSeconds`) determines the period for counting requests
+3. When the limit is reached, clients receive a 429 (Too Many Requests) response with a Retry-After header
+
+## Response Format
+
+When a client exceeds the rate limit, they receive:
+
+- HTTP Status: 429 Too Many Requests
+- Header: `Retry-After: [seconds]`
+- Body: Error message indicating when they can try again
+
+## Use Cases
+
+Rate limiting is particularly useful for:
+
+1. **Preventing API Abuse**: Limits the number of requests a user or client can make
+2. **Ensuring Fair Usage**: Prevents individual clients from monopolizing server resources
+3. **Protecting Resources**: Controls the number of requests to prevent server overload
+4. **Enhancing Security**: Helps mitigate certain types of denial of service attacks
+
+## Important Notes
+
+- Rate limiting is applied at the IP address level and affects all endpoints
+- If your application has legitimate high-volume needs, consider adjusting the limits accordingly
+- For applications with multiple clients behind a single IP (e.g., corporate proxies), consider implementing your own rate limiting logic that takes into account application-specific identifiers
\ No newline at end of file
diff --git a/docs/RestServer/RateLimitingUsage.md b/docs/RestServer/RateLimitingUsage.md
new file mode 100644
index 0000000000..35568a4220
--- /dev/null
+++ b/docs/RestServer/RateLimitingUsage.md
@@ -0,0 +1,101 @@
+# Using Rate Limiting in Controllers
+
+## Overview
+
+This document explains how to use rate limiting at the controller and endpoint level in the Neo REST Server. Rate limiting prevents abuse by limiting the number of requests a client can make to your API within a specified time window.
+
+## Prerequisites
+
+Before using controller-level rate limiting, ensure:
+
+1. Rate limiting is enabled in `RestServer.json`:
+ ```json
+ {
+ "EnableRateLimiting": true,
+ "RateLimitPermitLimit": 10,
+ "RateLimitWindowSeconds": 60,
+ "RateLimitQueueLimit": 0
+ }
+ ```
+
+2. The necessary imports are added to your controller:
+ ```csharp
+ using Microsoft.AspNetCore.RateLimiting;
+ ```
+
+## Global Rate Limiting vs. Controller-level Rate Limiting
+
+The REST Server supports two levels of rate limiting:
+
+1. **Global Rate Limiting**: Applies to all endpoints by default when enabled in the configuration.
+2. **Controller/Endpoint Rate Limiting**: Apply specific rate limiting policies to controllers or endpoints.
+
+## Rate Limiting Attributes
+
+### EnableRateLimiting
+
+Apply rate limiting to a controller or specific endpoint:
+
+```csharp
+[EnableRateLimiting("policyName")]
+```
+
+### DisableRateLimiting
+
+Disable rate limiting for a controller or specific endpoint:
+
+```csharp
+[DisableRateLimiting]
+```
+
+## Example Controller
+
+```csharp
+[ApiController]
+[Route("api/[controller]")]
+[EnableRateLimiting("strict")] // Apply strict rate limiting to the entire controller
+public class ExampleController : ControllerBase
+{
+ [HttpGet]
+ public IActionResult Get()
+ {
+ return Ok("This endpoint uses the strict rate limiting policy");
+ }
+
+ [HttpGet("unlimited")]
+ [DisableRateLimiting] // Disable rate limiting for this specific endpoint
+ public IActionResult GetUnlimited()
+ {
+ return Ok("This endpoint has no rate limiting");
+ }
+
+ [HttpGet("custom")]
+ [EnableRateLimiting("custom")] // Apply a different policy to this endpoint
+ public IActionResult GetCustom()
+ {
+ return Ok("This endpoint uses a custom rate limiting policy");
+ }
+}
+```
+
+## Rate Limiting Behavior
+
+When rate limiting is applied to a controller or endpoint, the following behaviors occur:
+
+1. When the rate limit is reached, clients receive a `429 Too Many Requests` response.
+2. The response includes a `Retry-After` header indicating when to retry.
+3. The response body contains an error message explaining the rate limit.
+
+## Priority of Rate Limiting Policies
+
+Rate limiting policies are applied in the following order of precedence:
+
+1. Endpoint-specific attributes (`[EnableRateLimiting]` or `[DisableRateLimiting]`)
+2. Controller-level attributes
+3. Global rate limiting configuration
+
+## Important Notes
+
+- `[DisableRateLimiting]` will disable rate limiting for a controller or endpoint regardless of parent policies.
+- When applying `[EnableRateLimiting]` with a named policy, ensure the policy is defined in the rate limiter configuration.
+- Controller-level rate limiting requires additional code in the `RestWebServer.cs` file.
\ No newline at end of file
diff --git a/src/Neo.Network.RpcClient/API_REFERENCE.md b/docs/RpcClient/rpclient-api-reference.md
similarity index 100%
rename from src/Neo.Network.RpcClient/API_REFERENCE.md
rename to docs/RpcClient/rpclient-api-reference.md
diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md
new file mode 100644
index 0000000000..d89d261bf3
--- /dev/null
+++ b/docs/native-contracts-api.md
@@ -0,0 +1,205 @@
+# Native Contracts API
+Native contracts are the contracts that are implemented in the Neo blockchain,
+and native contract APIsare the methods that are provided by the native contracts.
+
+When calling a native contract method by transaction script, there are several tips and notes:
+1. A part of native contract methods require CallFlags. If no such CallFlags is provided, the call will be failed.
+2. Some native contract methods are only allowed to be called before or after a certain hardfork.
+3. A native contract method may have different behaviors in different hardforks.
+
+## Table of Contents
+
+1. [ContractManagement](#contractmanagement)
+2. [StdLib](#stdlib)
+3. [CryptoLib](#cryptolib)
+4. [LedgerContract](#ledgercontract)
+5. [NeoToken](#neotoken)
+6. [GasToken](#gastoken)
+7. [PolicyContract](#policycontract)
+8. [RoleManagement](#rolemanagement)
+9. [OracleContract](#oraclecontract)
+10. [Notary](#notary)
+
+## ContractManagement
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| getMinimumDeploymentFee | Gets the minimum deployment fee for deploying a contract. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| setMinimumDeploymentFee | Sets the minimum deployment fee for deploying a contract. Only committee members can call this method. | BigInteger(*value*) | Void | 1<<15 | 0 | States | -- |
+| getContract | Gets the deployed contract with the specified hash. | UInt160(*hash*) | ContractState | 1<<15 | 0 | ReadStates | -- |
+| isContract | Check if exists the deployed contract with the specified hash. | UInt160(*hash*) | Boolean | 1<<14 | 0 | ReadStates | HF_Echidna |
+| getContractById | Maps specified ID to deployed contract. | Int32(*id*) | ContractState | 1<<15 | 0 | ReadStates | -- |
+| getContractHashes | Gets hashes of all non native deployed contracts. | -- | IIterator | 1<<15 | 0 | ReadStates | -- |
+| hasMethod | Check if a method exists in a contract. | UInt160(*hash*), String(*method*), Int32(*pcount*) | Boolean | 1<<15 | 0 | ReadStates | -- |
+| deploy | Deploys a contract. It needs to pay the deployment fee and storage fee. | Byte[](*nefFile*), Byte[](*manifest*) | ContractState | 0 | 0 | States,AllowNotify | -- |
+| deploy | Deploys a contract. It needs to pay the deployment fee and storage fee. | Byte[](*nefFile*), Byte[](*manifest*), StackItem(*data*) | ContractState | 0 | 0 | States,AllowNotify | -- |
+| update | Updates a contract. It needs to pay the storage fee. | Byte[](*nefFile*), Byte[](*manifest*) | Void | 0 | 0 | States,AllowNotify | -- |
+| update | Updates a contract. It needs to pay the storage fee. | Byte[](*nefFile*), Byte[](*manifest*), StackItem(*data*) | Void | 0 | 0 | States,AllowNotify | -- |
+| destroy | Destroys a contract. | -- | Void | 1<<15 | 0 | States,AllowNotify | -- |
+
+
+## StdLib
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| serialize | -- | StackItem(*item*) | Byte[] | 1<<12 | 0 | -- | -- |
+| deserialize | -- | Byte[](*data*) | StackItem | 1<<14 | 0 | -- | -- |
+| jsonSerialize | -- | StackItem(*item*) | Byte[] | 1<<12 | 0 | -- | -- |
+| jsonDeserialize | -- | Byte[](*json*) | StackItem | 1<<14 | 0 | -- | -- |
+| itoa | Converts an integer to a String. | BigInteger(*value*) | String | 1<<12 | 0 | -- | -- |
+| itoa | Converts an integer to a String. | BigInteger(*value*), Int32(*base*) | String | 1<<12 | 0 | -- | -- |
+| atoi | Converts a String to an integer. | String(*value*) | BigInteger | 1<<6 | 0 | -- | -- |
+| atoi | Converts a String to an integer. | String(*value*), Int32(*base*) | BigInteger | 1<<6 | 0 | -- | -- |
+| base64Encode | Encodes a byte array into a base64 String. | Byte[](*data*) | String | 1<<5 | 0 | -- | -- |
+| base64Decode | Decodes a byte array from a base64 String. | String(*s*) | Byte[] | 1<<5 | 0 | -- | -- |
+| base64UrlEncode | Encodes a byte array into a base64Url string. | String(*data*) | String | 1<<5 | 0 | -- | HF_Echidna |
+| base64UrlDecode | Decodes a byte array from a base64Url string. | String(*s*) | String | 1<<5 | 0 | -- | HF_Echidna |
+| base58Encode | Encodes a byte array into a base58 String. | Byte[](*data*) | String | 1<<13 | 0 | -- | -- |
+| base58Decode | Decodes a byte array from a base58 String. | String(*s*) | Byte[] | 1<<10 | 0 | -- | -- |
+| base58CheckEncode | Converts a byte array to its equivalent String representation that is encoded with base-58 digits. The encoded String contains the checksum of the binary data. | Byte[](*data*) | String | 1<<16 | 0 | -- | -- |
+| base58CheckDecode | Converts the specified String, which encodes binary data as base-58 digits, to an equivalent byte array. The encoded String contains the checksum of the binary data. | String(*s*) | Byte[] | 1<<16 | 0 | -- | -- |
+| hexEncode | -- | Byte[](*bytes*) | String | 1<<5 | 0 | -- | HF_Faun |
+| hexDecode | -- | String(*str*) | Byte[] | 1<<5 | 0 | -- | HF_Faun |
+| memoryCompare | -- | Byte[](*str1*), Byte[](*str2*) | Int32 | 1<<5 | 0 | -- | -- |
+| memorySearch | -- | Byte[](*mem*), Byte[](*value*) | Int32 | 1<<6 | 0 | -- | -- |
+| memorySearch | -- | Byte[](*mem*), Byte[](*value*), Int32(*start*) | Int32 | 1<<6 | 0 | -- | -- |
+| memorySearch | -- | Byte[](*mem*), Byte[](*value*), Int32(*start*), Boolean(*backward*) | Int32 | 1<<6 | 0 | -- | -- |
+| stringSplit | -- | String(*str*), String(*separator*) | String[] | 1<<8 | 0 | -- | -- |
+| stringSplit | -- | String(*str*), String(*separator*), Boolean(*removeEmptyEntries*) | String[] | 1<<8 | 0 | -- | -- |
+| strLen | -- | String(*str*) | Int32 | 1<<8 | 0 | -- | -- |
+
+
+## CryptoLib
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| bls12381Serialize | Serialize a bls12381 point. | InteropInterface(*g*) | Byte[] | 1<<19 | 0 | -- | -- |
+| bls12381Deserialize | Deserialize a bls12381 point. | Byte[](*data*) | InteropInterface | 1<<19 | 0 | -- | -- |
+| bls12381Equal | Determines whether the specified points are equal. | InteropInterface(*x*), InteropInterface(*y*) | Boolean | 1<<5 | 0 | -- | -- |
+| bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- |
+| bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- |
+| bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- |
+| recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna |
+| ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- |
+| sha256 | Computes the hash value for the specified byte array using the sha256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- |
+| murmur32 | Computes the hash value for the specified byte array using the murmur32 algorithm. | Byte[](*data*), UInt32(*seed*) | Byte[] | 1<<13 | 0 | -- | -- |
+| keccak256 | Computes the hash value for the specified byte array using the keccak256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | HF_Cockatrice |
+| verifyWithECDsa | Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curveHash*) | Boolean | 1<<15 | 0 | -- | HF_Cockatrice |
+| verifyWithECDsa | -- | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curve*) | Boolean | 1<<15 | 0 | -- | Deprecated in HF_Cockatrice |
+| verifyWithEd25519 | Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*) | Boolean | 1<<15 | 0 | -- | HF_Echidna |
+
+
+## LedgerContract
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| currentHash | Gets the hash of the current block. | -- | UInt256 | 1<<15 | 0 | ReadStates | -- |
+| currentIndex | Gets the index of the current block. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| getBlock | -- | Byte[](*indexOrHash*) | TrimmedBlock | 1<<15 | 0 | ReadStates | -- |
+| getTransaction | -- | UInt256(*hash*) | Transaction | 1<<15 | 0 | ReadStates | -- |
+| getTransactionSigners | -- | UInt256(*hash*) | Signer[] | 1<<15 | 0 | ReadStates | -- |
+| getTransactionVMState | -- | UInt256(*hash*) | VMState | 1<<15 | 0 | ReadStates | -- |
+| getTransactionHeight | -- | UInt256(*hash*) | Int32 | 1<<15 | 0 | ReadStates | -- |
+| getTransactionFromBlock | -- | Byte[](*blockIndexOrHash*), Int32(*txIndex*) | Transaction | 1<<16 | 0 | ReadStates | -- |
+
+
+## NeoToken
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| totalSupply | -- | -- | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| setGasPerBlock | Sets the amount of GAS generated in each block. Only committee members can call this method. | BigInteger(*gasPerBlock*) | Void | 1<<15 | 0 | States | -- |
+| getGasPerBlock | Gets the amount of GAS generated in each block. | -- | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| setRegisterPrice | Sets the fees to be paid to register as a candidate. Only committee members can call this method. | Int64(*registerPrice*) | Void | 1<<15 | 0 | States | -- |
+| getRegisterPrice | Gets the fees to be paid to register as a candidate. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| unclaimedGas | Get the amount of unclaimed GAS in the specified account. | UInt160(*account*), UInt32(*end*) | BigInteger | 1<<17 | 0 | ReadStates | -- |
+| onNEP17Payment | Handles the payment of GAS. | UInt160(*from*), BigInteger(*amount*), StackItem(*data*) | Void | 0 | 0 | States,AllowNotify | HF_Echidna |
+| registerCandidate | Registers a candidate. | ECPoint(*pubkey*) | Boolean | 0 | 0 | States | Deprecated in HF_Echidna |
+| registerCandidate | Registers a candidate. | ECPoint(*pubkey*) | Boolean | 0 | 0 | States,AllowNotify | HF_Echidna |
+| unregisterCandidate | Unregisters a candidate. | ECPoint(*pubkey*) | Boolean | 1<<16 | 0 | States | Deprecated in HF_Echidna |
+| unregisterCandidate | Unregisters a candidate. | ECPoint(*pubkey*) | Boolean | 1<<16 | 0 | States,AllowNotify | HF_Echidna |
+| vote | Votes for a candidate. | UInt160(*account*), ECPoint(*voteTo*) | Boolean | 1<<16 | 0 | States | Deprecated in HF_Echidna |
+| vote | Votes for a candidate. | UInt160(*account*), ECPoint(*voteTo*) | Boolean | 1<<16 | 0 | States,AllowNotify | HF_Echidna |
+| getCandidates | Gets the first 256 registered candidates. | -- | ValueTuple`2[] | 1<<22 | 0 | ReadStates | -- |
+| getAllCandidates | Gets the registered candidates iterator. | -- | IIterator | 1<<22 | 0 | ReadStates | -- |
+| getCandidateVote | Gets votes from specific candidate. | ECPoint(*pubKey*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| getCommittee | Gets all the members of the committee. | -- | ECPoint[] | 1<<16 | 0 | ReadStates | -- |
+| getAccountState | Get account state. | UInt160(*account*) | NeoAccountState | 1<<15 | 0 | ReadStates | -- |
+| getCommitteeAddress | Gets the address of the committee. | -- | UInt160 | 1<<16 | 0 | ReadStates | HF_Cockatrice |
+| getNextBlockValidators | Gets the validators of the next block. | -- | ECPoint[] | 1<<16 | 0 | ReadStates | -- |
+| balanceOf | Gets the balance of the specified account. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| transfer | -- | UInt160(*from*), UInt160(*to*), BigInteger(*amount*), StackItem(*data*) | Boolean | 1<<17 | 50 | All | -- |
+| symbol | -- | -- | String | 0 | 0 | -- | -- |
+| decimals | -- | -- | Byte | 0 | 0 | -- | -- |
+
+
+## GasToken
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| totalSupply | Gets the total supply of the token. | -- | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| balanceOf | Gets the balance of the specified account. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| transfer | -- | UInt160(*from*), UInt160(*to*), BigInteger(*amount*), StackItem(*data*) | Boolean | 1<<17 | 50 | All | -- |
+| symbol | -- | -- | String | 0 | 0 | -- | -- |
+| decimals | -- | -- | Byte | 0 | 0 | -- | -- |
+
+
+## PolicyContract
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| getFeePerByte | Gets the network fee per transaction byte. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| getExecFeeFactor | Gets the execution fee factor. This is a multiplier that can be adjusted by the committee to adjust the system fees for transactions. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| getStoragePrice | Gets the storage price. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| getMillisecondsPerBlock | Gets the block generation time in milliseconds. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| getMaxValidUntilBlockIncrement | Gets the upper increment size of blockchain height (in blocks) exceeding that a transaction should fail validation. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| getMaxTraceableBlocks | Gets the length of the chain accessible to smart contracts. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| getAttributeFee | Gets the fee for attribute before Echidna hardfork. NotaryAssisted attribute type not supported. | Byte(*attributeType*) | UInt32 | 1<<15 | 0 | ReadStates | Deprecated in HF_Echidna |
+| getAttributeFee | Gets the fee for attribute after Echidna hardfork. NotaryAssisted attribute type supported. | Byte(*attributeType*) | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| isBlocked | Determines whether the specified account is blocked. | UInt160(*account*) | Boolean | 1<<15 | 0 | ReadStates | -- |
+| setMillisecondsPerBlock | Sets the block generation time in milliseconds. | UInt32(*value*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Echidna |
+| setAttributeFee | Sets the fee for attribute before Echidna hardfork. NotaryAssisted attribute type not supported. | Byte(*attributeType*), UInt32(*value*) | Void | 1<<15 | 0 | States | Deprecated in HF_Echidna |
+| setAttributeFee | Sets the fee for attribute after Echidna hardfork. NotaryAssisted attribute type supported. | Byte(*attributeType*), UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna |
+| setFeePerByte | -- | Int64(*value*) | Void | 1<<15 | 0 | States | -- |
+| setExecFeeFactor | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | -- |
+| setStoragePrice | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | -- |
+| setMaxValidUntilBlockIncrement | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna |
+| setMaxTraceableBlocks | Sets the length of the chain accessible to smart contracts. | UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna |
+| blockAccount | -- | UInt160(*account*) | Boolean | 1<<15 | 0 | States | -- |
+| unblockAccount | -- | UInt160(*account*) | Boolean | 1<<15 | 0 | States | -- |
+| getBlockedAccounts | -- | -- | StorageIterator | 1<<15 | 0 | ReadStates | HF_Faun |
+
+
+## RoleManagement
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| getDesignatedByRole | Gets the list of nodes for the specified role. | Role(*role*), UInt32(*index*) | ECPoint[] | 1<<15 | 0 | ReadStates | -- |
+| designateAsRole | -- | Role(*role*), ECPoint[](*nodes*) | Void | 1<<15 | 0 | States,AllowNotify | -- |
+
+
+## OracleContract
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| setPrice | Sets the price for an Oracle request. Only committee members can call this method. | Int64(*price*) | Void | 1<<15 | 0 | States | -- |
+| getPrice | Gets the price for an Oracle request. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| finish | Finishes an Oracle response. | -- | Void | 0 | 0 | All | -- |
+| request | -- | String(*url*), String(*filter*), String(*callback*), StackItem(*userData*), Int64(*gasForResponse*) | Void | 0 | 0 | States,AllowNotify | -- |
+| verify | -- | -- | Boolean | 1<<15 | 0 | -- | -- |
+
+
+## Notary
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| verify | Verify checks whether the transaction is signed by one of the notaries and ensures whether deposited amount of GAS is enough to pay the actual sender's fee. | Byte[](*signature*) | Boolean | 1<<15 | 0 | ReadStates | -- |
+| onNEP17Payment | OnNEP17Payment is a callback that accepts GAS transfer as Notary deposit. It also sets the deposit's lock height after which deposit can be withdrawn. | UInt160(*from*), BigInteger(*amount*), StackItem(*data*) | Void | 1<<15 | 0 | States | -- |
+| lockDepositUntil | Lock asset until the specified height is unlocked. | UInt160(*account*), UInt32(*till*) | Boolean | 1<<15 | 0 | States | -- |
+| expirationOf | ExpirationOf returns deposit lock height for specified address. | UInt160(*account*) | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| balanceOf | BalanceOf returns deposited GAS amount for specified address. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| withdraw | Withdraw sends all deposited GAS for "from" address to "to" address. If "to" address is not specified, then "from" will be used as a sender. | UInt160(*from*), UInt160(*to*) | Boolean | 1<<15 | 0 | All | -- |
+| getMaxNotValidBeforeDelta | GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| setMaxNotValidBeforeDelta | SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. | UInt32(*value*) | Void | 1<<15 | 0 | States | -- |
+
+
diff --git a/docs/persistence-architecture.md b/docs/persistence-architecture.md
new file mode 100644
index 0000000000..153ebee0d9
--- /dev/null
+++ b/docs/persistence-architecture.md
@@ -0,0 +1,122 @@
+# Neo Persistence System - Class Relationships
+
+## Interface Hierarchy
+
+```
+IDisposable
+ │
+ ├── IReadOnlyStore
+ ├── IWriteStore
+ └── IStoreProvider
+ │
+ ▼
+ IStore ◄─── IStoreSnapshot
+```
+
+## Class Structure
+
+```
+StoreFactory
+ │
+ ├── MemoryStoreProvider ──creates──> MemoryStore
+ ├── LevelDBStore (Plugin) ──creates──> LevelDBStore
+ └── RocksDBStore (Plugin) ──creates──> RocksDBStore
+ │
+ ▼
+ IStoreSnapshot
+ │
+ ▼
+ Cache Layer
+ │
+ ┌─────────┼─────────┐
+ │ │ │
+ DataCache StoreCache ClonedCache
+```
+
+## Interface Definitions
+
+### IStore
+```csharp
+public interface IStore : IReadOnlyStore, IWriteStore, IDisposable
+{
+ IStoreSnapshot GetSnapshot();
+}
+```
+
+### IStoreSnapshot
+```csharp
+public interface IStoreSnapshot : IReadOnlyStore, IWriteStore, IDisposable
+{
+ IStore Store { get; }
+ void Commit();
+}
+```
+
+### IReadOnlyStore
+```csharp
+public interface IReadOnlyStore where TKey : class?
+{
+ TValue this[TKey key] { get; }
+ bool TryGet(TKey key, out TValue? value);
+ bool Contains(TKey key);
+ IEnumerable<(TKey Key, TValue Value)> Find(TKey? key_prefix = null, SeekDirection direction = SeekDirection.Forward);
+}
+```
+
+### IWriteStore
+```csharp
+public interface IWriteStore
+{
+ void Delete(TKey key);
+ void Put(TKey key, TValue value);
+ void PutSync(TKey key, TValue value) => Put(key, value);
+}
+```
+
+### IStoreProvider
+```csharp
+public interface IStoreProvider
+{
+ string Name { get; }
+ IStore GetStore(string path);
+}
+```
+
+## Core Classes
+
+### StoreFactory
+- Static registry for storage providers
+- Manages provider registration and discovery
+- Creates store instances
+
+## Cache System
+
+### Why Three Cache Classes?
+
+The Neo persistence system uses three cache classes to separate different responsibilities:
+
+1. **DataCache** - Provides common caching infrastructure and change tracking
+2. **StoreCache** - Connects cache to actual storage (database/memory)
+3. **ClonedCache** - Creates isolated copies to prevent data corruption
+
+### Relationships
+
+```
+DataCache (Abstract)
+ │
+ ├── StoreCache ──connects to──> IStore/IStoreSnapshot
+ └── ClonedCache ──wraps──> Any DataCache
+```
+
+### When to Use Each
+
+**StoreCache**:
+- Direct access to storage
+- When you need to read/write to database
+- Base layer for other caches
+
+**ClonedCache**:
+- When you need isolated data manipulation
+- Preventing accidental mutations between components
+- Creating temporary working environments
+- Smart contract execution (isolated from main state)
diff --git a/docs/plugin-rpc-server.md b/docs/plugin-rpc-server.md
new file mode 100644
index 0000000000..4c5784f2b7
--- /dev/null
+++ b/docs/plugin-rpc-server.md
@@ -0,0 +1,2036 @@
+# Plugin RpcServer Documentation
+
+This document provides a comprehensive reference for the plugin RpcServer.
+Including how to enable RPC server, and RPC method definitions from RpcServer plugin and other plugins.
+
+## Table of Contents
+
+1. [Get Started](#get-started)
+1. [Node Methods](#node-methods)
+2. [Blockchain Methods](#blockchain-methods)
+3. [Smart Contract Methods](#smart-contract-methods)
+4. [Wallet Methods](#wallet-methods)
+5. [Utility Methods](#utility-methods)
+6. [RpcMethods from other Plugins](#rpcmethods-from-other-plugins)
+
+---
+
+## Get Started
+
+### Install by `neo-cli`
+
+1. **Start the `neo-cli`**: Just run `neo-cli` in the terminal.
+2. **Download the Plugin**: Run `help install` to get help about how to install plugin.
+3. **Configure the Plugin**: Create or modify the `RpcServer.json` configuration file in the `neo-cli` binary directory (`Plugins/RpcServer`) if needed.
+If want to use RPC methods from other plugins, need to enable the plugin first.
+
+
+### Compile Manually
+
+1. **Clone the Repository**:
+```bash
+git clone https://github.com/neo-project/neo.git
+cd neo
+dotnet build
+```
+2. **Copy to `neo-cli` folder**: Copy the built plugin to the `neo-cli` binary directory.
+3. **Create a `RpcServer.json` file**: Create or Copy the `RpcServer.json` file in `Plugins/RpcServer` directory according to the next section.
+4. **Start the `neo-cli`**: Start/Restart `neo-cli` if needed.
+
+
+### Configuration
+
+Create or Copy the `RpcServer.json` file in `Plugins/RpcServer` directory:
+
+```json
+{
+ "PluginConfiguration": {
+ "UnhandledExceptionPolicy": "Ignore", // The unhandled exception policy, the default value is "Ignore"
+ "Servers": [
+ {
+ "Network": 860833102, // The network ID
+ "BindAddress": "127.0.0.1", // The bind address, 127.0.0.1 is the default value and for security reasons.
+ "Port": 10332, // The listening port
+ "SslCert": "", // The SSL certificate, if want to use SSL, need to set the SSL certificate
+ "SslCertPassword": "", // The SSL certificate password, if want to use SSL, you can set the password
+ "TrustedAuthorities": [], // The trusted authorities, and if set, the RPC server will verify the certificate of the client
+ "RpcUser": "", // The RPC user, if want to verify the RPC user and password, need to set the user
+ "RpcPass": "", // The RPC password, if want to verify the RPC user and password, need to set the password
+ "EnableCors": true, // Whether to enable CORS, if want to use CORS, need to set to true
+ "AllowOrigins": [], // The allowed origins, if want to use CORS, need to set the allowed origins
+ "KeepAliveTimeout": 60, // The keep alive timeout, the default value is 60 seconds
+ "RequestHeadersTimeout": 15, // The request headers timeout, the default value is 15 seconds
+ "MaxGasInvoke": 20, // The maximum gas invoke, the default value is 20 GAS
+ "MaxFee": 0.1, // The maximum fee, the default value is 0.1 GAS
+ "MaxConcurrentConnections": 40, // The maximum concurrent connections, the default value is 40
+ "MaxIteratorResultItems": 100, // The maximum iterator result items, the default value is 100
+ "MaxStackSize": 65535, // The maximum stack size, the default value is 65535
+ "DisabledMethods": [ "openwallet" ], // The disabled methods, the default value is [ "openwallet" ]
+ "SessionEnabled": false, // Whether to enable session, the default value is false
+ "SessionExpirationTime": 60, // The session expiration time, the default value is 60 seconds
+ "FindStoragePageSize": 50 // The find storage page size, the default value is 50
+ }
+ ]
+ }
+}
+```
+
+## Node Methods
+
+### getconnectioncount
+Gets the number of connections to the node.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getconnectioncount"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": 10 // The connected peers count
+}
+```
+
+### getpeers
+Gets information about the peers connected to the node.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getpeers"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "unconnected": [
+ {"address": "The peer IP address", "port": "The port"}
+ ],
+ "bad": [],
+ "connected": [
+ {"address": "The peer IP address", "port": "The port"}
+ ]
+ }
+}
+```
+
+### getversion
+Gets version information about the node, including network, protocol, and RPC settings.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getversion"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "tcpport": 10333,
+ "nonce": 1,
+ "useragent": "The user agent",
+ "rpc": {
+ "maxiteratorresultitems": 100, // The maximum number of items in the iterator result
+ "sessionenabled": false // Whether the session is enabled
+ },
+ "protocol": {
+ "addressversion": 0x35, // The address version
+ "network": 5195086, // The network ID
+ "validatorscount": 0, // The number of validators
+ "msperblock": 15000, // The number of milliseconds per block
+ "maxtraceableblocks": 2102400, // The maximum number of traceable blocks
+ "maxvaliduntilblockincrement": 5760, // The maximum valid until block increment
+ "maxtransactionsperblock": 512, // The maximum number of transactions per block
+ "memorypoolmaxtransactions": 50000, // The maximum number of transactions in the memory pool
+ "initialgasdistribution": 5200000000000000, // The initial gas distribution
+ "hardforks": [
+ {"name": "The hardfork name", "blockheight": 0} // The hardfork name and the block height
+ ],
+ "standbycommittee": ["The public key"], // The public keys of the standby committee
+ "seedlist": ["The seed 'host:port' list"]
+ }
+ }
+}
+```
+
+### sendrawtransaction
+Sends a raw transaction to the network.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "sendrawtransaction",
+ "params": ["A Base64 encoded transaction"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {"hash": "The hash of the transaction(UInt256)"}
+}
+```
+
+### submitblock
+Submits a new block to the network.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "submitblock",
+ "params": ["A Base64 encoded block"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {"hash": "The hash of the block(UInt256)"}
+}
+```
+
+---
+
+## Blockchain Methods
+
+### getbestblockhash
+Gets the hash of the best (most recent) block.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getbestblockhash"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The block hash(UInt256)"
+}
+```
+
+### getblock
+Gets a block by its hash or index.
+
+**Request with block hash:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblock",
+ "params": ["The block hash(UInt256)"]
+}
+```
+
+**Request with block index:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblock",
+ "params": [100] // The block index
+}
+```
+
+**Request with block hash and verbose:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblock",
+ "params": ["The block hash(UInt256)", true] // The block hash and verbose is true
+}
+```
+
+**Response (verbose=false):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "A base64-encoded string of the block"
+}
+```
+
+**Response (verbose=true):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The block hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "size": 697, // The size of the block
+ "version": 0, // The version of the block
+ "previousblockhash": "The previous block hash(UInt256)",
+ "merkleroot": "The merkle root(UInt256)",
+ "time": 1627896461306, // The time of the block, unix timestamp in milliseconds
+ "nonce": "09D4422954577BCE", // The nonce of the block
+ "index": 100, // The index of the block
+ "primary": 2, // The primary of the block
+ "nextconsensus": "The Base58Check encoded next consensus address",
+ "witnesses": [
+ {"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}
+ ],
+ "tx": [], // The transactions in the block
+ "confirmations": 200, // The number of confirmations of the block
+ "nextblockhash": "The next block hash(UInt256)" // The hash of the next block
+ }
+}
+```
+
+### getblockheadercount
+Gets the number of block headers in the blockchain.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblockheadercount"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": 100 // The number of block headers in the blockchain
+}
+```
+
+### getblockcount
+Gets the number of blocks in the blockchain.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblockcount"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": 100 // The number of blocks in the blockchain
+}
+```
+
+### getblockhash
+Gets the hash of the block at the specified height.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblockhash",
+ "params": [100] // The block index
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The block hash(UInt256)" // The hash of the block at the specified height
+}
+```
+
+### getblockheader
+Gets a block header by its hash or index.
+
+**Request with block hash:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblockheader",
+ "params": ["The block hash(UInt256)"]
+}
+```
+
+**Request with block index:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblockheader",
+ "params": [100] // The block index
+}
+```
+
+**Request with block index and verbose:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getblockheader",
+ "params": [100, true] // The block index and verbose is true
+}
+```
+
+**Response (verbose=false):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "A base64-encoded string of the block header"
+}
+```
+
+**Response (verbose=true):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The block hash(UInt256)",
+ "size": 696, // The size of the block header
+ "version": 0, // The version of the block header
+ "previousblockhash": "The previous block hash(UInt256)", // The hash of the previous block
+ "merkleroot": "The merkle root(UInt256)", // The merkle root of the block header
+ "time": 1627896461306, // The time of the block header, unix timestamp in milliseconds
+ "nonce": "09D4422954577BCE", // The nonce of the block header
+ "index": 100, // The index of the block header
+ "primary": 2, // The primary of the block header
+ "nextconsensus": "The Base58Check-encoded next consensus address", // The Base58Check-encoded next consensus address
+ "witnesses": [
+ {"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}
+ ],
+ "confirmations": 200, // The number of confirmations of the block header
+ "nextblockhash": "The next block hash(UInt256)" // The hash of the next block
+ }
+}
+```
+
+### getcontractstate
+Gets the contract state by contract name, script hash, or ID.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getcontractstate",
+ "params": ["Contract name, script hash, or the native contract id"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "A json string of the contract state"
+}
+```
+
+### getrawmempool
+Gets the current memory pool transactions.
+
+**Request (shouldGetUnverified=false):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getrawmempool",
+ "params": [false] // The shouldGetUnverified is false
+}
+```
+
+**Request (shouldGetUnverified=true):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getrawmempool",
+ "params": [true] // The shouldGetUnverified is true
+}
+```
+
+**Response (shouldGetUnverified=false):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "verified": ["The tx hash(UInt256)"], // The verified transactions
+ }
+}
+```
+
+**Response (shouldGetUnverified=true):**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "height": 100, // The height of the block
+ "verified": ["The tx hash(UInt256)"], // The verified transactions
+ "unverified": ["The tx hash(UInt256)"] // The unverified transactions
+ }
+}
+```
+
+### getrawtransaction
+Gets a transaction by its hash.
+
+**Request (verbose=true):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getrawtransaction",
+ "params": ["The tx hash", true]
+}
+```
+
+**Response (verbose=false):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The Base64 encoded tx data"
+}
+```
+
+**Response (verbose=true):**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The tx hash(UInt256)", // The hash of the transaction
+ "size": 272, // The size of the transaction
+ "version": 0, // The version of the transaction
+ "nonce": 1553700339, // The nonce of the transaction
+ "sender": "The Base58Check encoded sender address", // The Base58Check-encoded sender address
+ "sysfee": "100000000", // The system fee of the transaction
+ "netfee": "1272390", // The network fee of the transaction
+ "validuntilblock": 2105487, // The valid until block of the transaction
+ "attributes": [], // The attributes of the transaction
+ "signers": [], // The signers of the transaction
+ "script": "A Base64 encoded string", // The script of the transaction
+ "witnesses": [
+ {"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}
+ ],
+ "confirmations": 100, // The number of confirmations of the transaction
+ "blockhash": "The block hash(UInt256)", // The hash of the block
+ "blocktime": 1627896461306 // The time of the block, unix timestamp in milliseconds
+ }
+}
+```
+
+### getstorage
+Gets the storage item by contract ID or script hash and key.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getstorage",
+ "params": ["The contract id(int), hash(UInt160), or native contract name(string)", "The Base64 encoded key"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The Base64 encoded storage value"
+}
+```
+
+### findstorage
+Lists storage items by contract ID or script hash and prefix.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "findstorage",
+ "params": [
+ "The contract id(int), hash(UInt160), or native contract name(string)",
+ "The Base64 encoded key prefix", // The Base64 encoded key prefix
+ 0 // The start index, optional
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "truncated": true, // Whether the results are truncated
+ "next": 100, // The next index
+ "results": [
+ {"key": "The Base64 encoded storage key", "value": "The Base64 encoded storage value"}, // The storage item
+ {"key": "The Base64 encoded storage key", "value": "The Base64 encoded storage value"} // The storage item
+ // ...
+ ]
+ }
+}
+```
+
+### gettransactionheight
+Gets the height of a transaction by its hash.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "gettransactionheight",
+ "params": ["The tx hash(UInt256)"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": 100 // The height of the transaction
+}
+```
+
+### getnextblockvalidators
+Gets the next block validators.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnextblockvalidators"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": [
+ {
+ "publickey": "The public key", // The public key of the validator
+ "votes": 100 // The votes of the validator
+ }
+ // ...
+ ]
+}
+```
+
+### getcandidates
+Gets the list of candidates for the next block validators.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getcandidates"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": [
+ {
+ "publickey": "The public key", // The public key of the candidate
+ "votes": 100, // The votes of the candidate
+ "active": true // Is active or not
+ }
+ // ...
+ ]
+}
+```
+
+### getcommittee
+Gets the list of committee members.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getcommittee"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": ["The public key"] // The public keys of the committee
+}
+```
+
+### getnativecontracts
+Gets the list of native contracts.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnativecontracts"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": [
+ {
+ "id": -1, // The contract id
+ "updatecounter": 0, // The update counter
+ "hash": "The contract hash(UInt160)", // The contract hash
+ "nef": {
+ "magic": 0x3346454E, // The magic number, always 0x3346454E at present.
+ "compiler": "The compiler name",
+ "source": "The url of the source file",
+ "tokens": [
+ {
+ "hash": "The token hash(UInt160)",
+ "method": "The token method name",
+ "paramcount": 0, // The number of parameters
+ "hasreturnvalue": false, // Whether the method has a return value
+ "callflags": 0 // see CallFlags
+ } // A token in the contract
+ // ...
+ ],
+ "script": "The Base64 encoded script", // The Base64 encoded script
+ "checksum": 0x12345678 // The checksum
+ },
+ "manifest": {
+ "name": "The contract name",
+ "groups": [
+ {"pubkey": "The public key", "signature": "The signature"} // A group in the manifest
+ ],
+ "features": {}, // The features that the contract supports
+ "supportedstandards": ["The standard name"], // The standards that the contract supports
+ "abi": {
+ "methods": [
+ {
+ "name": "The method name",
+ "parameters": [
+ {"name": "The parameter name", "type": "The parameter type"} // A ContractParameter in the method
+ // ...
+ ],
+ "returntype": "The return type",
+ "offset": 0, // The offset in script of the method
+ "safe": false // Whether the method is safe
+ } // A method in the abi
+ // ...
+ ],
+ "events": [
+ {
+ "name": "The event name",
+ "parameters": [
+ {"name": "The parameter name", "type": "The parameter type"} // A ContractParameter in the event
+ // ...
+ ]
+ } // An event in the abi
+ // ...
+ ]
+ }, // The abi of the contract
+ "permissions": [
+ {
+ "contract": "The contract hash(UInt160), group(ECPoint), or '*'", // '*' means all contracts
+ "methods": ["The method name or '*'"] // '*' means all methods
+ } // A permission in the contract
+ // ...
+ ], // The permissions of the contract
+ "trusts": [
+ {
+ "contract": "The contract hash(UInt160), group(ECPoint), or '*'", // '*' means all contracts
+ "methods": ["The method name or '*'"] // '*' means all methods
+ } // A trust in the contract
+ // ...
+ ], // The trusts of the contract
+ "extra": {} // A json object, the extra content of the contract
+ } // The manifest of the contract
+ }
+ ]
+}
+```
+
+---
+
+## Smart Contract Methods
+
+### invokefunction
+Invokes a function on a smart contract.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "invokefunction",
+ "params": [
+ "The script hash(UInt160)",
+ "The operation to invoke as a string",
+ [
+ {
+ "type": "ContractParameterType", // The type of the parameter, see ContractParameterType
+ "value": "The parameter value" // The value of the parameter
+ } // A parameter in the operation
+ // ...
+ ], // The parameters of the operation, optional(can be null)
+ [
+ {
+ // The part of the Signer
+ "account": "An UInt160 or Base58Check address", // The account of the signer, required
+ "scopes": "WitnessScope", // The scopes of the signer, see WitnessScope, required
+ "allowedcontracts": ["The contract hash(UInt160)"], // The allowed contracts of the signer, optional
+ "allowedgroups": ["PublicKey"], // The allowed groups of the signer, optional
+ "rules": [
+ {
+ "action": "WitnessRuleAction", // The action of the witness rule, see WitnessRuleAction
+ "condition": { /* A json of WitnessCondition */ } // The condition of the witness rule, see WitnessCondition
+ } // A rule in the witness
+ // ...
+ ], // WitnessRule array, optional(can be null)
+
+ // The part of the Witness
+ "invocation": "A Base64 encoded string", // The invocation of the witness, optional
+ "verification": "A Base64 encoded string" // The verification of the witness, optional
+ }
+ ], // The signers and witnesses list, optional(can be null)
+ false // useDiagnostic, a bool value indicating whether to use diagnostic information, optional
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "script": "A Base64 encoded script",
+ "state": "A string of VMState", // see VMState
+ "gasconsumed": "An integer number in string", // The gas consumed
+ "exception": "The exception message", // The exception message
+ "stack": [
+ {"type": "The stack item type(StackItemType)", "value": "The stack item value"} // A StackItem in the stack
+ // ...
+ ],
+ "notifications": [
+ {
+ "eventname": "The event name", // The name of the event
+ "contract": "The contract hash", // The hash of the contract
+ "state": {"interface": "A string", "id": "The GUID string"} // The state of the event
+ }
+ ],
+ "diagnostics": {
+ "invokedcontracts": {"hash": "The contract hash", "call": [{"hash": "The contract hash"}]}, // The invoked contracts
+ "storagechanges": [
+ {
+ "state": "The TrackState string", // The type of the state, see TrackState
+ "key": "The Base64 encoded key", // The key of the storage change
+ "value": "The Base64 encoded value" // The value of the storage change
+ } // A storage change
+ // ...
+ ] // The storage changes
+ }, // The diagnostics, optional
+ "session": "A GUID string" // The session id, optional
+ }
+}
+```
+
+### invokescript
+Invokes a script.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "invokescript",
+ "params": [
+ "A Base64 encoded script",
+ [
+ {
+ // The part of the Signer
+ "account": "An UInt160 or Base58Check address", // The account of the signer, required
+ "scopes": "WitnessScope", // The scopes of the signer, see WitnessScope, required
+ "allowedcontracts": ["The contract hash(UInt160)"], // The allowed contracts of the signer, optional
+ "allowedgroups": ["PublicKey"], // The allowed groups of the signer, optional
+ "rules": [
+ {
+ "action": "WitnessRuleAction", // The action of the witness rule, see WitnessRuleAction
+ "condition": { /* A json of WitnessCondition */ } // The condition of the witness rule, see WitnessCondition
+ } // A rule in the witness
+ // ...
+ ], // WitnessRule array, optional(can be null)
+
+ // The part of the Witness
+ "invocation": "A Base64 encoded string", // The invocation of the witness, optional
+ "verification": "A Base64 encoded string" // The verification of the witness, optional
+ }
+ ], // The signers and witnesses list, optional(can be null)
+ false // useDiagnostic, a bool value indicating whether to use diagnostic information, optional
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "script": "A Base64 encoded string",
+ "state": "A string of VMState",
+ "gasconsumed": "An integer number in string",
+ "exception": "The exception message",
+ "stack": [
+ {"type": "The stack item type(StackItemType)", "value": "The stack item value"}
+ ],
+ "notifications": [
+ {
+ "eventname": "The event name",
+ "contract": "The contract hash",
+ "state": {"interface": "A string", "id": "The GUID string"}
+ }
+ ],
+ "diagnostics": {
+ "invokedcontracts": {"hash": "The contract hash", "call": [{"hash": "The contract hash"}]},
+ "storagechanges": [{"state": "The state", "key": "The key", "value": "The value"}]
+ },
+ "session": "A GUID string"
+ }
+}
+```
+
+### traverseiterator
+Traverses an iterator to get more items.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "traverseiterator",
+ "params": [
+ "A GUID string(The session id)",
+ "A GUID string(The iterator id)",
+ 100 // An integer number(The number of items to traverse)
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": [
+ {"type": "The stack item type(StackItemType)", "value": "The stack item value"}
+ ]
+}
+```
+
+### terminatesession
+Terminates a session.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "terminatesession",
+ "params": ["A GUID string(The session id)"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": true // true if the session is terminated successfully, otherwise false
+}
+```
+
+### getunclaimedgas
+Gets the unclaimed gas of an address.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getunclaimedgas",
+ "params": ["An UInt160 or Base58Check address"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {"unclaimed": "An integer in string", "address": "The Base58Check encoded address"}
+}
+```
+
+---
+
+## Wallet Methods
+
+### closewallet
+Closes the currently opened wallet.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "closewallet",
+ "params": []
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": true
+}
+```
+
+### dumpprivkey
+Exports the private key of a specified address.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "dumpprivkey",
+ "params": ["An UInt160 or Base58Check address"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "A WIF-encoded private key"
+}
+```
+
+### getnewaddress
+Creates a new address in the wallet.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnewaddress",
+ "params": []
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The newly created address" // Base58Check address
+}
+```
+
+### getwalletbalance
+Gets the balance of a specified asset in the wallet.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getwalletbalance",
+ "params": ["An UInt160 address"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {"balance": "0"} // An integer number in string, the balance of the specified asset in the wallet
+}
+```
+
+### getwalletunclaimedgas
+Gets the amount of unclaimed GAS in the wallet.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getwalletunclaimedgas",
+ "params": []
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The amount of unclaimed GAS(an integer number in string)"
+}
+```
+
+### importprivkey
+Imports a private key into the wallet.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "importprivkey",
+ "params": ["A WIF-encoded private key"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "address": "The Base58Check address",
+ "haskey": true,
+ "label": "The label",
+ "watchonly": false
+ }
+}
+```
+
+### calculatenetworkfee
+Calculates the network fee for a given transaction.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "calculatenetworkfee",
+ "params": ["A Base64 encoded transaction"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {"networkfee": "The network fee(an integer number in string)"}
+}
+```
+
+### listaddress
+Lists all addresses in the wallet.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "listaddress",
+ "params": []
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": [
+ {"address": "address", "haskey": true, "label": "label", "watchonly": false}
+ ]
+}
+```
+
+### openwallet
+Opens a wallet file.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "openwallet",
+ "params": ["path", "password"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": true
+}
+```
+
+### sendfrom
+Transfers an asset from a specific address to another address.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "sendfrom",
+ "params": [
+ "An UInt160 assetId",
+ "An UInt160 from address",
+ "An UInt160 to address",
+ "An amount as a string(An integer/decimal number in string)",
+ ["UInt160 or Base58Check address"] // signers, optional(can be null)
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The tx hash(UInt256)", // The hash of the transaction
+ "size": 272, // The size of the transaction
+ "version": 0, // The version of the transaction
+ "nonce": 1553700339, // The nonce of the transaction
+ "sender": "The Base58Check address", // The sender of the transaction
+ "sysfee": "100000000", // The system fee of the transaction
+ "netfee": "1272390", // The network fee of the transaction
+ "validuntilblock": 2105487, // The valid until block of the transaction
+ "attributes": [], // The attributes of the transaction
+ "signers": [{"account": "The UInt160 address", "scopes": "CalledByEntry"}], // The signers of the transaction
+ "script": "A Base64 encoded script",
+ "witnesses": [{"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}] // The witnesses of the transaction
+ }
+}
+```
+
+### sendmany
+Transfers assets to multiple addresses.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "sendmany",
+ "params": [
+ "An UInt160 address", // "from", optional(can be null)
+ [
+ {
+ "asset": "An UInt160 assetId",
+ "value": "An integer/decimal as a string",
+ "address": "An UInt160 address"
+ }
+ // ...
+ ], // The transfers list, optional(can be null)
+ ["UInt160 or Base58Check address"] // signers, optional(can be null)
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The tx hash(UInt256)", // The hash of the transaction
+ "size": 483, // The size of the transaction
+ "version": 0, // The version of the transaction
+ "nonce": 34429660, // The nonce of the transaction
+ "sender": "The Base58Check address", // The sender of the transaction
+ "sysfee": "100000000", // The system fee of the transaction
+ "netfee": "2483780", // The network fee of the transaction
+ "validuntilblock": 2105494, // The valid until block of the transaction
+ "attributes": [], // The attributes of the transaction
+ "signers": [{"account": "The UInt160 address", "scopes": "CalledByEntry"}], // The signers of the transaction
+ "script": "A Base64 encoded script",
+ "witnesses": [{"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}] // The witnesses of the transaction
+ }
+}
+```
+
+### sendtoaddress
+Transfers an asset to a specific address.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "sendtoaddress",
+ "params": [
+ "An UInt160 assetId",
+ "An UInt160 address(to)",
+ "An amount as a string(An integer/decimal number)"
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The tx hash(UInt256)", // The hash of the transaction
+ "size": 483, // The size of the transaction
+ "version": 0, // The version of the transaction
+ "nonce": 34429660, // The nonce of the transaction
+ "sender": "The Base58Check address", // The sender of the transaction
+ "sysfee": "100000000", // The system fee of the transaction
+ "netfee": "2483780", // The network fee of the transaction
+ "validuntilblock": 2105494, // The valid until block of the transaction
+ "attributes": [], // The attributes of the transaction
+ "signers": [
+ {
+ "account": "The UInt160 address",
+ "scopes": "CalledByEntry" // see WitnessScope
+ }
+ // ...
+ ], // The signers of the transaction
+ "script": "A Base64 encoded script", // The script of the transaction
+ "witnesses": [{"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}] // The witnesses of the transaction
+ }
+}
+```
+
+### canceltransaction
+Cancels an unconfirmed transaction.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "canceltransaction",
+ "params": [
+ "An tx hash(UInt256)",
+ ["UInt160 or Base58Check address"], // signers, optional(can be null)
+ "An amount as a string(An integer/decimal number)" // extraFee, optional
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "hash": "The tx hash(UInt256)", // The hash of the transaction
+ "size": 483, // The size of the transaction
+ "version": 0, // The version of the transaction
+ "nonce": 34429660, // The nonce of the transaction
+ "sender": "The Base58Check address", // The sender of the transaction
+ "sysfee": "100000000", // The system fee of the transaction
+ "netfee": "2483780", // The network fee of the transaction
+ "validuntilblock": 2105494, // The valid until block of the transaction
+ "attributes": [], // The attributes of the transaction
+ "signers": [{"account": "The UInt160 address", "scopes": "CalledByEntry"}], // The signers of the transaction
+ "script": "A Base64 encoded script",
+ "witnesses": [{"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}] // The witnesses of the transaction
+ }
+}
+```
+
+### invokecontractverify
+Invokes the verify method of a contract.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "invokecontractverify",
+ "params": [
+ "The script hash(UInt160)",
+ [
+ {
+ "type": "The type of the parameter",
+ "value": "The value of the parameter"
+ }
+ // ...
+ ], // The arguments as an array of ContractParameter JSON objects
+ [
+ {
+ // The part of the Signer
+ "account": "An UInt160 or Base58Check address", // The account of the signer, required
+ "scopes": "WitnessScope", // The scopes of the signer, see WitnessScope, required
+ "allowedcontracts": ["The contract hash(UInt160)"], // The allowed contracts of the signer, optional
+ "allowedgroups": ["PublicKey"], // The allowed groups of the signer, optional
+ "rules": [
+ {
+ "action": "WitnessRuleAction", // The action of the witness rule, see WitnessRuleAction
+ "condition": { /* A json of WitnessCondition */ } // The condition of the witness rule, see WitnessCondition
+ } // A rule in the witness
+ // ...
+ ], // WitnessRule array, optional(can be null)
+
+ // The part of the Witness
+ "invocation": "A Base64 encoded string", // The invocation of the witness, optional
+ "verification": "A Base64 encoded string" // The verification of the witness, optional
+ }
+ // ...
+ ] // The signers and witnesses as an array of JSON objects
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "script": "A Base64 encoded string",
+ "state": "A string of VMState",
+ "gasconsumed": "An integer number in string",
+ "exception": "The exception message",
+ "stack": [{"type": "The stack item type", "value": "The stack item value"}]
+ }
+}
+```
+
+## Utility Methods
+
+### listplugins
+Lists all plugins.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "listplugins"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": [
+ {"name": "The plugin name", "version": "The plugin version", "interfaces": ["The plugin method name"]}
+ ]
+}
+```
+
+### validateaddress
+Validates an address.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "validateaddress",
+ "params": ["The Base58Check address"]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {"address": "The Base58Check address", "isvalid": true}
+}
+```
+
+# RpcMethods from other Plugins
+
+## Plugin: ApplicationLogs
+
+### getppplicationlog
+Gets the block or the transaction execution log. The execution logs are stored if the ApplicationLogs plugin is enabled.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getppplicationlog",
+ "params": [
+ "The block hash or the transaction hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "The trigger type(string)" // The trigger type, optional, default is "" and means no filter trigger type. It can be "OnPersist", "PostPersist", "Verification", "Application", "System" or "All"(enum TriggerType). If want to filter by trigger type, need to set the trigger type.
+ ]
+}
+```
+
+**Response:**
+If the block hash is provided, the response is a block execution log.
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "blockhash": "The block hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "executions": [ // The execution logs of OnPersist or PostPersist
+ {
+ "trigger": "The trigger type(string)", // see TriggerType
+ "vmstate": "The VM state(string)", // see VMState
+ "gasconsumed": "The gas consumed(number in string)",
+ "stack": [{"type": "The stack item type", "value": "The stack item value"}], // The stack of the execution, optional. No stack if get stack failed.
+ "exception": "The exception message", // The exception message if get stack failed, optional
+ "notifications": [
+ {
+ "contract": "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "eventname": "The event name",
+ "state": { // Object if the state or 'error: recursive reference' if get state failed.
+ "type": "Array", // always "Array" now.
+ "value": [
+ {
+ "type": "The stack item type", // see StackItemType
+ "value": "The stack item value" // see StackItem, maybe Integer, Boolean, String, Array, Map, etc.
+ }
+ // ...
+ ]
+ }
+ }
+ // ...
+ ],
+ "logs": [ // The logs of the execution, optional. Only Debug option is enabled, the logs will be returned.
+ {
+ "contract": "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "message": "The message"
+ }
+ // ...
+ ]
+ }
+ // ...
+ ] // The execution logs of OnPersist or PostPersist
+ }
+}
+```
+
+If the transaction hash is provided, the response is a transaction execution log.
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "txid": "The transaction hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "executions": [ // The execution log of Verification or Application
+ {
+ "trigger": "The trigger type(string)", // see TriggerType
+ "vmstate": "The VM state(string)", // see VMState
+ "gasconsumed": "The gas consumed(number in string)",
+ "stack": [{"type": "The stack item type", "value": "The stack item value"}], // The stack of the execution, optional. No stack if get stack failed.
+ "exception": "The exception message", // The exception message if get stack failed, optional
+ "notifications": [
+ {
+ "contract": "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "eventname": "The event name",
+ "state": { // Object if the state or 'error: recursive reference' if get state failed.
+ "type": "Array", // always "Array" now.
+ "value": [
+ {
+ "type": "The stack item type", // see StackItemType
+ "value": "The stack item value" // see StackItem, maybe Integer, Boolean, String, Array, Map, etc.
+ }
+ // ...
+ ]
+ }
+ }
+ // ...
+ ],
+ "logs": [ // The logs of the execution, optional. Only Debug option is enabled, the logs will be returned.
+ {
+ "contract": "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "message": "The message"
+ }
+ // ...
+ ]
+ }
+ // ...
+ ] // The execution log of Verification or Application
+ }
+}
+```
+
+## Plugin: OracleService
+
+### submitoracleresponse
+Submits the oracle response of an Oracle request.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "submitoracleresponse",
+ "params": [
+ "The oracle public key(byte[])", // Base64-encoded if access from json-rpc
+ "The request id(ulong)", // The Oracle request id
+ "The transaction signature(byte[])", // Base64-encoded if access from json-rpc
+ "The message signature(byte[])" // Base64-encoded if access from json-rpc
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {} // Empty object if success
+}
+```
+
+## Plugin: StateService
+
+### getstateroot
+Gets the state root by index.
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getstateroot",
+ "params": [
+ 1 // It's an uint number, the index of the state root
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "version": 0, // A byte number, the version of the state root
+ "index": 1, // An uint number, the index of the state root
+ "roothash": "The state root hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "witnesses": [{"invocation": "A Base64 encoded string", "verification": "A Base64 encoded string"}]
+ }
+}
+```
+
+### getproof
+Gets the proof of a key
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getproof",
+ "params": [
+ "The state root hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "The key(Base64-encoded string)" // The key of the storage
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The proof(Base64-encoded string)" // var-bytes storage-key + var-int proof-count + var-bytes proof-item
+}
+```
+
+### verifyproof
+Verifies the proof of a key
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "verifyproof",
+ "params": [
+ "The state root hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "The proof(Base64-encoded string)" // var-bytes storage-key + var-int proof-count + var-bytes proof-item
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The verify result(Base64-encoded string)"
+}
+```
+
+### getstateheight
+Gets the current state height information
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getstateheight"
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "localrootindex": 1, // An uint number, optional, the index of the local state root
+ "validatedrootindex": 1 // An uint number, optional, the index of the validated state root
+ }
+}
+```
+
+### findstates
+List the states of a key prefix
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "findstates",
+ "params": [
+ "The state root hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "The key prefix(Base64-encoded string)", // The key prefix of the storage
+ "The key(Base64-encoded string)", // The key of the storage
+ "The count(int)" // The count of the results, If not set or greater than the MaxFindResultItems, the MaxFindResultItems will be used
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "firstproof": "The proof of the first key(Base64-encoded string)", // Optional, if the results are not empty, the proof of the first key will be returned
+ "lastproof": "The proof of the last key(Base64-encoded string)", // Optional, if the results length is greater than 1, the proof of the last key will be returned
+ "truncated": true, // Whether the results are truncated
+ "results": [
+ {"key": "The key(Base64-encoded string)", "value": "The value(Base64-encoded string)"} // The key-value pair of the state
+ ]
+ }
+}
+```
+
+### getstate
+Gets the state of a key
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getstate",
+ "params": [
+ "The state root hash(UInt256)", // Hex-encoded UInt256 with 0x prefix
+ "The contract hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "The key(Base64-encoded string)" // The key of the state
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": "The state value(Base64-encoded string)" // The value of the state
+}
+```
+
+## Plugin: TokensTracker
+
+### getnep11transfers
+Gets the transfers of NEP-11 token
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnep11transfers",
+ "params": [
+ "The address(Address)", // UInt160 or Base58Check-encoded address
+ 0, // It's an ulong number, the unix timestamp in milliseconds, optional, default to 1 week ago
+ 0 // It's an ulong number, the unix timestamp in milliseconds, optional, default to now
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "address": "The address(Address)", // UInt160 or Base58Check-encoded address
+ "sent": [
+ {
+ "tokenid": "The token id(Hex-encoded string)",
+ "timestamp": 123000, // The unix timestamp in milliseconds
+ "assethash": "The asset hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "transferaddress": "The transfer address(UInt160)", // The address of the transfer, null if no transfer address
+ "amount": "The amount(integer number in string)",
+ "blockindex": 123, // The block index
+ "transfernotifyindex": 123, // The transfer notify index
+ "txhash": "The transaction hash(UInt256)" // Hex-encoded UInt256 with 0x prefix
+ }
+ // ...
+ ],
+ "received": [
+ {
+ "tokenid": "The token id(Hex-encoded string)",
+ "timestamp": 123000, // The unix timestamp in milliseconds
+ "assethash": "The asset hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "transferaddress": "The transfer address(UInt160)", // The address of the transfer, null if no transfer address
+ "amount": "The amount(integer number in string)",
+ "blockindex": 123, // The block index
+ "transfernotifyindex": 123, // The transfer notify index
+ "txhash": "The transaction hash(UInt256)" // Hex-encoded UInt256 with 0x prefix
+ }
+ // ...
+ ]
+ }
+}
+```
+
+### getnep11balances
+Gets the balances of NEP-11 token
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnep11balances",
+ "params": [
+ "The address(Address)" // UInt160 or Base58Check-encoded address
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "address": "The address",
+ "balance": [
+ {
+ "assethash": "The asset hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "name": "The name(string)",
+ "symbol": "The symbol(string)",
+ "decimals": "The decimals(integer number in string)",
+ "tokens": [
+ {
+ "tokenid": "The token id(Hex-encoded string)",
+ "amount": "The amount(integer number in string)",
+ "lastupdatedblock": 123 // The block index
+ }
+ // ...
+ ]
+ }
+ // ...
+ ]
+ }
+}
+```
+
+### getnep11properties
+Gets the properties of NEP-11 token
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnep11properties",
+ "params": [
+ "The address(Address)", // UInt160 or Base58Check-encoded address
+ "The token id(Hex-encoded string)" // The token id of the NEP-11 token
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ // The properties of the NEP-11 token
+ }
+}
+```
+
+### getnep17transfers
+Gets the transfers of NEP-17 token
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnep17transfers",
+ "params": [
+ "The address(Address)", // UInt160 or Base58Check-encoded address
+ 0, // It's an ulong number, the unix timestamp in milliseconds, optional, default to 1 week ago
+ 0 // It's an ulong number, the unix timestamp in milliseconds, optional, default to now
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "address": "The address(Address)", // UInt160 or Base58Check-encoded address
+ "sent": [
+ {
+ "tokenid": "The token id(Hex-encoded string)",
+ "timestamp": 123000, // The unix timestamp in milliseconds
+ "assethash": "The asset hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "transferaddress": "The transfer address(UInt160)", // The address of the transfer, null if no transfer address
+ "amount": "The amount(integer number in string)",
+ "blockindex": 123, // The block index
+ "transfernotifyindex": 123, // The transfer notify index
+ "txhash": "The transaction hash(UInt256)" // Hex-encoded UInt256 with 0x prefix
+ }
+ // ...
+ ],
+ "received": [
+ {
+ "tokenid": "The token id(Hex-encoded string)",
+ "timestamp": 123000, // The unix timestamp in milliseconds
+ "assethash": "The asset hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "transferaddress": "The transfer address(UInt160)", // The address of the transfer, null if no transfer address
+ "amount": "The amount(integer number in string)",
+ "blockindex": 123, // The block index
+ "transfernotifyindex": 123, // The transfer notify index
+ "txhash": "The transaction hash(UInt256)" // Hex-encoded UInt256 with 0x prefix
+ }
+ // ...
+ ]
+ }
+}
+```
+
+### getnep17balances
+Gets the balances of NEP-17 token
+
+**Request:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "getnep17balances",
+ "params": [
+ "The address(Address)" // UInt160 or Base58Check-encoded address
+ ]
+}
+```
+**Response:**
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "address": "The address(Address)", // UInt160 or Base58Check-encoded address
+ "balance": [
+ {
+ "assethash": "The asset hash(UInt160)", // Hex-encoded UInt160 with 0x prefix
+ "name": "The name(string)",
+ "symbol": "The symbol(string)",
+ "decimals": "The decimals(integer number in string)",
+ "amount": "The amount(integer number in string)",
+ "lastupdatedblock": 123 // The block index
+ }
+ // ...
+ ]
+ }
+}
+```
diff --git a/docs/plugin-secure-sign-guide.md b/docs/plugin-secure-sign-guide.md
new file mode 100644
index 0000000000..0d9f38105f
--- /dev/null
+++ b/docs/plugin-secure-sign-guide.md
@@ -0,0 +1,189 @@
+# Secure Sign Plugin
+
+## Purpose
+
+The Secure Sign Plugin (SignClient) is a Neo blockchain plugin that provides secure `ExtensiblePayload` and `Block` signing capabilities through a gRPC-based sign service. This plugin enables:
+
+- **Secure Key Management**: Private keys are stored and managed by a separate sign service, not within the Neo node itself. The private keys should be protected by some mechanisms(like Intel SGX or AWS Nitro Enclave)
+- **Multi Transport Layers Support**: Supports both TCP and Vsock connections for different deployment scenarios
+
+## How to enable plugin `SignClient`
+
+Users can enable plugin `SignClient` by installing it or compiling it manually.
+
+### Install by `neo-cli`
+
+1. **Start the Signing Service**: Ensure, your sign service is running and accessible. You can select a sign service implementation or implement a sign service on your own.
+2. **Download the Plugin**: The SignClient plugin should be installed. You can run `neo-cli` then execute `help install` to get help abort how to install plugin.
+3. **Configure the Plugin**: Create or modify the `SignClient.json` configuration file in the `neo-cli` binary directory (`Plugins/SignClient`).
+4. **Start `neo-cli`**: Start/Restart `neo-cli` if needed.
+
+### Compile Manually
+
+The .Net SDK needs to be installed before compiling it.
+
+1. **Clone the Repository**:
+ ```bash
+ git clone https://github.com/neo-project/neo
+ cd neo
+ donet build
+ ```
+
+2. **Copy to `neo-cli` folder**: Copy the built plugin to the `neo-cli` binary directory.
+- Step 0. Find the `.dll` files. For example:
+ - The `neo-cli` compile products should exist in `./bin/Neo.CLI/net{dotnet-version}/`(i.e. `neo-cli` binary directory, `./bin/Neo.CLI/net9.0/`).
+ - The plugin `SignClient` should exist in `./bin/Neo.Plugins.SignClient/{dotnet-version}/`(i.e. `SignClient` binary directory, `./bin/Neo.Network.RpcClient/9.0/`).
+- Step 1. Copy files `Google.Protobuf.dll Grpc.Core.Api.dll Grpc.Net.Client.dll Grpc.Net.Common.dll `(These files should exist in folder `Neo.Plugins.SignClient`) to the `neo-cli` binary directory.
+- Step 2. `mkdir -p Plugins/SignClient` in the `neo-cli` binary directory. Then copy file `SignClient.dll` from the plugin `SignClient` binary directory to `Plugins/SignClient`.
+- Step 3. Create a `SignClient.json` file `Plugins/SignClient` directory according to the next section.
+- Step 4. Start the `neo-cli`.
+
+
+## Configuration
+
+### Basic Configuration
+
+Create a `SignClient.json` file in `Plugins/SignClient` directory:
+
+```json
+{
+ "PluginConfiguration": {
+ "Name": "SignClient",
+ "Endpoint": "http://127.0.0.1:9991"
+ }
+}
+```
+
+### Configuration Parameters
+
+- **Name**: The name of the sign client (default: "SignClient")
+- **Endpoint**: The endpoint of the sign service
+ - TCP: `http://host:port` or `https://host:port`
+ - VSock: `vsock://contextId:port`
+
+### Connection Types
+
+#### TCP Connection
+```json
+{
+ "PluginConfiguration": {
+ "Name": "SignClient",
+ "Endpoint": "http://127.0.0.1:9991"
+ }
+}
+```
+
+#### VSock Connection
+```json
+{
+ "PluginConfiguration": {
+ "Name": "SignClient",
+ "Endpoint": "vsock://2345:9991"
+ }
+}
+```
+
+## Sign Service Implementation Guide
+
+The SignClient plugin communicates with a sign service using gRPC.
+The service must implement the following interface defined in `proto/servicepb.proto`:
+
+### Service Interface
+
+```protobuf
+service SecureSign {
+ rpc SignExtensiblePayload(SignExtensiblePayloadRequest) returns (SignExtensiblePayloadResponse) {}
+ rpc SignBlock(SignBlockRequest) returns (SignBlockResponse) {}
+ rpc GetAccountStatus(GetAccountStatusRequest) returns (GetAccountStatusResponse) {}
+}
+```
+
+### Methods
+
+#### SignExtensiblePayload
+
+Signs extensible payloads for the specified script hashes.
+
+**Request**:
+- `payload`: The extensible payload to sign
+- `script_hashes`: List of script hashes (UInt160) that need signatures
+- `network`: Network ID
+
+**Response**:
+- `signs`: List of account signs corresponding to each script hash
+
+**Implementation Notes**:
+- The service should check if it has private keys for the requested script hashes.
+- For multi-signature accounts, return all available signatures.
+- Return appropriate account status for each script hash.
+- If a feature not support(for example, multi-signature account), it should return gRPC error code `Unimplemented`.
+- If the `payload` or `script_hashes` is not provided or invalid, it should return gRPC error code `InvalidArgument`.
+
+#### SignBlock
+
+Signs a block with the specified public key.
+
+**Request**:
+- `block`: The block header and transaction hashes
+- `public_key`: The public key to sign with (compressed or uncompressed)
+- `network`: Network ID
+
+**Response**:
+- `signature`: The signature bytes
+
+**Implementation Notes**:
+- The service should verify it has the private key corresponding to the public key.
+- Sign the block header data according to Neo's block signing specification.
+- If the `block` or `public_key` is not provided or invalid, it should return gRPC error code `InvalidArgument`.
+
+#### GetAccountStatus
+
+Retrieves the status of an account for the specified public key.
+
+**Request**:
+- `public_key`: The public key to check (compressed or uncompressed)
+
+**Response**:
+- `status`: Account status enum value
+
+**Implementation Notes**:
+- If the `public_key` is not provided or invalid, it should return gRPC error code `InvalidArgument`.
+
+**Account Status Values**:
+- `NoSuchAccount`: Account doesn't exist
+- `NoPrivateKey`: Account exists but no private key available
+- `Single`: Single-signature account with private key available
+- `Multiple`: Multi-signature account with private key available
+- `Locked`: Account is locked and cannot sign
+
+## Usage Examples
+
+### Console Commands
+
+The plugin provides a console command to check account status:
+
+```bash
+get account status
+```
+
+Example:
+```bash
+get account status 026ff03b949241ce1dadd43519e6960e0a85b41a69a05c328103aa2bce1594ca16
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"No signer service is connected"**
+ - Check if the sign service is running
+ - Verify the endpoint configuration
+ - Check network connectivity
+
+2. **"Invalid vsock endpoint"**
+ - Ensure VSock is only used on Linux
+ - Verify the VSock address format: `vsock://contextId:port`
+
+3. **"Failed to get account status"**
+ - Check if the public key format is correct
+ - Verify the sign service has the requested account
diff --git a/docs/ReferenceCounter.md b/docs/reference-counter.md
similarity index 100%
rename from docs/ReferenceCounter.md
rename to docs/reference-counter.md
diff --git a/docs/serialization-format.md b/docs/serialization-format.md
new file mode 100644
index 0000000000..6cbf0f8c95
--- /dev/null
+++ b/docs/serialization-format.md
@@ -0,0 +1,311 @@
+# Neo Serialization Format
+
+This document describes the binary serialization format used by the Neo blockchain platform. The format is designed for efficient serialization and deserialization of blockchain data structures.
+
+## Overview
+
+Neo uses a custom binary serialization format that supports:
+- Primitive data types (integers, booleans, bytes)
+- Variable-length integers (VarInt)
+- Strings (fixed and variable length)
+- Arrays and collections
+- Custom serializable objects
+- Nullable objects
+
+## Core Interfaces
+
+### ISerializable
+
+All serializable objects in Neo implement the `ISerializable` interface:
+
+```csharp
+public interface ISerializable
+{
+ int Size { get; }
+
+ void Serialize(BinaryWriter writer);
+
+ void Deserialize(ref MemoryReader reader);
+}
+```
+
+- `Size`: Returns the serialized size in bytes
+- `Serialize`: Writes the object to a BinaryWriter
+- `Deserialize`: Reads the object from a MemoryReader
+
+## Primitive Data Types
+
+### Integers
+
+Neo supports both little-endian and big-endian integer formats:
+
+| Type | Size | Endianness | Description |
+|------|------|------------|-------------|
+| `sbyte` | 1 byte | N/A | Signed 8-bit integer |
+| `byte` | 1 byte | N/A | Unsigned 8-bit integer |
+| `short` | 2 bytes | Little-endian | Signed 16-bit integer |
+| `ushort` | 2 bytes | Little-endian | Unsigned 16-bit integer |
+| `int` | 4 bytes | Little-endian | Signed 32-bit integer |
+| `uint` | 4 bytes | Little-endian | Unsigned 32-bit integer |
+| `long` | 8 bytes | Little-endian | Signed 64-bit integer |
+| `ulong` | 8 bytes | Little-endian | Unsigned 64-bit integer |
+
+Big-endian variants are available for `short`, `ushort`, `int`, `uint`, `long`, and `ulong`.
+
+### Boolean
+
+Booleans are serialized as single bytes:
+- `false` → `0x00`
+- `true` → `0x01`
+- Any other value throws `FormatException`
+
+### Variable-Length Integers (VarInt)
+
+Neo uses a compact variable-length integer format:
+
+| Value Range | Format | Size |
+|-------------|--------|------|
+| 0-252 | Direct value | 1 byte |
+| 253-65535 | `0xFD` + 2-byte little-endian | 3 bytes |
+| 65536-4294967295 | `0xFE` + 4-byte little-endian | 5 bytes |
+| 4294967296+ | `0xFF` + 8-byte little-endian | 9 bytes |
+
+**Serialization:**
+```csharp
+if (value < 0xFD)
+{
+ writer.Write((byte)value);
+}
+else if (value <= 0xFFFF)
+{
+ writer.Write((byte)0xFD);
+ writer.Write((ushort)value);
+}
+else if (value <= 0xFFFFFFFF)
+{
+ writer.Write((byte)0xFE);
+ writer.Write((uint)value);
+}
+else
+{
+ writer.Write((byte)0xFF);
+ writer.Write(value);
+}
+```
+
+**Deserialization:**
+```csharp
+var b = ReadByte();
+var value = b switch
+{
+ 0xfd => ReadUInt16(),
+ 0xfe => ReadUInt32(),
+ 0xff => ReadUInt64(),
+ _ => b
+};
+```
+
+## Strings
+
+### Fixed-Length Strings
+
+Fixed-length strings are padded with null bytes:
+
+**Format:** `[UTF-8 bytes][zero padding]`
+
+**Serialization:**
+```csharp
+var bytes = value.ToStrictUtf8Bytes();
+if (bytes.Length > length)
+ throw new ArgumentException();
+writer.Write(bytes);
+if (bytes.Length < length)
+ writer.Write(new byte[length - bytes.Length]);
+```
+
+**Deserialization:**
+```csharp
+var end = currentOffset + length;
+var offset = currentOffset;
+while (offset < end && _span[offset] != 0) offset++;
+var data = _span[currentOffset..offset];
+for (; offset < end; offset++)
+ if (_span[offset] != 0)
+ throw new FormatException();
+currentOffset = end;
+return data.ToStrictUtf8String();
+```
+
+### Variable-Length Strings
+
+Variable-length strings use VarInt for length prefix:
+
+**Format:** `[VarInt length][UTF-8 bytes]`
+
+**Serialization:**
+```csharp
+writer.WriteVarInt(value.Length);
+writer.Write(value.ToStrictUtf8Bytes());
+```
+
+**Deserialization:**
+```csharp
+var length = (int)ReadVarInt((ulong)max);
+EnsurePosition(length);
+var data = _span.Slice(currentOffset, length);
+currentOffset += length;
+return data.ToStrictUtf8String();
+```
+
+## Byte Arrays
+
+### Fixed-Length Byte Arrays
+
+**Format:** `[raw bytes]`
+
+### Variable-Length Byte Arrays
+
+**Format:** `[VarInt length][raw bytes]`
+
+**Serialization:**
+```csharp
+writer.WriteVarInt(value.Length);
+writer.Write(value);
+```
+
+**Deserialization:**
+```csharp
+return ReadMemory((int)ReadVarInt((ulong)max));
+```
+
+## Collections
+
+### Serializable Arrays
+
+**Format:** `[VarInt count][item1][item2]...[itemN]`
+
+**Serialization:**
+```csharp
+writer.WriteVarInt(value.Count);
+foreach (T item in value)
+{
+ item.Serialize(writer);
+}
+```
+
+**Deserialization:**
+```csharp
+var array = new T[reader.ReadVarInt((ulong)max)];
+for (var i = 0; i < array.Length; i++)
+{
+ array[i] = new T();
+ array[i].Deserialize(ref reader);
+}
+return array;
+```
+
+### Nullable Arrays
+
+**Format:** `[VarInt count][bool1][item1?][bool2][item2?]...[boolN][itemN?]`
+
+**Serialization:**
+```csharp
+writer.WriteVarInt(value.Length);
+foreach (var item in value)
+{
+ var isNull = item is null;
+ writer.Write(!isNull);
+ if (isNull) continue;
+ item!.Serialize(writer);
+}
+```
+
+**Deserialization:**
+```csharp
+var array = new T[reader.ReadVarInt((ulong)max)];
+for (var i = 0; i < array.Length; i++)
+ array[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null;
+return array;
+```
+
+## UTF-8 Encoding
+
+Neo uses strict UTF-8 encoding with the following characteristics:
+
+- **Strict Mode**: Invalid UTF-8 sequences throw exceptions
+- **No Fallback**: No replacement characters for invalid sequences
+- **Exception Handling**: Detailed error messages for debugging
+
+**String to Bytes:**
+```csharp
+public static byte[] ToStrictUtf8Bytes(this string value)
+{
+ return StrictUTF8.GetBytes(value);
+}
+```
+
+**Bytes to String:**
+```csharp
+public static string ToStrictUtf8String(this ReadOnlySpan value)
+{
+ return StrictUTF8.GetString(value);
+}
+```
+
+## Error Handling
+
+The serialization format includes comprehensive error handling:
+
+- **FormatException**: Invalid data format or corrupted data
+- **ArgumentNullException**: Null values where not allowed
+- **ArgumentException**: Invalid arguments (e.g., string too long)
+- **ArgumentOutOfRangeException**: Values outside allowed ranges
+- **DecoderFallbackException**: Invalid UTF-8 sequences
+- **EncoderFallbackException**: Characters that cannot be encoded
+
+## Examples
+
+### Simple Object Serialization
+
+```csharp
+public class SimpleData : ISerializable
+{
+ public string Name { get; set; }
+ public int Value { get; set; }
+
+ public int Size => Name.GetStrictUtf8ByteCount() + sizeof(int);
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.WriteVarString(Name);
+ writer.Write(Value);
+ }
+
+ public void Deserialize(ref MemoryReader reader)
+ {
+ Name = reader.ReadVarString();
+ Value = reader.ReadInt32();
+ }
+}
+```
+
+### Array Serialization
+
+```csharp
+public class DataArray : ISerializable
+{
+ public SimpleData[] Items { get; set; }
+
+ public int Size => Items.Sum(item => item.Size) + GetVarSize(Items.Length);
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(Items);
+ }
+
+ public void Deserialize(ref MemoryReader reader)
+ {
+ Items = reader.ReadSerializableArray();
+ }
+}
+```
diff --git a/global.json b/global.json
index e6d547b04e..a5d75d8411 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "9.0.203",
+ "version": "9.0.300",
"rollForward": "latestFeature",
"allowPrerelease": false
}
diff --git a/neo.sln b/neo.sln
index 3779ae8d78..62904b97bd 100644
--- a/neo.sln
+++ b/neo.sln
@@ -1,4 +1,5 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32516.85
MinimumVisualStudioVersion = 10.0.40219.1
@@ -26,8 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.VM.Tests", "tests\Neo.V
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.ConsoleService", "src\Neo.ConsoleService\Neo.ConsoleService.csproj", "{9E886812-7243-48D8-BEAF-47AADC11C054}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.GUI", "src\Neo.GUI\Neo.GUI.csproj", "{02ABDE42-9880-43B4-B6F7-8D618602A277}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.CLI", "src\Neo.CLI\Neo.CLI.csproj", "{BDFBE455-4C1F-4FC4-B5FC-1387B93A8687}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.ConsoleService.Tests", "tests\Neo.ConsoleService.Tests\Neo.ConsoleService.Tests.csproj", "{B40F8584-5AFB-452C-AEFA-009C80CC23A9}"
@@ -80,13 +79,27 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions.Benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Json.Benchmarks", "benchmarks\Neo.Json.Benchmarks\Neo.Json.Benchmarks.csproj", "{5F984D2B-793F-4683-B53A-80050E6E0286}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Network.RpcClient", "src\Neo.Network.RpcClient\Neo.Network.RpcClient.csproj", "{9ADB4E11-8655-42C2-8A75-E4436F56F17A}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie", "src\Neo.Cryptography.MPTTrie\Neo.Cryptography.MPTTrie.csproj", "{E384C5EF-493E-4ED6-813C-6364F968CEE8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie.Tests", "tests\Neo.Cryptography.MPTTrie.Tests\Neo.Cryptography.MPTTrie.Tests.csproj", "{40A23D45-1E81-41A4-B587-16AF26630103}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Network.RPC.Tests", "tests\Neo.Network.RPC.Tests\Neo.Network.RPC.Tests.csproj", "{19B1CF1A-17F4-4E04-AB9C-55CE74952E11}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignClient", "src\Plugins\SignClient\SignClient.csproj", "{CAD55942-48A3-4526-979D-7519FADF19FE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.SignClient.Tests", "tests\Neo.Plugins.SignClient.Tests\Neo.Plugins.SignClient.Tests.csproj", "{E2CFEAA1-45F2-4075-94ED-866862C6863F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.MPTTrie.Benchmarks", "benchmarks\Neo.Cryptography.MPTTrie.Benchmarks\Neo.Cryptography.MPTTrie.Benchmarks.csproj", "{69B0D53B-D97A-4315-B205-CCEBB7289EA9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RpcClient", "src\RpcClient\RpcClient.csproj", "{977B7BD7-93AE-14AD-CA79-91537F8964E5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.RpcClient.Tests", "tests\Neo.RpcClient.Tests\Neo.RpcClient.Tests.csproj", "{8C7A7070-08E3-435A-A909-9541B5C66E8C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.RestServer.Tests", "tests\Neo.Plugins.RestServer.Tests\Neo.Plugins.RestServer.Tests.csproj", "{A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestServer", "src\Plugins\RestServer\RestServer.csproj", "{4865C487-C1A1-4E36-698D-1EC4CCF08FDB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.StateService.Tests", "tests\Neo.Plugins.StateService.Tests\Neo.Plugins.StateService.Tests.csproj", "{229C7877-C0FA-4399-A0DB-96E714A59481}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.SQLiteWallet.Tests", "tests\Neo.Plugins.SQLiteWallet.Tests\Neo.Plugins.SQLiteWallet.Tests.csproj", "{92E091FE-C7E0-4526-8352-779B73F55F13}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -130,10 +143,6 @@ Global
{9E886812-7243-48D8-BEAF-47AADC11C054}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E886812-7243-48D8-BEAF-47AADC11C054}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E886812-7243-48D8-BEAF-47AADC11C054}.Release|Any CPU.Build.0 = Release|Any CPU
- {02ABDE42-9880-43B4-B6F7-8D618602A277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {02ABDE42-9880-43B4-B6F7-8D618602A277}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {02ABDE42-9880-43B4-B6F7-8D618602A277}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {02ABDE42-9880-43B4-B6F7-8D618602A277}.Release|Any CPU.Build.0 = Release|Any CPU
{BDFBE455-4C1F-4FC4-B5FC-1387B93A8687}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDFBE455-4C1F-4FC4-B5FC-1387B93A8687}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDFBE455-4C1F-4FC4-B5FC-1387B93A8687}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -214,6 +223,10 @@ Global
{8C866DC8-2E55-4399-9563-2F47FD4602EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C866DC8-2E55-4399-9563-2F47FD4602EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C866DC8-2E55-4399-9563-2F47FD4602EC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Release|Any CPU.Build.0 = Release|Any CPU
{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -226,14 +239,6 @@ Global
{5F984D2B-793F-4683-B53A-80050E6E0286}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F984D2B-793F-4683-B53A-80050E6E0286}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F984D2B-793F-4683-B53A-80050E6E0286}.Release|Any CPU.Build.0 = Release|Any CPU
- {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Release|Any CPU.Build.0 = Release|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A}.Release|Any CPU.Build.0 = Release|Any CPU
{E384C5EF-493E-4ED6-813C-6364F968CEE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E384C5EF-493E-4ED6-813C-6364F968CEE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E384C5EF-493E-4ED6-813C-6364F968CEE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -242,10 +247,42 @@ Global
{40A23D45-1E81-41A4-B587-16AF26630103}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40A23D45-1E81-41A4-B587-16AF26630103}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40A23D45-1E81-41A4-B587-16AF26630103}.Release|Any CPU.Build.0 = Release|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CAD55942-48A3-4526-979D-7519FADF19FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CAD55942-48A3-4526-979D-7519FADF19FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CAD55942-48A3-4526-979D-7519FADF19FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CAD55942-48A3-4526-979D-7519FADF19FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2CFEAA1-45F2-4075-94ED-866862C6863F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E2CFEAA1-45F2-4075-94ED-866862C6863F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2CFEAA1-45F2-4075-94ED-866862C6863F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E2CFEAA1-45F2-4075-94ED-866862C6863F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C7A7070-08E3-435A-A909-9541B5C66E8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C7A7070-08E3-435A-A909-9541B5C66E8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C7A7070-08E3-435A-A909-9541B5C66E8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C7A7070-08E3-435A-A909-9541B5C66E8C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4865C487-C1A1-4E36-698D-1EC4CCF08FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4865C487-C1A1-4E36-698D-1EC4CCF08FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4865C487-C1A1-4E36-698D-1EC4CCF08FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4865C487-C1A1-4E36-698D-1EC4CCF08FDB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {229C7877-C0FA-4399-A0DB-96E714A59481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {229C7877-C0FA-4399-A0DB-96E714A59481}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {229C7877-C0FA-4399-A0DB-96E714A59481}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {229C7877-C0FA-4399-A0DB-96E714A59481}.Release|Any CPU.Build.0 = Release|Any CPU
+ {92E091FE-C7E0-4526-8352-779B73F55F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {92E091FE-C7E0-4526-8352-779B73F55F13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {92E091FE-C7E0-4526-8352-779B73F55F13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {92E091FE-C7E0-4526-8352-779B73F55F13}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -260,7 +297,6 @@ Global
{0603710E-E0BA-494C-AA0F-6FB0C8A8C754} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{005F84EB-EA2E-449F-930A-7B4173DDC7EC} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
{9E886812-7243-48D8-BEAF-47AADC11C054} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
- {02ABDE42-9880-43B4-B6F7-8D618602A277} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{BDFBE455-4C1F-4FC4-B5FC-1387B93A8687} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{B40F8584-5AFB-452C-AEFA-009C80CC23A9} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
{D48C1FAB-3471-4CA0-8688-25E6F43F2C25} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
@@ -282,14 +318,21 @@ Global
{FF76D8A4-356B-461A-8471-BC1B83E57BBC} = {C2DC830A-327A-42A7-807D-295216D30DBB}
{5E4947F3-05D3-4806-B0F3-30DAC71B5986} = {C2DC830A-327A-42A7-807D-295216D30DBB}
{8C866DC8-2E55-4399-9563-2F47FD4602EC} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {72997EAB-9B0C-4BC8-B797-955C219C2C97} = {7F257712-D033-47FF-B439-9D4320D06599}
{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
{B6CB2559-10F9-41AC-8D58-364BFEF9688B} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
{5F984D2B-793F-4683-B53A-80050E6E0286} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
- {72997EAB-9B0C-4BC8-B797-955C219C2C97} = {7F257712-D033-47FF-B439-9D4320D06599}
- {9ADB4E11-8655-42C2-8A75-E4436F56F17A} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{E384C5EF-493E-4ED6-813C-6364F968CEE8} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{40A23D45-1E81-41A4-B587-16AF26630103} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
- {19B1CF1A-17F4-4E04-AB9C-55CE74952E11} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
+ {CAD55942-48A3-4526-979D-7519FADF19FE} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {E2CFEAA1-45F2-4075-94ED-866862C6863F} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {69B0D53B-D97A-4315-B205-CCEBB7289EA9} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
+ {977B7BD7-93AE-14AD-CA79-91537F8964E5} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
+ {8C7A7070-08E3-435A-A909-9541B5C66E8C} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
+ {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {4865C487-C1A1-4E36-698D-1EC4CCF08FDB} = {C2DC830A-327A-42A7-807D-295216D30DBB}
+ {229C7877-C0FA-4399-A0DB-96E714A59481} = {7F257712-D033-47FF-B439-9D4320D06599}
+ {92E091FE-C7E0-4526-8352-779B73F55F13} = {7F257712-D033-47FF-B439-9D4320D06599}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}
diff --git a/scripts/run-dotnet-format.sh b/scripts/run-dotnet-format.sh
index f47af97061..6ffd6aa530 100644
--- a/scripts/run-dotnet-format.sh
+++ b/scripts/run-dotnet-format.sh
@@ -1,6 +1,8 @@
#!/bin/bash
# Script to run dotnet format with the same settings as the GitHub workflow
+set -e
+
VERIFY_ONLY=false
VERBOSITY="diagnostic"
FOLDER="."
diff --git a/scripts/run-localnet-nodes.md b/scripts/run-localnet-nodes.md
new file mode 100644
index 0000000000..c4fbd803c0
--- /dev/null
+++ b/scripts/run-localnet-nodes.md
@@ -0,0 +1,120 @@
+# Localnet Node Management Script
+
+This script helps you create and manage multiple Neo localnet nodes for development and testing purposes.
+
+## Features
+
+- **Automatic Configuration Generation**: Creates individual configuration files for each node
+- **Port Management**: Automatically assigns unique ports for P2P and RPC communication
+- **Node Management**: Start, stop, restart, and monitor multiple nodes
+- **Data Isolation**: Each node has its own data directory and storage
+- **Process Management**: Tracks running processes with PID files
+
+## Usage
+
+### Basic Commands
+
+```bash
+# Start 7 nodes (default)
+./run-localnet-nodes.sh start
+
+# Check status of all nodes
+./run-localnet-nodes.sh status
+
+# Stop all nodes
+./run-localnet-nodes.sh stop
+
+# Restart all nodes
+./run-localnet-nodes.sh restart
+
+# Clean up all data
+./run-localnet-nodes.sh clean
+```
+
+### Node Configuration
+
+Each node gets:
+- **Unique P2P Port**: Starting from 20333 (Node0: 20333, Node1: 20334, etc.)
+- **Unique RPC Port**: Starting from 10330 (Node0: 10330, Node1: 10331, etc.)
+- **Isolated Data Directory**: `localnet_nodes/node_X/`
+- **Individual Configuration**: `localnet_nodes/node_X/config.json`
+- **Process Logs**: `localnet_nodes/node_X/neo.log`
+
+### Network Configuration
+
+- **Network ID**: 1234567890 (localnet)
+- **Block Time**: 15 seconds
+- **Validators**: 7 validators configured
+- **Seed List**: All nodes are configured to connect to each other
+
+## Directory Structure
+
+```
+localnet_nodes/
+├── node_0/
+│ ├── config.json
+│ ├── neo.log
+│ ├── neo.pid
+│ └── Data_LevelDB_Node0/
+├── node_1/
+│ ├── config.json
+│ ├── neo.log
+│ ├── neo.pid
+│ └── Data_LevelDB_Node1/
+└── ...
+```
+
+## Prerequisites
+
+1. **Build Neo Project**: Make sure the project is built
+ ```bash
+ dotnet build
+ ```
+
+2. **Neo-CLI Available**: The script looks for `neo-cli` or `neo-cli.dll` in the `bin/` directory
+
+## Troubleshooting
+
+### Node Won't Start
+- Check if neo-cli is built: `ls bin/neo-cli*`
+- Check logs: `cat localnet_nodes/node_X/neo.log`
+- Verify ports are not in use: `netstat -an | grep 20333`
+
+### Port Conflicts
+- The script uses ports 20333+ for P2P and 10330+ for RPC
+- Make sure these ports are available
+- You can modify `BASE_PORT` and `BASE_RPC_PORT` in the script
+
+### Process Management
+- Each node runs as a background process
+- PID files are stored in each node's directory
+- Use `./run-localnet-nodes.sh status` to check running nodes
+
+## Development Tips
+
+1. **Start with 7 Nodes**: For development, 7 nodes is best configuration for testing.
+2. **Monitor Logs**: Check individual node logs for debugging
+3. **Clean Restart**: Use `clean` command to start fresh
+4. **Network Connectivity**: Nodes automatically discover each other via seed list
+
+## Example Workflow
+
+```bash
+# 1. Build the project
+dotnet build
+
+# 2. Start 3 localnet nodes
+./run-localnet-nodes.sh start 3
+
+# 3. Check status
+./run-localnet-nodes.sh status
+
+# 4. Monitor a specific node
+tail -f localnet_nodes/node_0/neo.log
+
+# 5. Stop all nodes
+./run-localnet-nodes.sh stop
+
+# 6. Clean up when done
+./run-localnet-nodes.sh clean
+```
diff --git a/scripts/run-localnet-nodes.sh b/scripts/run-localnet-nodes.sh
new file mode 100755
index 0000000000..9176654068
--- /dev/null
+++ b/scripts/run-localnet-nodes.sh
@@ -0,0 +1,495 @@
+#!/bin/bash
+# Script to generate and manage multiple localnet neo-cli nodes
+# Usage: ./run-localnet-nodes.sh [start|stop|status|clean] [node_count] [base_port] [base_rpc_port]
+
+set -e
+
+# Configuration
+NODE_COUNT=${2:-7} # 7 nodes
+BASE_PORT=${3:-20333} # Default P2P port, can be overridden
+BASE_RPC_PORT=${4:-10330} # Default RPC port, can be overridden
+BASE_DATA_DIR="localnet_nodes"
+
+DOTNET_VERSION="net9.0"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+NEO_CLI_DIR="${NEO_CLI_DIR:-$SCRIPT_DIR/../bin/Neo.CLI/$DOTNET_VERSION}"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Logging functions
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Check if neo-cli exists
+check_neo_cli() {
+ log_info "Using NEO_CLI_DIR: $NEO_CLI_DIR"
+ if [ ! -f "$NEO_CLI_DIR/neo-cli" ] && [ ! -f "$NEO_CLI_DIR/neo-cli.dll" ]; then
+ log_error "neo-cli not found in $NEO_CLI_DIR"
+ log_info "Please build the project first: dotnet build"
+ log_info "Or set NEO_CLI_DIR environment variable to the correct path"
+ exit 1
+ fi
+}
+
+# An Array of addresses, NOTE: just for test
+ADDRESSES=(
+ "NSL83LVKbvCpg5gjWC9bfsmERN5kRJSs9d"
+ "NRPf2BLaP595UFybH1nwrExJSt5ZGbKnjd"
+ "NXRrR4VU3TJyZ6iPBfvoddRKAGynVPvjKm"
+ "NfGwaZPHGXLqZ17U7p5hqkZGivArXbXUbL"
+ "NjCgqnnJpCsRQwEayWy1cZSWVwQ7eRejRq"
+ "NYXoVMFa3ekGUnX4qzk8DTD2hhs5aSh2k4"
+ "NQSjfdeawkxqcUXQ3Vvbka66Frr4hQJoBr"
+)
+
+# An Array of keys
+KEYS=(
+ "6PYVdEBZe7Mg4CiikuCXkEpcbwX7WXT72xfHTYd6hJzRWN3iBPDfGis7kV"
+ "6PYNZ7WDsjXwn2Mo8T3N7fTw7ZSfY71MXbVeRf1zZjv2baEdjbWNHm5mGQ"
+ "6PYXGkpWLLXtyC6cQthCcShioQJupRvyhDrz6xfLyiEa9HeJW4oTb4aJHP"
+ "6PYUCCNgCrVrB5vpCbsFwzEA7d2SkCzCTYMyhYw2TL51CaGeie2UWyehzw"
+ "6PYQpWR6CGrWDKauPWfVEfmwMKp2xKFod4X1AvV39ud5qhaSkrsFQeCBPy"
+ "6PYTm6sJLR1oWX2svdJkzWhkbqTAGurEybDdcCTBa19WNzDuFXURX2NAaE"
+ "6PYQM2Tdkon4kqzYSboctKLEXyLLub4vQFSXVwwgtSPcPTsqC2VhQXwf5R"
+)
+
+# An Array of scripts
+SCRIPTS=(
+ "DCEChSZdyIWdBeHkKpDWwpqd4VUx6sGCSJdD5qlHgX0qn2ZBVuezJw=="
+ "DCECozKyXb9hGPwlv2Tw2DALu2I7eDRDcazwy1ByffMtnbNBVuezJw=="
+ "DCECqgIsK8NhTSOvwSFvxD2tkHINHilrTgm37izZvrgNm+pBVuezJw=="
+ "DCEDabXhB8SMjperdGnbbr8JAZz7MiPToYxK+iFwQoE9+d5BVuezJw=="
+ "DCECnkPTdNxK3KFYu0ZbSthBegdmQaU5UOPLccY0PdJYk9RBVuezJw=="
+ "DCEChLsd71mcGde7lMvdiOx+1IXbId6mTIa7kXYi+1ac6cpBVuezJw=="
+ "DCED5FrD4mtUqJfwU41g1MwcKIS43Zk78Ie+REaoLdQE/9hBVuezJw=="
+)
+
+
+# Generate configuration for a specific node
+generate_node_config() {
+ local node_id=$1
+ local port=$((BASE_PORT + node_id))
+ local rpc_port=$((BASE_RPC_PORT + node_id))
+ local data_dir="$BASE_DATA_DIR/node_$node_id"
+ local config_file="$data_dir/config.json"
+ local wallet_file="$data_dir/wallet.json"
+
+ log_info "Generating config for node $node_id (port: $port, rpc: $rpc_port)"
+
+ # Create data directory
+ mkdir -p "$data_dir"
+
+ # Generate seed list (all other nodes)
+ local seed_list=""
+ for i in $(seq 0 $((NODE_COUNT-1))); do
+ local seed_port=$((BASE_PORT + i))
+ if [ -n "$seed_list" ]; then
+ seed_list="$seed_list,"
+ fi
+ seed_list="$seed_list\"localhost:$seed_port\""
+ done
+
+ # Create configuration file
+ cat > "$config_file" << EOF
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": true,
+ "Active": true
+ },
+ "Storage": {
+ "Engine": "LevelDBStore",
+ "Path": "Data_LevelDB_Node$node_id"
+ },
+ "P2P": {
+ "Port": $port,
+ "EnableCompression": true,
+ "MinDesiredConnections": 3,
+ "MaxConnections": 10,
+ "MaxKnownHashes": 1000,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "wallet.json",
+ "Password": "123",
+ "IsActive": true
+ },
+ "Contracts": {
+ "NeoNameService": "0x50ac1c37690cc2cfc594472833cf57505d5f46de"
+ },
+ "Plugins": {
+ "DownloadUrl": "https://api.github.com/repos/neo-project/neo/releases"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 1234567890,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 5000,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "Hardforks": {
+ "HF_Aspidochelone": 1,
+ "HF_Basilisk": 1,
+ "HF_Cockatrice": 1,
+ "HF_Domovoi": 1,
+ "HF_Echidna": 1,
+ "HF_Faun": 1
+ },
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "0285265dc8859d05e1e42a90d6c29a9de15531eac182489743e6a947817d2a9f66",
+ "02a332b25dbf6118fc25bf64f0d8300bbb623b78344371acf0cb50727df32d9db3",
+ "02aa022c2bc3614d23afc1216fc43dad90720d1e296b4e09b7ee2cd9beb80d9bea",
+ "0369b5e107c48c8e97ab7469db6ebf09019cfb3223d3a18c4afa217042813df9de",
+ "029e43d374dc4adca158bb465b4ad8417a076641a53950e3cb71c6343dd25893d4",
+ "0284bb1def599c19d7bb94cbdd88ec7ed485db21dea64c86bb917622fb569ce9ca",
+ "03e45ac3e26b54a897f0538d60d4cc1c2884b8dd993bf087be4446a82dd404ffd8"
+ ],
+ "SeedList": [
+ $seed_list
+ ]
+ }
+}
+EOF
+
+ cat > "$wallet_file" << EOF
+{
+ "name": "node_$node_id",
+ "version": "1.0",
+ "scrypt": {"n": 2, "r": 1, "p": 1 },
+ "accounts": [{
+ "address": "${ADDRESSES[$node_id]}",
+ "isDefault": true,
+ "lock": false,
+ "key": "${KEYS[$node_id]}",
+ "contract": {
+ "script": "${SCRIPTS[$node_id]}",
+ "parameters": [{"name": "signature","type": "Signature"}],
+ "deployed": false
+ }
+ }]
+}
+EOF
+
+ log_success "Generated config for node $node_id"
+}
+
+
+initialize_plugins() {
+ for plugin in "DBFTPlugin" "RpcServer" "ApplicationLogs"; do
+ plugin_dir="$NEO_CLI_DIR/../../Neo.Plugins.$plugin/$DOTNET_VERSION"
+ if [ ! -d "$NEO_CLI_DIR/Plugins/$plugin" ]; then
+ mkdir -p "$NEO_CLI_DIR/Plugins/$plugin"
+ fi
+
+ if [ -f "$plugin_dir/$plugin.dll" ]; then
+ cp "$plugin_dir/$plugin.dll" "$NEO_CLI_DIR/Plugins/$plugin/$plugin.dll"
+ fi
+
+ if [ -f "$plugin_dir/$plugin.json" ]; then
+ cp "$plugin_dir/$plugin.json" "$NEO_CLI_DIR/Plugins/$plugin/$plugin.json"
+ fi
+ done
+}
+
+# Update plugin configuration files to use local test network ID
+update_plugin_configs() {
+ log_info "Updating plugin configurations for local test network..."
+
+ # Find and update all plugin JSON files in the Plugins directory under NEO_CLI_DIR
+ find "$NEO_CLI_DIR/Plugins" -name "*.json" -type f 2>/dev/null | while read -r plugin_file; do
+ if [ -f "$plugin_file" ]; then
+ # Check if the file contains any Network configuration
+ if grep -q '"Network":' "$plugin_file"; then
+ # Get the current network ID for logging
+ current_network=$(grep '"Network":' "$plugin_file" | sed 's/.*"Network": *\([0-9]*\).*/\1/')
+ log_info "Updating network ID from $current_network to 1234567890 in: $plugin_file"
+
+ # Replace any network ID with local test network ID
+ sed -i.bak 's/"Network": [0-9]*/"Network": 1234567890/g' "$plugin_file"
+
+ # Remove backup file
+ rm -f "$plugin_file.bak"
+ fi
+ fi
+ done
+
+ if [ -f "$NEO_CLI_DIR/Plugins/DBFTPlugin/DBFTPlugin.json" ]; then
+ # set AutoStart to true
+ sed -i.bak 's/"AutoStart": false/"AutoStart": true/g' "$NEO_CLI_DIR/Plugins/DBFTPlugin/DBFTPlugin.json"
+ rm -f "$NEO_CLI_DIR/Plugins/DBFTPlugin/DBFTPlugin.json.bak"
+ fi
+
+ log_success "Plugin configurations updated for local test network"
+}
+
+# Generate all node configurations
+generate_configs() {
+ local force=${1:-false}
+
+ log_info "Generating configurations for $NODE_COUNT nodes..."
+
+ # Create base directory if it doesn't exist
+ mkdir -p "$BASE_DATA_DIR"
+
+ # Generate config for each node only if it doesn't exist or force regenerate
+ for i in $(seq 0 $((NODE_COUNT-1))); do
+ local data_dir="$BASE_DATA_DIR/node_$i"
+ local config_file="$data_dir/config.json"
+ local wallet_file="$data_dir/wallet.json"
+
+ if [ "$force" = "true" ] || [ ! -f "$config_file" ] || [ ! -f "$wallet_file" ]; then
+ if [ "$force" = "true" ]; then
+ log_info "Force regenerating configuration for node $i..."
+ fi
+ generate_node_config $i
+ else
+ log_info "Node $i configuration already exists, skipping..."
+ fi
+ done
+
+ log_success "Generated $NODE_COUNT node configurations"
+}
+
+# Start a specific node
+start_node() {
+ local node_id=$1
+ local data_dir="$BASE_DATA_DIR/node_$node_id"
+ local config_file="$data_dir/config.json"
+ local pid_file="$data_dir/neo.pid"
+
+ if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
+ log_warning "Node $node_id is already running (PID: $(cat "$pid_file"))"
+ return
+ fi
+
+ log_info "Starting node $node_id..."
+
+ # Ensure data directory exists
+ mkdir -p "$data_dir"
+
+ # Change to the data directory
+ cd "$data_dir"
+
+ # Start neo-cli in background
+ log_info "Starting $NEO_CLI_DIR/neo-cli in $data_dir"
+ if [ -f "$NEO_CLI_DIR/neo-cli" ]; then
+ nohup "$NEO_CLI_DIR/neo-cli" --background > neo.log 2>&1 &
+ else
+ log_error "neo-cli executable not found"
+ return 1
+ fi
+
+ local pid=$!
+ log_info "node $node_id started with pid $pid"
+ echo $pid > neo.pid
+
+ # Wait a moment and check if process is still running
+ sleep 1
+ if kill -0 $pid 2>/dev/null; then
+ log_success "Node $node_id started (PID: $pid)"
+ else
+ log_error "Failed to start node $node_id"
+ rm -f neo.pid
+ return 1
+ fi
+
+ # Return to original directory
+ cd - > /dev/null
+}
+
+# Start all nodes
+start_nodes() {
+ log_info "Starting $NODE_COUNT localnet nodes..."
+
+ check_neo_cli # Check if neo-cli exists
+ initialize_plugins # Initialize required plugins
+ generate_configs # Always generate configs to ensure they're up to date
+ update_plugin_configs # Update plugin configuration files to use local test network ID
+
+ # Start each node
+ for i in $(seq 0 $((NODE_COUNT-1))); do
+ # set RpcServer Port to BASE_RPC_PORT + node_id
+ if [ -f "$NEO_CLI_DIR/Plugins/RpcServer/RpcServer.json" ]; then
+ local rpc_port=$((BASE_RPC_PORT + i))
+ sed -i.bak "s/\"Port\": [0-9]*/\"Port\": $rpc_port/g" "$NEO_CLI_DIR/Plugins/RpcServer/RpcServer.json"
+ rm -f "$NEO_CLI_DIR/Plugins/RpcServer/RpcServer.json.bak"
+ fi
+
+ start_node $i
+ sleep 1 # Small delay between starts
+ done
+
+ log_success "All nodes started!"
+ show_status
+}
+
+# Stop a specific node
+stop_node() {
+ local node_id=$1
+ local pid_file="$BASE_DATA_DIR/node_$node_id/neo.pid"
+
+ if [ -f "$pid_file" ]; then
+ local pid=$(cat "$pid_file")
+ if kill -0 $pid 2>/dev/null; then
+ log_info "Stopping node $node_id (PID: $pid)..."
+ kill $pid
+ rm -f "$pid_file"
+ log_success "Node $node_id stopped"
+ else
+ log_warning "Node $node_id was not running"
+ rm -f "$pid_file"
+ fi
+ else
+ log_warning "Node $node_id is not running"
+ fi
+}
+
+# Stop all nodes
+stop_nodes() {
+ log_info "Stopping all localnet nodes..."
+
+ for i in $(seq 0 $((NODE_COUNT-1))); do
+ stop_node $i
+ done
+
+ log_success "All nodes stopped!"
+}
+
+# Show status of all nodes
+show_status() {
+ log_info "Localnet nodes status:"
+ echo "----------------------------------------------"
+
+ # if RpcServer plugin not installed, don't show RPC port
+ show_rpc_port=false
+ if [ ! -f "$NEO_CLI_DIR/Plugins/RpcServer/RpcServer.json" ]; then
+ show_rpc_port=false
+ else
+ show_rpc_port=true
+ fi
+
+ if [ "$show_rpc_port" = "true" ]; then
+ printf "%-8s %-8s %-12s %-8s %-8s\n" "Node" "Status" "PID" "Port" "RPC"
+ else
+ printf "%-8s %-8s %-12s %-8s\n" "Node" "Status" "PID" "Port"
+ fi
+ echo "----------------------------------------------"
+
+ for i in $(seq 0 $((NODE_COUNT-1))); do
+ local pid_file="$BASE_DATA_DIR/node_$i/neo.pid"
+ local port=$((BASE_PORT + i))
+ local rpc_port=$((BASE_RPC_PORT + i))
+
+ if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
+ local pid=$(cat "$pid_file")
+ if [ "$show_rpc_port" = "false" ]; then
+ printf "%-8s %-8s %-12s %-8s\n" "Node$i" "Running" "$pid" "$port"
+ else
+ printf "%-8s %-8s %-12s %-8s %-8s\n" "Node$i" "Running" "$pid" "$port" "$rpc_port"
+ fi
+ else
+ if [ "$show_rpc_port" = "false" ]; then
+ printf "%-8s %-8s %-12s %-8s\n" "Node$i" "Stopped" "-" "$port"
+ else
+ printf "%-8s %-8s %-12s %-8s %-8s\n" "Node$i" "Stopped" "-" "$port" "$rpc_port"
+ fi
+ fi
+ done
+ echo "----------------------------------------------"
+}
+
+# Clean up all data
+clean_data() {
+ log_info "Cleaning up all localnet data..."
+ rm -rf "$BASE_DATA_DIR"
+ log_success "All localnet data cleaned up"
+}
+
+# Show usage
+show_usage() {
+ echo "Usage: $0 [command] [node_count] [base_port] [base_rpc_port]"
+ echo ""
+ echo "Commands:"
+ echo " start Start all localnet nodes (default: 7 nodes)"
+ echo " stop Stop all localnet nodes"
+ echo " status Show status of all nodes"
+ echo " clean Clean up all node data"
+ echo " restart Stop and start all nodes"
+ echo " regenerate Force regenerate all node configurations"
+ echo ""
+ echo "Parameters:"
+ echo " node_count Number of nodes to start (default: 7)"
+ echo " base_port Starting P2P port (default: 20333)"
+ echo " base_rpc_port Starting RPC port (default: 10330)"
+ echo ""
+ echo "Environment Variables:"
+ echo " NEO_CLI_DIR Path to neo-cli directory (default: ../bin/Neo.CLI/$DOTNET_VERSION)"
+ echo ""
+ echo "Examples:"
+ echo " $0 start # Start 7 nodes with default ports"
+ echo " $0 start 7 30000 20000 # Start 7 nodes with P2P ports 30000-30006, RPC ports 20000-20006"
+ echo " $0 status # Show status"
+ echo " $0 stop # Stop all nodes"
+ echo " $0 regenerate # Force regenerate all configurations"
+ echo " NEO_CLI_DIR=/path/to/neo-cli $0 start # Use custom neo-cli path"
+ echo ""
+}
+
+# Main script logic
+case "${1:-start}" in
+ "start")
+ start_nodes
+ ;;
+ "stop")
+ stop_nodes
+ ;;
+ "status")
+ show_status
+ ;;
+ "clean")
+ clean_data
+ ;;
+ "restart")
+ stop_nodes
+ sleep 2
+ start_nodes
+ ;;
+ "regenerate")
+ log_info "Force regenerating all node configurations..."
+ check_neo_cli
+ generate_configs true
+ update_plugin_configs
+ log_success "All configurations regenerated!"
+ ;;
+ "help"|"-h"|"--help")
+ show_usage
+ ;;
+ *)
+ log_error "Unknown command: $1"
+ show_usage
+ exit 1
+ ;;
+esac
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 34e8191e67..9b09ccd7fd 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -36,8 +36,4 @@
-
-
-
-
diff --git a/src/IsExternalInit.cs b/src/IsExternalInit.cs
deleted file mode 100644
index e095d55a3f..0000000000
--- a/src/IsExternalInit.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// IsExternalInit.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-#if !NET5_0_OR_GREATER
-
-using System.ComponentModel;
-
-namespace System.Runtime.CompilerServices
-{
- ///
- /// Reserved to be used by the compiler for tracking metadata.
- /// This class should not be used by developers in source code.
- ///
- [EditorBrowsable(EditorBrowsableState.Never)]
- internal static class IsExternalInit
- {
- }
-}
-
-#endif
diff --git a/src/Neo.CLI/CLI/CommandLineOption.cs b/src/Neo.CLI/CLI/CommandLineOption.cs
index e775e8bf5f..d3d5b1ba7d 100644
--- a/src/Neo.CLI/CLI/CommandLineOption.cs
+++ b/src/Neo.CLI/CLI/CommandLineOption.cs
@@ -21,6 +21,7 @@ public class CommandLineOptions
public string? DBPath { get; init; }
public LogLevel Verbose { get; init; } = LogLevel.Info;
public bool? NoVerify { get; init; }
+ public bool Background { get; init; }
///
/// Check if CommandLineOptions was configured
diff --git a/src/Neo.CLI/CLI/Helper.cs b/src/Neo.CLI/CLI/Helper.cs
index c7b1437a70..d0893a14e7 100644
--- a/src/Neo.CLI/CLI/Helper.cs
+++ b/src/Neo.CLI/CLI/Helper.cs
@@ -35,7 +35,7 @@ public static void IsScriptValid(this ReadOnlyMemory script, ContractAbi a
}
catch (Exception e)
{
- throw new FormatException($"Bad Script or Manifest Format: {e.Message}");
+ throw new FormatException($"Contract script validation failed. The provided script or manifest format is invalid and cannot be processed. Please verify the script bytecode and manifest are correctly formatted and compatible. Original error: {e.Message}", e);
}
}
}
diff --git a/src/Neo.CLI/CLI/MainService.Block.cs b/src/Neo.CLI/CLI/MainService.Block.cs
index 61531f9d63..76a0be35bc 100644
--- a/src/Neo.CLI/CLI/MainService.Block.cs
+++ b/src/Neo.CLI/CLI/MainService.Block.cs
@@ -69,12 +69,12 @@ private void OnExportBlocksStartCountCommand(uint start, uint count = uint.MaxVa
/// Reads blocks from a stream and yields blocks that are not yet in the blockchain.
///
/// The stream to read blocks from.
- /// If true, reads the start block index from the stream.
+ /// If true, reads the start block index from the stream.
/// An enumerable of blocks that are not yet in the blockchain.
- private IEnumerable GetBlocks(Stream stream, bool read_start = false)
+ private IEnumerable GetBlocks(Stream stream, bool readStart = false)
{
using BinaryReader r = new BinaryReader(stream);
- uint start = read_start ? r.ReadUInt32() : 0;
+ uint start = readStart ? r.ReadUInt32() : 0;
uint count = r.ReadUInt32();
uint end = start + count - 1;
uint currentHeight = NativeContract.Ledger.CurrentIndex(NeoSystem.StoreView);
@@ -83,7 +83,7 @@ private IEnumerable GetBlocks(Stream stream, bool read_start = false)
{
var size = r.ReadInt32();
if (size > Message.PayloadMaxSize)
- throw new ArgumentException($"Block {height} exceeds the maximum allowed size");
+ throw new ArgumentException($"Block at height {height} has a size of {size} bytes, which exceeds the maximum allowed payload size of {Message.PayloadMaxSize} bytes. This block cannot be processed due to size constraints.");
byte[] array = r.ReadBytes(size);
if (height > currentHeight)
diff --git a/src/Neo.CLI/CLI/MainService.CommandLine.cs b/src/Neo.CLI/CLI/MainService.CommandLine.cs
index 04cb4ecfbc..7e37986929 100644
--- a/src/Neo.CLI/CLI/MainService.CommandLine.cs
+++ b/src/Neo.CLI/CLI/MainService.CommandLine.cs
@@ -21,11 +21,12 @@ public partial class MainService
{
public int OnStartWithCommandLine(string[] args)
{
- RootCommand rootCommand = new(Assembly.GetExecutingAssembly().GetCustomAttribute()!.Title)
+ var rootCommand = new RootCommand(Assembly.GetExecutingAssembly().GetCustomAttribute()!.Title)
{
new Option(["-c", "--config","/config"], "Specifies the config file."),
new Option(["-w", "--wallet","/wallet"], "The path of the neo3 wallet [*.json]."),
new Option(["-p", "--password" ,"/password"], "Password to decrypt the wallet, either from the command line or config file."),
+ new Option(["--background","/background"], "Run the service in background."),
new Option(["--db-engine","/db-engine"], "Specify the db engine."),
new Option(["--db-path","/db-path"], "Specify the db path."),
new Option(["--noverify","/noverify"], "Indicates whether the blocks need to be verified when importing."),
@@ -39,6 +40,7 @@ public int OnStartWithCommandLine(string[] args)
private void Handle(RootCommand command, CommandLineOptions options, InvocationContext context)
{
+ IsBackground = options.Background;
Start(options);
}
@@ -72,7 +74,9 @@ private static void CustomProtocolSettings(CommandLineOptions options, ProtocolS
private static void CustomApplicationSettings(CommandLineOptions options, Settings settings)
{
- var tempSetting = string.IsNullOrEmpty(options.Config) ? settings : new Settings(new ConfigurationBuilder().AddJsonFile(options.Config, optional: true).Build().GetSection("ApplicationConfiguration"));
+ var tempSetting = string.IsNullOrEmpty(options.Config)
+ ? settings
+ : new Settings(new ConfigurationBuilder().AddJsonFile(options.Config, optional: true).Build().GetSection("ApplicationConfiguration"));
var customSetting = new Settings
{
Logger = tempSetting.Logger,
diff --git a/src/Neo.CLI/CLI/MainService.Contracts.cs b/src/Neo.CLI/CLI/MainService.Contracts.cs
index aca30c1989..b6b703d8f3 100644
--- a/src/Neo.CLI/CLI/MainService.Contracts.cs
+++ b/src/Neo.CLI/CLI/MainService.Contracts.cs
@@ -10,11 +10,13 @@
// modifications are permitted.
using Neo.ConsoleService;
+using Neo.Cryptography.ECC;
using Neo.Json;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Numerics;
@@ -186,5 +188,282 @@ private void OnInvokeCommand(UInt160 scriptHash, string operation, JArray? contr
}
SignAndSendTx(NeoSystem.StoreView, tx);
}
+
+ ///
+ /// Process "invokeabi" command - invokes a contract method with parameters parsed according to the contract's ABI
+ ///
+ /// Script hash
+ /// Operation
+ /// Arguments as an array of values that will be parsed according to the ABI
+ /// Transaction's sender
+ /// Signer's accounts
+ /// Max fee for running the script, in the unit of GAS
+ [ConsoleCommand("invokeabi", Category = "Contract Commands")]
+ private void OnInvokeAbiCommand(UInt160 scriptHash, string operation,
+ JArray? args = null, UInt160? sender = null, UInt160[]? signerAccounts = null, decimal maxGas = 20)
+ {
+ // Get the contract from storage
+ var contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
+ if (contract == null)
+ {
+ ConsoleHelper.Error("Contract does not exist.");
+ return;
+ }
+
+ // Check if contract has valid ABI
+ if (contract.Manifest?.Abi == null)
+ {
+ ConsoleHelper.Error("Contract ABI is not available.");
+ return;
+ }
+
+ // Find the method in the ABI with matching parameter count
+ var paramCount = args?.Count ?? 0;
+ var method = contract.Manifest.Abi.GetMethod(operation, paramCount);
+ if (method == null)
+ {
+ // Try to find any method with that name for a better error message
+ var anyMethod = contract.Manifest.Abi.GetMethod(operation, -1);
+ if (anyMethod != null)
+ {
+ ConsoleHelper.Error($"Method '{operation}' exists but expects {anyMethod.Parameters.Length} parameters, not {paramCount}.");
+ }
+ else
+ {
+ ConsoleHelper.Error($"Method '{operation}' does not exist in this contract.");
+ }
+ return;
+ }
+
+ // Validate parameter count - moved outside parsing loop for better performance
+ var expectedParamCount = method.Parameters.Length;
+ var actualParamCount = args?.Count ?? 0;
+
+ if (actualParamCount != expectedParamCount)
+ {
+ ConsoleHelper.Error($"Method '{operation}' expects exactly {expectedParamCount} parameters but {actualParamCount} were provided.");
+ return;
+ }
+
+ // Parse parameters according to the ABI
+ JArray? contractParameters = null;
+ if (args != null && args.Count > 0)
+ {
+ contractParameters = new JArray();
+ for (int i = 0; i < args.Count; i++)
+ {
+ var paramDef = method.Parameters[i];
+ var paramValue = args[i];
+
+ try
+ {
+ var contractParam = ParseParameterFromAbi(paramDef.Type, paramValue);
+ contractParameters.Add(contractParam.ToJson());
+ }
+ catch (Exception ex)
+ {
+ ConsoleHelper.Error($"Failed to parse parameter '{paramDef.Name ?? $"at index {i}"}' (index {i}): {ex.Message}");
+ return;
+ }
+ }
+ }
+
+ // Call the original invoke command with the parsed parameters
+ OnInvokeCommand(scriptHash, operation, contractParameters, sender, signerAccounts, maxGas);
+ }
+
+ ///
+ /// Parse a parameter value according to its ABI type
+ ///
+ private ContractParameter ParseParameterFromAbi(ContractParameterType type, JToken? value)
+ {
+ if (value == null || value == JToken.Null)
+ return new ContractParameter { Type = type, Value = null };
+
+ return type switch
+ {
+ ContractParameterType.Boolean => new ContractParameter { Type = type, Value = value.AsBoolean() },
+ ContractParameterType.Integer => ParseIntegerParameter(value),
+ ContractParameterType.ByteArray => ParseByteArrayParameter(value),
+ ContractParameterType.String => new ContractParameter { Type = type, Value = value.AsString() },
+ ContractParameterType.Hash160 => ParseHash160Parameter(value),
+ ContractParameterType.Hash256 => ParseHash256Parameter(value),
+ ContractParameterType.PublicKey => ParsePublicKeyParameter(value),
+ ContractParameterType.Signature => ParseSignatureParameter(value),
+ ContractParameterType.Array => ParseArrayParameter(value),
+ ContractParameterType.Map => ParseMapParameter(value),
+ ContractParameterType.Any => InferParameterFromToken(value),
+ ContractParameterType.InteropInterface => throw new NotSupportedException("InteropInterface type cannot be parsed from JSON"),
+ _ => throw new ArgumentException($"Unsupported parameter type: {type}")
+ };
+ }
+
+ ///
+ /// Parse integer parameter with error handling
+ ///
+ private ContractParameter ParseIntegerParameter(JToken value)
+ {
+ try
+ {
+ return new ContractParameter { Type = ContractParameterType.Integer, Value = BigInteger.Parse(value.AsString()) };
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"Invalid integer format. Expected a numeric string, got: '{value.AsString()}'");
+ }
+ }
+
+ ///
+ /// Parse byte array parameter with error handling
+ ///
+ private ContractParameter ParseByteArrayParameter(JToken value)
+ {
+ try
+ {
+ return new ContractParameter { Type = ContractParameterType.ByteArray, Value = Convert.FromBase64String(value.AsString()) };
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"Invalid ByteArray format. Expected a Base64 encoded string, got: '{value.AsString()}'");
+ }
+ }
+
+ ///
+ /// Parse Hash160 parameter with error handling
+ ///
+ private ContractParameter ParseHash160Parameter(JToken value)
+ {
+ try
+ {
+ return new ContractParameter { Type = ContractParameterType.Hash160, Value = UInt160.Parse(value.AsString()) };
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"Invalid Hash160 format. Expected format: '0x' followed by 40 hex characters (e.g., '0x1234...abcd'), got: '{value.AsString()}'");
+ }
+ }
+
+ ///
+ /// Parse Hash256 parameter with error handling
+ ///
+ private ContractParameter ParseHash256Parameter(JToken value)
+ {
+ try
+ {
+ return new ContractParameter { Type = ContractParameterType.Hash256, Value = UInt256.Parse(value.AsString()) };
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"Invalid Hash256 format. Expected format: '0x' followed by 64 hex characters, got: '{value.AsString()}'");
+ }
+ }
+
+ ///
+ /// Parse PublicKey parameter with error handling
+ ///
+ private ContractParameter ParsePublicKeyParameter(JToken value)
+ {
+ try
+ {
+ return new ContractParameter { Type = ContractParameterType.PublicKey, Value = ECPoint.Parse(value.AsString(), ECCurve.Secp256r1) };
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"Invalid PublicKey format. Expected a hex string starting with '02' or '03' (33 bytes) or '04' (65 bytes), got: '{value.AsString()}'");
+ }
+ }
+
+ ///
+ /// Parse Signature parameter with error handling
+ ///
+ private ContractParameter ParseSignatureParameter(JToken value)
+ {
+ try
+ {
+ return new ContractParameter { Type = ContractParameterType.Signature, Value = Convert.FromBase64String(value.AsString()) };
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"Invalid Signature format. Expected a Base64 encoded string, got: '{value.AsString()}'");
+ }
+ }
+
+ ///
+ /// Parse Array parameter with type inference
+ ///
+ private ContractParameter ParseArrayParameter(JToken value)
+ {
+ if (value is not JArray array)
+ throw new ArgumentException($"Expected array value for Array parameter type, got: {value.GetType().Name}");
+
+ var items = new ContractParameter[array.Count];
+ for (int j = 0; j < array.Count; j++)
+ {
+ var element = array[j];
+ // Check if this is already a ContractParameter format
+ if (element is JObject obj && obj.ContainsProperty("type") && obj.ContainsProperty("value"))
+ {
+ items[j] = ContractParameter.FromJson(obj);
+ }
+ else
+ {
+ // Otherwise, infer the type
+ items[j] = element != null ? InferParameterFromToken(element) : new ContractParameter { Type = ContractParameterType.Any, Value = null };
+ }
+ }
+ return new ContractParameter { Type = ContractParameterType.Array, Value = items };
+ }
+
+ ///
+ /// Parse Map parameter with type inference
+ ///
+ private ContractParameter ParseMapParameter(JToken value)
+ {
+ if (value is not JObject map)
+ throw new ArgumentException("Expected object value for Map parameter type");
+
+ // Check if this is a ContractParameter format map
+ if (map.ContainsProperty("type") && map["type"]?.AsString() == "Map" && map.ContainsProperty("value"))
+ {
+ return ContractParameter.FromJson(map);
+ }
+
+ // Otherwise, parse as a regular map with inferred types
+ var dict = new List>();
+ foreach (var kvp in map.Properties)
+ {
+ // Keys are always strings in JSON
+ var key = new ContractParameter { Type = ContractParameterType.String, Value = kvp.Key };
+
+ // For values, check if they are ContractParameter format
+ var val = kvp.Value;
+ if (val is JObject valObj && valObj.ContainsProperty("type") && valObj.ContainsProperty("value"))
+ {
+ dict.Add(new KeyValuePair(key, ContractParameter.FromJson(valObj)));
+ }
+ else
+ {
+ var valueParam = val != null ? InferParameterFromToken(val) : new ContractParameter { Type = ContractParameterType.Any, Value = null };
+ dict.Add(new KeyValuePair(key, valueParam));
+ }
+ }
+ return new ContractParameter { Type = ContractParameterType.Map, Value = dict };
+ }
+
+ ///
+ /// Infers the parameter type from a JToken and parses it accordingly
+ ///
+ private ContractParameter InferParameterFromToken(JToken value)
+ {
+ return value switch
+ {
+ JBoolean => ParseParameterFromAbi(ContractParameterType.Boolean, value),
+ JNumber => ParseParameterFromAbi(ContractParameterType.Integer, value),
+ JString => ParseParameterFromAbi(ContractParameterType.String, value),
+ JArray => ParseParameterFromAbi(ContractParameterType.Array, value),
+ JObject => ParseParameterFromAbi(ContractParameterType.Map, value),
+ _ => throw new ArgumentException($"Cannot infer type for value: {value}")
+ };
+ }
}
}
diff --git a/src/Neo.CLI/CLI/MainService.Plugins.cs b/src/Neo.CLI/CLI/MainService.Plugins.cs
index e82fd21a99..921c3f4deb 100644
--- a/src/Neo.CLI/CLI/MainService.Plugins.cs
+++ b/src/Neo.CLI/CLI/MainService.Plugins.cs
@@ -80,29 +80,28 @@ private void OnReinstallCommand(string pluginName)
/// Downloaded content
private static async Task DownloadPluginAsync(string pluginName, Version pluginVersion, string? customDownloadUrl = null, bool prerelease = false)
{
- ConsoleHelper.Info($"Downloading {pluginName} {pluginVersion}...");
using var httpClient = new HttpClient();
var asmName = Assembly.GetExecutingAssembly().GetName();
httpClient.DefaultRequestHeaders.UserAgent.Add(new(asmName.Name!, asmName.Version!.ToString(3)));
var url = customDownloadUrl == null ? Settings.Default.Plugins.DownloadUrl : new Uri(customDownloadUrl);
- var json = await httpClient.GetFromJsonAsync(url) ?? throw new HttpRequestException($"Failed: {url}");
+ var json = await httpClient.GetFromJsonAsync(url) ?? throw new HttpRequestException($"Failed to retrieve plugin catalog from URL: {url}. Please check your network connection and verify the plugin repository is accessible.");
var jsonRelease = json.AsArray()
.SingleOrDefault(s =>
s != null &&
s["tag_name"]!.GetValue() == $"v{pluginVersion.ToString(3)}" &&
- s["prerelease"]!.GetValue() == prerelease) ?? throw new Exception($"Could not find Release {pluginVersion}");
+ s["prerelease"]!.GetValue() == prerelease) ?? throw new Exception($"Plugin release version {pluginVersion} (prerelease: {prerelease}) was not found in the plugin repository. Please verify the version number or check if the release is available.");
var jsonAssets = jsonRelease
.AsObject()
- .SingleOrDefault(s => s.Key == "assets").Value ?? throw new Exception("Could not find any Plugins");
+ .SingleOrDefault(s => s.Key == "assets").Value ?? throw new Exception($"No plugin assets found for release version {pluginVersion}. The plugin release may be incomplete or corrupted in the repository.");
var jsonPlugin = jsonAssets
.AsArray()
.SingleOrDefault(s =>
Path.GetFileNameWithoutExtension(
s!["name"]!.GetValue()).Equals(pluginName, StringComparison.InvariantCultureIgnoreCase))
- ?? throw new Exception($"Could not find {pluginName}");
+ ?? throw new Exception($"Plugin '{pluginName}' was not found in the available assets for version {pluginVersion}. Please verify the plugin name is correct and the plugin is available for this version.");
var downloadUrl = jsonPlugin["browser_download_url"]!.GetValue();
return await httpClient.GetStreamAsync(downloadUrl);
@@ -270,7 +269,7 @@ private async Task> GetPluginListAsync()
httpClient.DefaultRequestHeaders.UserAgent.Add(new(asmName.Name!, asmName.Version!.ToString(3)));
var json = await httpClient.GetFromJsonAsync(Settings.Default.Plugins.DownloadUrl)
- ?? throw new HttpRequestException($"Failed: {Settings.Default.Plugins.DownloadUrl}");
+ ?? throw new HttpRequestException($"Failed to retrieve plugin catalog from URL: {Settings.Default.Plugins.DownloadUrl}. Please check your network connection and verify the plugin repository is accessible.");
return json.AsArray()
.Where(w =>
w != null &&
diff --git a/src/Neo.CLI/CLI/MainService.Tools.cs b/src/Neo.CLI/CLI/MainService.Tools.cs
index cf27de3cd8..40a4ae1299 100644
--- a/src/Neo.CLI/CLI/MainService.Tools.cs
+++ b/src/Neo.CLI/CLI/MainService.Tools.cs
@@ -75,7 +75,7 @@ private void OnParseCommand(string value)
[ParseFunction(".nef file path to content base64")]
private string? NefFileToBase64(string path)
{
- if (Path.GetExtension(path).ToLower() != ".nef") return null;
+ if (!Path.GetExtension(path).Equals(".nef", StringComparison.CurrentCultureIgnoreCase)) return null;
if (!File.Exists(path)) return null;
return Convert.ToBase64String(File.ReadAllBytes(path));
}
diff --git a/src/Neo.CLI/CLI/MainService.Vote.cs b/src/Neo.CLI/CLI/MainService.Vote.cs
index 0edbb6fd81..23e18b411b 100644
--- a/src/Neo.CLI/CLI/MainService.Vote.cs
+++ b/src/Neo.CLI/CLI/MainService.Vote.cs
@@ -24,6 +24,17 @@
namespace Neo.CLI
{
+ public static class VoteMethods
+ {
+ public const string Register = "registerCandidate";
+ public const string Unregister = "unregisterCandidate";
+ public const string Vote = "vote";
+ public const string GetAccountState = "getAccountState";
+ public const string GetCandidates = "getCandidates";
+ public const string GetCommittee = "getCommittee";
+ public const string GetNextBlockValidators = "getNextBlockValidators";
+ }
+
partial class MainService
{
///
@@ -35,30 +46,12 @@ private void OnRegisterCandidateCommand(UInt160 account)
{
var testGas = NativeContract.NEO.GetRegisterPrice(NeoSystem.StoreView) + (BigInteger)Math.Pow(10, NativeContract.GAS.Decimals) * 10;
if (NoWallet()) return;
- WalletAccount currentAccount = CurrentWallet!.GetAccount(account);
-
- if (currentAccount == null)
- {
- ConsoleHelper.Warning("This address isn't in your wallet!");
- return;
- }
- else
- {
- if (currentAccount.Lock || currentAccount.WatchOnly)
- {
- ConsoleHelper.Warning("Locked or WatchOnly address.");
- return;
- }
- }
- ECPoint? publicKey = currentAccount.GetKey()?.PublicKey;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "registerCandidate", publicKey);
- script = scriptBuilder.ToArray();
- }
+ var currentAccount = GetValidAccountOrWarn(account);
+ if (currentAccount == null) return;
+ var publicKey = currentAccount.GetKey()?.PublicKey;
+ var script = BuildNeoScript(VoteMethods.Register, publicKey);
SendTransaction(script, account, (long)testGas);
}
@@ -70,30 +63,12 @@ private void OnRegisterCandidateCommand(UInt160 account)
private void OnUnregisterCandidateCommand(UInt160 account)
{
if (NoWallet()) return;
- WalletAccount currentAccount = CurrentWallet!.GetAccount(account);
- if (currentAccount == null)
- {
- ConsoleHelper.Warning("This address isn't in your wallet!");
- return;
- }
- else
- {
- if (currentAccount.Lock || currentAccount.WatchOnly)
- {
- ConsoleHelper.Warning("Locked or WatchOnly address.");
- return;
- }
- }
-
- ECPoint? publicKey = currentAccount?.GetKey()?.PublicKey;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "unregisterCandidate", publicKey);
- script = scriptBuilder.ToArray();
- }
+ var currentAccount = GetValidAccountOrWarn(account);
+ if (currentAccount == null) return;
+ var publicKey = currentAccount?.GetKey()?.PublicKey;
+ var script = BuildNeoScript(VoteMethods.Unregister, publicKey);
SendTransaction(script, account);
}
@@ -106,13 +81,8 @@ private void OnUnregisterCandidateCommand(UInt160 account)
private void OnVoteCommand(UInt160 senderAccount, ECPoint publicKey)
{
if (NoWallet()) return;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "vote", senderAccount, publicKey);
- script = scriptBuilder.ToArray();
- }
+ var script = BuildNeoScript(VoteMethods.Vote, senderAccount, publicKey);
SendTransaction(script, senderAccount);
}
@@ -124,13 +94,8 @@ private void OnVoteCommand(UInt160 senderAccount, ECPoint publicKey)
private void OnUnvoteCommand(UInt160 senderAccount)
{
if (NoWallet()) return;
- byte[] script;
- using (ScriptBuilder scriptBuilder = new())
- {
- scriptBuilder.EmitDynamicCall(NativeContract.NEO.Hash, "vote", senderAccount, null);
- script = scriptBuilder.ToArray();
- }
+ var script = BuildNeoScript(VoteMethods.Vote, senderAccount, null);
SendTransaction(script, senderAccount);
}
@@ -140,7 +105,7 @@ private void OnUnvoteCommand(UInt160 senderAccount)
[ConsoleCommand("get candidates", Category = "Vote Commands")]
private void OnGetCandidatesCommand()
{
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getCandidates", out StackItem result, null, null, false)) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetCandidates, out var result, null, null, false)) return;
var resJArray = (Array)result;
@@ -166,7 +131,7 @@ private void OnGetCandidatesCommand()
[ConsoleCommand("get committee", Category = "Vote Commands")]
private void OnGetCommitteeCommand()
{
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getCommittee", out StackItem result, null, null, false)) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetCommittee, out StackItem result, null, null, false)) return;
var resJArray = (Array)result;
@@ -188,7 +153,7 @@ private void OnGetCommitteeCommand()
[ConsoleCommand("get next validators", Category = "Vote Commands")]
private void OnGetNextBlockValidatorsCommand()
{
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getNextBlockValidators", out StackItem result, null, null, false)) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetNextBlockValidators, out var result, null, null, false)) return;
var resJArray = (Array)result;
@@ -210,24 +175,24 @@ private void OnGetNextBlockValidatorsCommand()
[ConsoleCommand("get accountstate", Category = "Vote Commands")]
private void OnGetAccountState(UInt160 address)
{
- const string notice = "No vote record!";
+ const string Notice = "No vote record!";
var arg = new JObject
{
["type"] = "Hash160",
["value"] = address.ToString()
};
- if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getAccountState", out var result, null, new JArray(arg))) return;
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, VoteMethods.GetAccountState, out var result, null, new JArray(arg))) return;
Console.WriteLine();
if (result.IsNull)
{
- ConsoleHelper.Warning(notice);
+ ConsoleHelper.Warning(Notice);
return;
}
var resJArray = (Array)result;
if (resJArray is null)
{
- ConsoleHelper.Warning(notice);
+ ConsoleHelper.Warning(Notice);
return;
}
@@ -235,7 +200,7 @@ private void OnGetAccountState(UInt160 address)
{
if (value.IsNull)
{
- ConsoleHelper.Warning(notice);
+ ConsoleHelper.Warning(Notice);
return;
}
}
@@ -258,5 +223,28 @@ private void OnGetAccountState(UInt160 address)
ConsoleHelper.Error("Error parsing the result");
}
}
+ ///
+ /// Get account or log a warm
+ ///
+ ///
+ /// account or null
+ private WalletAccount? GetValidAccountOrWarn(UInt160 account)
+ {
+ var acct = CurrentWallet?.GetAccount(account);
+ if (acct == null)
+ {
+ ConsoleHelper.Warning("This address isn't in your wallet!");
+ return null;
+ }
+ if (acct.Lock || acct.WatchOnly)
+ {
+ ConsoleHelper.Warning("Locked or WatchOnly address.");
+ return null;
+ }
+ return acct;
+ }
+
+ private byte[] BuildNeoScript(string method, params object?[] args)
+ => NativeContract.NEO.Hash.MakeScript(method, args);
}
}
diff --git a/src/Neo.CLI/CLI/MainService.Wallet.cs b/src/Neo.CLI/CLI/MainService.Wallet.cs
index 9b6dbd3058..5362f23730 100644
--- a/src/Neo.CLI/CLI/MainService.Wallet.cs
+++ b/src/Neo.CLI/CLI/MainService.Wallet.cs
@@ -86,7 +86,7 @@ private void OnCloseWalletCommand()
[ConsoleCommand("upgrade wallet", Category = "Wallet Commands")]
private void OnUpgradeWalletCommand(string path)
{
- if (Path.GetExtension(path).ToLowerInvariant() != ".db3")
+ if (!Path.GetExtension(path).Equals(".db3", StringComparison.InvariantCultureIgnoreCase))
{
ConsoleHelper.Warning("Can't upgrade the wallet file. Check if your wallet is in db3 format.");
return;
@@ -449,7 +449,8 @@ private void OnListAssetCommand()
Console.WriteLine();
}
Console.WriteLine("----------------------------------------------------");
- ConsoleHelper.Info("Total: NEO: ", $"{CurrentWallet.GetAvailable(snapshot, NativeContract.NEO.Hash),10} ", "GAS: ", $"{CurrentWallet.GetAvailable(snapshot, NativeContract.GAS.Hash),18}");
+ ConsoleHelper.Info("Total: NEO: ", $"{CurrentWallet.GetAvailable(snapshot, NativeContract.NEO.Hash),10} ",
+ "GAS: ", $"{CurrentWallet.GetAvailable(snapshot, NativeContract.GAS.Hash),18}");
Console.WriteLine();
ConsoleHelper.Info("NEO hash: ", NativeContract.NEO.Hash.ToString());
ConsoleHelper.Info("GAS hash: ", NativeContract.GAS.Hash.ToString());
@@ -488,7 +489,7 @@ private void OnSignCommand(JObject jsonObjectToSign)
try
{
var snapshot = NeoSystem.StoreView;
- ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToSign.ToString(), snapshot);
+ var context = ContractParametersContext.Parse(jsonObjectToSign.ToString(), snapshot);
if (context.Network != NeoSystem.Settings.Network)
{
ConsoleHelper.Warning("Network mismatch.");
diff --git a/src/Neo.CLI/CLI/MainService.cs b/src/Neo.CLI/CLI/MainService.cs
index 449fa9c14b..9610c9c94b 100644
--- a/src/Neo.CLI/CLI/MainService.cs
+++ b/src/Neo.CLI/CLI/MainService.cs
@@ -13,7 +13,6 @@
using Neo.ConsoleService;
using Neo.Extensions;
using Neo.Json;
-using Neo.Ledger;
using Neo.Network.P2P;
using Neo.Network.P2P.Payloads;
using Neo.Plugins;
@@ -28,13 +27,11 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Numerics;
using System.Reflection;
using System.Security.Cryptography;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Array = System.Array;
@@ -171,108 +168,78 @@ private bool NoWallet()
return true;
}
- private byte[] LoadDeploymentScript(string nefFilePath, string? manifestFilePath, JObject? data, out NefFile nef, out ContractManifest manifest)
+ private static ContractParameter? LoadScript(string nefFilePath, string? manifestFilePath, JObject? data,
+ out NefFile nef, out ContractManifest manifest)
{
if (string.IsNullOrEmpty(manifestFilePath))
- {
manifestFilePath = Path.ChangeExtension(nefFilePath, ".manifest.json");
- }
// Read manifest
-
var info = new FileInfo(manifestFilePath);
- if (!info.Exists || info.Length >= Transaction.MaxTransactionSize)
- {
- throw new ArgumentException(nameof(manifestFilePath));
- }
+ if (!info.Exists)
+ throw new ArgumentException($"Contract manifest file not found at path: {manifestFilePath}. Please ensure the manifest file exists and the path is correct.", nameof(manifestFilePath));
+ if (info.Length >= Transaction.MaxTransactionSize)
+ throw new ArgumentException($"Contract manifest file size ({info.Length} bytes) exceeds the maximum allowed transaction size ({Transaction.MaxTransactionSize} bytes). Please check the file size and ensure it's within limits.", nameof(manifestFilePath));
manifest = ContractManifest.Parse(File.ReadAllBytes(manifestFilePath));
// Read nef
-
info = new FileInfo(nefFilePath);
- if (!info.Exists || info.Length >= Transaction.MaxTransactionSize)
- {
- throw new ArgumentException(nameof(nefFilePath));
- }
+ if (!info.Exists)
+ throw new ArgumentException($"Contract NEF file not found at path: {nefFilePath}. Please ensure the NEF file exists and the path is correct.", nameof(nefFilePath));
+ if (info.Length >= Transaction.MaxTransactionSize)
+ throw new ArgumentException($"Contract NEF file size ({info.Length} bytes) exceeds the maximum allowed transaction size ({Transaction.MaxTransactionSize} bytes). Please check the file size and ensure it's within limits.", nameof(nefFilePath));
nef = File.ReadAllBytes(nefFilePath).AsSerializable();
- ContractParameter? dataParameter = null;
+ // Basic script checks
+ nef.Script.IsScriptValid(manifest.Abi);
+
if (data is not null)
+ {
try
{
- dataParameter = ContractParameter.FromJson(data);
+ return ContractParameter.FromJson(data);
}
- catch
+ catch (Exception ex)
{
- throw new FormatException("invalid data");
+ throw new FormatException($"Invalid contract deployment data format. The provided JSON data could not be parsed as valid contract parameters. Original error: {ex.Message}", ex);
}
+ }
- // Basic script checks
- nef.Script.IsScriptValid(manifest.Abi);
+ return null;
+ }
- // Build script
+ private byte[] LoadDeploymentScript(string nefFilePath, string? manifestFilePath, JObject? data,
+ out NefFile nef, out ContractManifest manifest)
+ {
+ var parameter = LoadScript(nefFilePath, manifestFilePath, data, out nef, out manifest);
+ var manifestJson = manifest.ToJson().ToString();
- using (ScriptBuilder sb = new ScriptBuilder())
+ // Build script
+ using (var sb = new ScriptBuilder())
{
- if (dataParameter is not null)
- sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nef.ToArray(), manifest.ToJson().ToString(), dataParameter);
+ if (parameter is not null)
+ sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nef.ToArray(), manifestJson, parameter);
else
- sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nef.ToArray(), manifest.ToJson().ToString());
+ sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nef.ToArray(), manifestJson);
return sb.ToArray();
}
}
- private byte[] LoadUpdateScript(UInt160 scriptHash, string nefFilePath, string manifestFilePath, JObject? data, out NefFile nef, out ContractManifest manifest)
+ private byte[] LoadUpdateScript(UInt160 scriptHash, string nefFilePath, string manifestFilePath, JObject? data,
+ out NefFile nef, out ContractManifest manifest)
{
- if (string.IsNullOrEmpty(manifestFilePath))
- {
- manifestFilePath = Path.ChangeExtension(nefFilePath, ".manifest.json");
- }
-
- // Read manifest
-
- var info = new FileInfo(manifestFilePath);
- if (!info.Exists || info.Length >= Transaction.MaxTransactionSize)
- {
- throw new ArgumentException(nameof(manifestFilePath));
- }
-
- manifest = ContractManifest.Parse(File.ReadAllBytes(manifestFilePath));
-
- // Read nef
-
- info = new FileInfo(nefFilePath);
- if (!info.Exists || info.Length >= Transaction.MaxTransactionSize)
- {
- throw new ArgumentException(nameof(nefFilePath));
- }
-
- nef = File.ReadAllBytes(nefFilePath).AsSerializable();
-
- ContractParameter? dataParameter = null;
- if (data is not null)
- try
- {
- dataParameter = ContractParameter.FromJson(data);
- }
- catch
- {
- throw new FormatException("invalid data");
- }
-
- // Basic script checks
- nef.Script.IsScriptValid(manifest.Abi);
+ var parameter = LoadScript(nefFilePath, manifestFilePath, data, out nef, out manifest);
+ var manifestJson = manifest.ToJson().ToString();
// Build script
-
- using (ScriptBuilder sb = new ScriptBuilder())
+ using (var sb = new ScriptBuilder())
{
- if (dataParameter is null)
- sb.EmitDynamicCall(scriptHash, "update", nef.ToArray(), manifest.ToJson().ToString());
+ if (parameter is null)
+ sb.EmitDynamicCall(scriptHash, "update", nef.ToArray(), manifestJson);
else
- sb.EmitDynamicCall(scriptHash, "update", nef.ToArray(), manifest.ToJson().ToString(), dataParameter);
+ sb.EmitDynamicCall(scriptHash, "update", nef.ToArray(), manifestJson, parameter);
return sb.ToArray();
}
}
@@ -293,30 +260,29 @@ public void OpenWallet(string path, string password)
{
if (!File.Exists(path))
{
- throw new FileNotFoundException($"Wallet file \"{path}\" not found.");
+ throw new FileNotFoundException($"Wallet file not found at path: {path}. Please verify the file path is correct and the wallet file exists.", path);
}
if (CurrentWallet is not null) SignerManager.UnregisterSigner(CurrentWallet.Name);
- CurrentWallet = Wallet.Open(path, password, NeoSystem.Settings) ?? throw new NotSupportedException();
+ CurrentWallet = Wallet.Open(path, password, NeoSystem.Settings) ?? throw new NotSupportedException($"Failed to open wallet at path: {path}. The wallet format may not be supported or the password may be incorrect. Please verify the wallet file integrity and password.");
SignerManager.RegisterSigner(CurrentWallet.Name, CurrentWallet);
}
- public async void Start(CommandLineOptions options)
+ private static void ShowDllNotFoundError(DllNotFoundException ex)
{
- if (NeoSystem != null) return;
- bool verifyImport = !(options.NoVerify ?? false);
-
- Utility.LogLevel = options.Verbose;
- var protocol = ProtocolSettings.Load("config.json");
- CustomProtocolSettings(options, protocol);
- CustomApplicationSettings(options, Settings.Default);
- try
+ void DisplayError(string primaryMessage, string? secondaryMessage = null)
{
- NeoSystem = new NeoSystem(protocol, Settings.Default.Storage.Engine,
- string.Format(Settings.Default.Storage.Path, protocol.Network.ToString("X8")));
+ ConsoleHelper.Error(primaryMessage + Environment.NewLine +
+ (secondaryMessage != null ? secondaryMessage + Environment.NewLine : "") +
+ "Press any key to exit.");
+ Console.ReadKey();
+ Environment.Exit(-1);
}
- catch (DllNotFoundException ex) when (ex.Message.Contains("libleveldb"))
+
+ const string neoUrl = "https://github.com/neo-project/neo/releases";
+ const string levelDbUrl = "https://github.com/neo-ngd/leveldb/releases";
+ if (ex.Message.Contains("libleveldb"))
{
if (OperatingSystem.IsWindows())
{
@@ -327,31 +293,66 @@ public async void Start(CommandLineOptions options)
}
else
{
- DisplayError("DLL not found, please get libleveldb.dll.",
- "Download from https://github.com/neo-ngd/leveldb/releases");
+ DisplayError("DLL not found, please get libleveldb.dll.", $"Download from {levelDbUrl}");
}
}
else if (OperatingSystem.IsLinux())
{
DisplayError("Shared library libleveldb.so not found, please get libleveldb.so.",
- "Use command \"sudo apt-get install libleveldb-dev\" in terminal or download from https://github.com/neo-ngd/leveldb/releases");
+ $"Use command \"sudo apt-get install libleveldb-dev\" in terminal or download from {levelDbUrl}");
}
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsMacCatalyst())
{
- DisplayError("Shared library libleveldb.dylib not found, please get libleveldb.dylib.",
- "Use command \"brew install leveldb\" in terminal or download from https://github.com/neo-ngd/leveldb/releases");
+ // Check if the error message contains information about missing dependencies
+ if (ex.Message.Contains("libtcmalloc") && ex.Message.Contains("gperftools"))
+ {
+ DisplayError("LevelDB dependency 'gperftools' not found. This is required for libleveldb on macOS.",
+ "To fix this issue:\n" +
+ "1. Install gperftools: brew install gperftools\n" +
+ "2. Install leveldb: brew install leveldb\n" +
+ "3. If the issue persists, try: brew reinstall gperftools leveldb\n" +
+ "\n" +
+ "Note: The system is looking for libtcmalloc.4.dylib which is provided by gperftools.");
+ }
+ else
+ {
+ DisplayError("Shared library libleveldb.dylib not found or has missing dependencies.",
+ "To fix this issue:\n" +
+ "1. Install dependencies: brew install gperftools snappy\n" +
+ "2. Install leveldb: brew install leveldb\n" +
+ "3. If already installed, try: brew reinstall gperftools leveldb\n" +
+ $"\n" +
+ $"Alternative: Download pre-compiled binaries from {levelDbUrl}");
+ }
}
else
{
- DisplayError("Neo CLI is broken, please reinstall it.",
- "Download from https://github.com/neo-project/neo/releases");
+ DisplayError("Neo CLI is broken, please reinstall it.", $"Download from {neoUrl}");
}
- return;
}
- catch (DllNotFoundException)
+ else
+ {
+ DisplayError("Neo CLI is broken, please reinstall it.", $"Download from {neoUrl}");
+ }
+ }
+
+ public async void Start(CommandLineOptions options)
+ {
+ if (NeoSystem != null) return;
+ bool verifyImport = !(options.NoVerify ?? false);
+
+ Utility.LogLevel = options.Verbose;
+ var protocol = ProtocolSettings.Load("config.json");
+ CustomProtocolSettings(options, protocol);
+ CustomApplicationSettings(options, Settings.Default);
+ try
+ {
+ NeoSystem = new NeoSystem(protocol, Settings.Default.Storage.Engine,
+ string.Format(Settings.Default.Storage.Path, protocol.Network.ToString("X8")));
+ }
+ catch (DllNotFoundException ex)
{
- DisplayError("Neo CLI is broken, please reinstall it.",
- "Download from https://github.com/neo-project/neo/releases");
+ ShowDllNotFoundError(ex);
return;
}
@@ -360,15 +361,18 @@ public async void Start(CommandLineOptions options)
LocalNode = NeoSystem.LocalNode.Ask(new LocalNode.GetInstance()).Result;
// installing plugins
- var installTasks = options.Plugins?.Select(p => p).Where(p => !string.IsNullOrEmpty(p)).ToList().Select(p => InstallPluginAsync(p));
+ var installTasks = options.Plugins?.Select(p => p)
+ .Where(p => !string.IsNullOrEmpty(p))
+ .ToList()
+ .Select(p => InstallPluginAsync(p));
if (installTasks is not null)
{
await Task.WhenAll(installTasks);
}
+
foreach (var plugin in Plugin.Plugins)
{
// Register plugins commands
-
RegisterCommand(plugin, plugin.Name);
}
@@ -413,17 +417,6 @@ public async void Start(CommandLineOptions options)
ConsoleHelper.Error(ex.GetBaseException().Message);
}
}
-
- return;
-
- void DisplayError(string primaryMessage, string? secondaryMessage = null)
- {
- ConsoleHelper.Error(primaryMessage + Environment.NewLine +
- (secondaryMessage != null ? secondaryMessage + Environment.NewLine : "") +
- "Press any key to exit.");
- Console.ReadKey();
- Environment.Exit(-1);
- }
}
public void Stop()
@@ -432,14 +425,6 @@ public void Stop()
Interlocked.Exchange(ref _neoSystem, null)?.Dispose();
}
- private static void WriteLineWithoutFlicker(string message = "", int maxWidth = 80)
- {
- if (message.Length > 0) Console.Write(message);
- var spacesToErase = maxWidth - message.Length;
- if (spacesToErase < 0) spacesToErase = 0;
- Console.WriteLine(new string(' ', spacesToErase));
- }
-
///
/// Make and send transaction with script, sender
///
@@ -450,9 +435,8 @@ private void SendTransaction(byte[] script, UInt160? account = null, long datosh
{
if (NoWallet()) return;
- Signer[] signers = Array.Empty();
+ var signers = Array.Empty();
var snapshot = NeoSystem.StoreView;
-
if (account != null)
{
signers = CurrentWallet!.GetAccounts()
@@ -463,10 +447,9 @@ private void SendTransaction(byte[] script, UInt160? account = null, long datosh
try
{
- Transaction tx = CurrentWallet!.MakeTransaction(snapshot, script, account, signers, maxGas: datoshi);
+ var tx = CurrentWallet!.MakeTransaction(snapshot, script, account, signers, maxGas: datoshi);
ConsoleHelper.Info("Invoking script with: ", $"'{Convert.ToBase64String(tx.Script.Span)}'");
-
- using (ApplicationEngine engine = ApplicationEngine.Run(tx.Script, snapshot, container: tx, settings: NeoSystem.Settings, gas: datoshi))
+ using (var engine = ApplicationEngine.Run(tx.Script, snapshot, container: tx, settings: NeoSystem.Settings, gas: datoshi))
{
PrintExecutionOutput(engine, true);
if (engine.State == VMState.FAULT) return;
@@ -496,10 +479,10 @@ private void SendTransaction(byte[] script, UInt160? account = null, long datosh
/// Show result stack if it is true
/// Max fee for running the script, in the unit of datoshi, 1 datoshi = 1e-8 GAS
/// Return true if it was successful
- private bool OnInvokeWithResult(UInt160 scriptHash, string operation, out StackItem result, IVerifiable? verifiable = null, JArray? contractParameters = null, bool showStack = true, long datoshi = TestModeGas)
+ private bool OnInvokeWithResult(UInt160 scriptHash, string operation, out StackItem result,
+ IVerifiable? verifiable = null, JArray? contractParameters = null, bool showStack = true, long datoshi = TestModeGas)
{
- List parameters = new();
-
+ var parameters = new List();
if (contractParameters != null)
{
foreach (var contractParameter in contractParameters)
@@ -511,7 +494,7 @@ private bool OnInvokeWithResult(UInt160 scriptHash, string operation, out StackI
}
}
- ContractState contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
+ var contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
if (contract == null)
{
ConsoleHelper.Error("Contract does not exist.");
@@ -529,8 +512,7 @@ private bool OnInvokeWithResult(UInt160 scriptHash, string operation, out StackI
}
byte[] script;
-
- using (ScriptBuilder scriptBuilder = new ScriptBuilder())
+ using (var scriptBuilder = new ScriptBuilder())
{
scriptBuilder.EmitDynamicCall(scriptHash, operation, parameters.ToArray());
script = scriptBuilder.ToArray();
@@ -542,7 +524,7 @@ private bool OnInvokeWithResult(UInt160 scriptHash, string operation, out StackI
tx.Script = script;
}
- using ApplicationEngine engine = ApplicationEngine.Run(script, NeoSystem.StoreView, container: verifiable, settings: NeoSystem.Settings, gas: datoshi);
+ using var engine = ApplicationEngine.Run(script, NeoSystem.StoreView, container: verifiable, settings: NeoSystem.Settings, gas: datoshi);
PrintExecutionOutput(engine, showStack);
result = engine.State == VMState.FAULT ? StackItem.Null : engine.ResultStack.Peek();
return engine.State != VMState.FAULT;
@@ -575,7 +557,7 @@ static string GetExceptionMessage(Exception exception)
public UInt160? ResolveNeoNameServiceAddress(string domain)
{
if (Settings.Default.Contracts.NeoNameService == UInt160.Zero)
- throw new Exception("Neo Name Service (NNS): is disabled on this network.");
+ throw new Exception($"Neo Name Service (NNS) is not available on the current network. The NNS contract is not configured for network: {NeoSystem.Settings.Network}. Please ensure you are connected to a network that supports NNS functionality.");
using var sb = new ScriptBuilder();
sb.EmitDynamicCall(Settings.Default.Contracts.NeoNameService, "resolve", CallFlags.ReadOnly, domain, 16);
@@ -598,18 +580,18 @@ static string GetExceptionMessage(Exception exception)
}
else if (data is Null)
{
- throw new Exception($"Neo Name Service (NNS): \"{domain}\" domain not found.");
+ throw new Exception($"Neo Name Service (NNS): Domain '{domain}' was not found in the NNS registry. Please verify the domain name is correct and has been registered in the NNS system.");
}
- throw new Exception("Neo Name Service (NNS): Record invalid address format.");
+ throw new Exception($"Neo Name Service (NNS): The resolved record for domain '{domain}' contains an invalid address format. The NNS record exists but the address data is not in the expected format.");
}
else
{
if (appEng.FaultException is not null)
{
- throw new Exception($"Neo Name Service (NNS): \"{appEng.FaultException.Message}\".");
+ throw new Exception($"Neo Name Service (NNS): Failed to resolve domain '{domain}' due to contract execution error: {appEng.FaultException.Message}. Please verify the domain exists and try again.");
}
}
- throw new Exception($"Neo Name Service (NNS): \"{domain}\" domain not found.");
+ throw new Exception($"Neo Name Service (NNS): Domain '{domain}' was not found in the NNS registry. The resolution operation completed but no valid record was returned. Please verify the domain name is correct and has been registered.");
}
}
}
diff --git a/src/Neo.CLI/Dockerfile b/src/Neo.CLI/Dockerfile
index 100056b6f7..848ca5daf6 100644
--- a/src/Neo.CLI/Dockerfile
+++ b/src/Neo.CLI/Dockerfile
@@ -1,20 +1,43 @@
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS Build
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble
-# Run this from the repository root folder
-COPY src .
-COPY NuGet.Config /Neo.CLI
+# Install all dependencies in a single RUN to reduce layers and speed up build
+RUN apt-get update && \
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ net-tools telnet bash-completion wget lrzsz zip \
+ software-properties-common \
+ apt-transport-https \
+ build-essential \
+ unzip \
+ sqlite3 libsqlite3-dev libunwind8-dev \
+ screen vim ca-certificates gnupg && \
+ add-apt-repository ppa:dotnet/backports && \
+ apt-get update && \
+ apt-get install -y dotnet-sdk-9.0 && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
-WORKDIR /Neo.CLI
-RUN dotnet restore && dotnet publish -f net9.0 -c Release -o /app
+# Set working directory
+WORKDIR /neo
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:9.0 AS Final
-RUN apt-get update && apt-get install -y \
- screen \
- libleveldb-dev \
- sqlite3
-RUN rm -rf /var/lib/apt/lists/*
+# Copy and run scripts
+COPY prepare-node.sh .
+RUN chmod +x prepare-node.sh
-WORKDIR /Neo.CLI
-COPY --from=Build /app .
+# Accept build arg and set environment
+ARG NEO_VERSION
+ENV NEO_VERSION=${NEO_VERSION}
-ENTRYPOINT ["screen","-DmS","node","dotnet","neo-cli.dll","-r"]
+RUN ./prepare-node.sh v${NEO_VERSION}
+
+# Modify config in-place
+RUN sed -i 's/"BindAddress":[^,]*/"BindAddress": "0.0.0.0"/' neo-cli/Plugins/RpcServer/RpcServer.json
+
+# Copy and set permissions
+COPY start.sh .
+RUN chmod -R +x ./neo-cli
+
+# Expose port (optional but informative)
+EXPOSE 10332
+
+# Define entrypoint
+ENTRYPOINT ["sh", "./start.sh"]
\ No newline at end of file
diff --git a/src/Neo.CLI/Makefile b/src/Neo.CLI/Makefile
new file mode 100644
index 0000000000..610ca95822
--- /dev/null
+++ b/src/Neo.CLI/Makefile
@@ -0,0 +1,12 @@
+# Extract version from Directory.Build.props using shell command
+VERSION := $(shell grep "" ../Directory.Build.props | head -1 | sed -E 's/.*([^<]+)<\/VersionPrefix>.*/\1/')
+
+.PHONY: build
+
+build:
+ifeq ($(strip $(VERSION)),)
+ $(error VersionPrefix not found in ../Directory.Build.props)
+endif
+ docker build --build-arg NEO_VERSION=$(VERSION) -t neo-node:$(VERSION) .
+ @if [ $$(docker ps -a -q -f name=neo-cli) ]; then docker rm -f neo-cli; fi
+ docker run --name neo-cli -p 10332:10332 neo-node:$(VERSION)
\ No newline at end of file
diff --git a/src/Neo.CLI/Settings.cs b/src/Neo.CLI/Settings.cs
index 930826f7f0..d66068c65a 100644
--- a/src/Neo.CLI/Settings.cs
+++ b/src/Neo.CLI/Settings.cs
@@ -13,6 +13,7 @@
using Neo.Network.P2P;
using Neo.Persistence.Providers;
using System;
+using System.IO;
using System.Reflection;
using System.Threading;
@@ -46,7 +47,8 @@ public static Settings Default
{
if (s_default == null)
{
- var config = new ConfigurationBuilder().AddJsonFile("config.json", optional: true).Build();
+ var configFile = ProtocolSettings.FindFile("config.json", Environment.CurrentDirectory);
+ var config = new ConfigurationBuilder().AddJsonFile(configFile, optional: true).Build();
Initialize(config);
}
return Custom ?? s_default!;
@@ -160,7 +162,9 @@ public ContractsSettings(IConfigurationSection section)
NeoNameService = hash;
}
else
+ {
throw new ArgumentException("Neo Name Service (NNS): NeoNameService hash is invalid. Check your config.json.", nameof(NeoNameService));
+ }
}
}
diff --git a/src/Neo.CLI/prepare-node.sh b/src/Neo.CLI/prepare-node.sh
new file mode 100644
index 0000000000..95ca8a4a83
--- /dev/null
+++ b/src/Neo.CLI/prepare-node.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+set -e
+
+# if $1 not provided, show usage
+if [ -z "$1" ]; then
+ echo "Usage: $0 [plugins-version]"
+ echo "Example: $0 x.y.z"
+ echo "Example: $0 x.y.z1 x.y.z2"
+ exit 1
+fi
+
+NEO_VERSION=$1
+PLUGINS_VERSION=$2
+
+# if $NEO_VERSION not start with v, then add v to it
+if [[ $NEO_VERSION != v* ]]; then
+ NEO_VERSION=v$NEO_VERSION
+fi
+
+# Allow CLI and Plugins in different versions in case only CLI is released or for any other test usage
+if [ -z "$2" ]; then
+ PLUGINS_VERSION=$NEO_VERSION
+elif [[ $PLUGINS_VERSION != v* ]]; then
+ PLUGINS_VERSION=v$PLUGINS_VERSION
+fi
+
+echo "Downloading neo node $NEO_VERSION"
+wget https://github.com/neo-project/neo/releases/download/$NEO_VERSION/neo-cli.$NEO_VERSION-linux-x64.tar.gz
+mkdir neo-cli-linux-x64
+tar -zxvf neo-cli.$NEO_VERSION-linux-x64.tar.gz -C neo-cli-linux-x64/
+mv neo-cli-linux-x64 neo-cli
+
+echo "Downloading plugins $PLUGINS_VERSION"
+wget https://github.com/neo-project/neo/releases/download/$PLUGINS_VERSION/ApplicationLogs.zip
+wget https://github.com/neo-project/neo/releases/download/$PLUGINS_VERSION/RpcServer.zip
+wget https://github.com/neo-project/neo/releases/download/$PLUGINS_VERSION/TokensTracker.zip
+
+unzip -n ApplicationLogs.zip -d ./neo-cli/
+unzip -n RpcServer.zip -d ./neo-cli/
+unzip -n TokensTracker.zip -d ./neo-cli/
+
+rm neo-cli.$NEO_VERSION-linux-x64.tar.gz ApplicationLogs.zip RpcServer.zip TokensTracker.zip
+
+echo "Node Is Ready!"
diff --git a/src/Neo.CLI/start.sh b/src/Neo.CLI/start.sh
new file mode 100644
index 0000000000..73890cfc92
--- /dev/null
+++ b/src/Neo.CLI/start.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# start neo-cli,output log into neo.log
+screen -dmS neo bash -c "./neo-cli/neo-cli > neo.log 2>&1"
+
+# wait for neo.log
+while [ ! -f neo.log ]; do
+ sleep 0.5
+done
+
+tail -f neo.log
\ No newline at end of file
diff --git a/src/Neo.ConsoleService/CommandQuoteToken.cs b/src/Neo.ConsoleService/CommandQuoteToken.cs
deleted file mode 100644
index a4de9651d6..0000000000
--- a/src/Neo.ConsoleService/CommandQuoteToken.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandQuoteToken.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-using System;
-using System.Diagnostics;
-
-namespace Neo.ConsoleService
-{
- [DebuggerDisplay("Value={Value}, Value={Value}")]
- internal class CommandQuoteToken : CommandToken
- {
- ///
- /// Constructor
- ///
- /// Offset
- /// Value
- public CommandQuoteToken(int offset, char value) : base(CommandTokenType.Quote, offset)
- {
- if (value != '\'' && value != '"')
- {
- throw new ArgumentException("Not valid quote");
- }
-
- Value = value.ToString();
- }
-
- ///
- /// Parse command line quotes
- ///
- /// Command line
- /// Index
- /// CommandQuoteToken
- internal static CommandQuoteToken Parse(string commandLine, ref int index)
- {
- var c = commandLine[index];
-
- if (c == '\'' || c == '"')
- {
- index++;
- return new CommandQuoteToken(index - 1, c);
- }
-
- throw new ArgumentException("No quote found");
- }
- }
-}
diff --git a/src/Neo.ConsoleService/CommandSpaceToken.cs b/src/Neo.ConsoleService/CommandSpaceToken.cs
deleted file mode 100644
index fc2c446611..0000000000
--- a/src/Neo.ConsoleService/CommandSpaceToken.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandSpaceToken.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-using System;
-using System.Diagnostics;
-
-namespace Neo.ConsoleService
-{
- [DebuggerDisplay("Value={Value}, Count={Count}")]
- internal class CommandSpaceToken : CommandToken
- {
- ///
- /// Count
- ///
- public int Count { get; }
-
- ///
- /// Constructor
- ///
- /// Offset
- /// Count
- public CommandSpaceToken(int offset, int count) : base(CommandTokenType.Space, offset)
- {
- Value = "".PadLeft(count, ' ');
- Count = count;
- }
-
- ///
- /// Parse command line spaces
- ///
- /// Command line
- /// Index
- /// CommandSpaceToken
- internal static CommandSpaceToken Parse(string commandLine, ref int index)
- {
- int offset = index;
- int count = 0;
-
- for (int ix = index, max = commandLine.Length; ix < max; ix++)
- {
- if (commandLine[ix] == ' ')
- {
- count++;
- }
- else
- {
- break;
- }
- }
-
- if (count == 0) throw new ArgumentException("No spaces found");
-
- index += count;
- return new CommandSpaceToken(offset, count);
- }
- }
-}
diff --git a/src/Neo.ConsoleService/CommandStringToken.cs b/src/Neo.ConsoleService/CommandStringToken.cs
deleted file mode 100644
index a8bf67e2db..0000000000
--- a/src/Neo.ConsoleService/CommandStringToken.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandStringToken.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-using System;
-using System.Diagnostics;
-
-namespace Neo.ConsoleService
-{
- [DebuggerDisplay("Value={Value}, RequireQuotes={RequireQuotes}")]
- internal class CommandStringToken : CommandToken
- {
- ///
- /// Require quotes
- ///
- public bool RequireQuotes { get; }
-
- ///
- /// Constructor
- ///
- /// Offset
- /// Value
- public CommandStringToken(int offset, string value) : base(CommandTokenType.String, offset)
- {
- Value = value;
- RequireQuotes = value.IndexOfAny(new char[] { '\'', '"' }) != -1;
- }
-
- ///
- /// Parse command line spaces
- ///
- /// Command line
- /// Index
- /// Quote (could be null)
- /// CommandSpaceToken
- internal static CommandStringToken Parse(string commandLine, ref int index, CommandQuoteToken? quote)
- {
- int end;
- int offset = index;
-
- if (quote != null)
- {
- var ix = index;
-
- do
- {
- end = commandLine.IndexOf(quote.Value[0], ix + 1);
-
- if (end == -1)
- {
- throw new ArgumentException("String not closed");
- }
-
- if (IsScaped(commandLine, end - 1))
- {
- ix = end;
- end = -1;
- }
- }
- while (end < 0);
- }
- else
- {
- end = commandLine.IndexOf(' ', index + 1);
- }
-
- if (end == -1)
- {
- end = commandLine.Length;
- }
-
- var ret = new CommandStringToken(offset, commandLine.Substring(index, end - index));
- index += end - index;
- return ret;
- }
-
- private static bool IsScaped(string commandLine, int index)
- {
- // TODO: Scape the scape
-
- return (commandLine[index] == '\\');
- }
- }
-}
diff --git a/src/Neo.ConsoleService/CommandToken.cs b/src/Neo.ConsoleService/CommandToken.cs
index 9ac1d90458..9f07e8ac98 100644
--- a/src/Neo.ConsoleService/CommandToken.cs
+++ b/src/Neo.ConsoleService/CommandToken.cs
@@ -9,221 +9,41 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.
-using System;
-using System.Collections.Generic;
-using System.Text;
-
namespace Neo.ConsoleService
{
- internal abstract class CommandToken
+ public readonly struct CommandToken(int offset, string value, char quoteChar)
{
- ///
- /// Offset
- ///
- public int Offset { get; }
-
- ///
- /// Type
- ///
- public CommandTokenType Type { get; }
-
- ///
- /// Value
- ///
- public string Value { get; protected init; } = string.Empty;
+ public const char NoQuoteChar = '\0';
+ public const char NoEscapedChar = '`';
///
- /// Constructor
+ /// The start offset of the token in the command line
///
- /// Type
- /// Offset
- protected CommandToken(CommandTokenType type, int offset)
- {
- Type = type;
- Offset = offset;
- }
+ public readonly int Offset { get; } = offset;
///
- /// Parse command line
+ /// The value of the token
///
- /// Command line
- ///
- public static IEnumerable Parse(string commandLine)
- {
- CommandToken? lastToken = null;
-
- for (int index = 0, count = commandLine.Length; index < count;)
- {
- switch (commandLine[index])
- {
- case ' ':
- {
- lastToken = CommandSpaceToken.Parse(commandLine, ref index);
- yield return lastToken;
- break;
- }
- case '"':
- case '\'':
- {
- // "'"
- if (lastToken is CommandQuoteToken quote && quote.Value[0] != commandLine[index])
- {
- goto default;
- }
-
- lastToken = CommandQuoteToken.Parse(commandLine, ref index);
- yield return lastToken;
- break;
- }
- default:
- {
- lastToken = CommandStringToken.Parse(commandLine, ref index,
- lastToken is CommandQuoteToken quote ? quote : null);
-
- if (lastToken is not null)
- {
- yield return lastToken;
- }
- break;
- }
- }
- }
- }
+ public readonly string Value { get; } = value;
///
- /// Create string arguments
+ /// Whether the token is an indicator. Like --key key.
///
- /// Tokens
- /// Remove escape
- /// Arguments
- public static string[] ToArguments(IEnumerable tokens, bool removeEscape = true)
- {
- var list = new List();
-
- CommandToken? lastToken = null;
-
- foreach (var token in tokens)
- {
- if (token is CommandStringToken str)
- {
- if (removeEscape && lastToken is CommandQuoteToken quote)
- {
- // Remove escape
-
- list.Add(str.Value.Replace("\\" + quote.Value, quote.Value));
- }
- else
- {
- list.Add(str.Value);
- }
- }
-
- lastToken = token;
- }
-
- return list.ToArray();
- }
+ public readonly bool IsIndicator => _quoteChar == NoQuoteChar && Value.StartsWith("--");
///
- /// Create a string from token list
+ /// The quote character of the token. It can be ', " or `.
///
- /// Tokens
- /// String
- public static string ToString(IEnumerable tokens)
- {
- var sb = new StringBuilder();
-
- foreach (var token in tokens)
- {
- sb.Append(token.Value);
- }
-
- return sb.ToString();
- }
+ private readonly char _quoteChar = quoteChar;
///
- /// Trim
+ /// The raw value of the token(includes quote character if raw value is quoted)
///
- /// Args
- public static void Trim(List args)
- {
- // Trim start
-
- while (args.Count > 0 && args[0].Type == CommandTokenType.Space)
- {
- args.RemoveAt(0);
- }
-
- // Trim end
-
- while (args.Count > 0 && args[^1].Type == CommandTokenType.Space)
- {
- args.RemoveAt(args.Count - 1);
- }
- }
+ public readonly string RawValue => _quoteChar == NoQuoteChar ? Value : $"{_quoteChar}{Value}{_quoteChar}";
///
- /// Read String
+ /// Whether the token is white spaces(includes empty) or not
///
- /// Args
- /// Consume all if not quoted
- /// String
- public static string? ReadString(List args, bool consumeAll)
- {
- Trim(args);
-
- var quoted = false;
-
- if (args.Count > 0 && args[0].Type == CommandTokenType.Quote)
- {
- quoted = true;
- args.RemoveAt(0);
- }
- else
- {
- if (consumeAll)
- {
- // Return all if it's not quoted
-
- var ret = ToString(args);
- args.Clear();
-
- return ret;
- }
- }
-
- if (args.Count > 0)
- {
- switch (args[0])
- {
- case CommandQuoteToken _:
- {
- if (quoted)
- {
- args.RemoveAt(0);
- return "";
- }
-
- throw new ArgumentException("Unmatched quote");
- }
- case CommandSpaceToken _: throw new ArgumentException("Unmatched space");
- case CommandStringToken str:
- {
- args.RemoveAt(0);
-
- if (quoted && args.Count > 0 && args[0].Type == CommandTokenType.Quote)
- {
- // Remove last quote
-
- args.RemoveAt(0);
- }
-
- return str.Value;
- }
- }
- }
-
- return null;
- }
+ public readonly bool IsWhiteSpace => _quoteChar == NoQuoteChar && string.IsNullOrWhiteSpace(Value);
}
}
diff --git a/src/Neo.ConsoleService/CommandTokenizer.cs b/src/Neo.ConsoleService/CommandTokenizer.cs
new file mode 100644
index 0000000000..655266213c
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandTokenizer.cs
@@ -0,0 +1,205 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// CommandTokenizer.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace Neo.ConsoleService
+{
+ public static class CommandTokenizer
+ {
+ private static char EscapedChar(char ch)
+ {
+ return ch switch
+ {
+ '\\' => '\\',
+ '"' => '"',
+ '\'' => '\'',
+ 'n' => '\n',
+ 'r' => '\r',
+ 't' => '\t',
+ 'v' => '\v',
+ 'b' => '\b',
+ 'f' => '\f',
+ 'a' => '\a',
+ 'e' => '\e',
+ '0' => '\0',
+ ' ' => ' ',
+ _ => throw new ArgumentException($"Invalid escaped character: \\{ch}. " +
+ "If you don't want to use escape character, please use backtick(`) to wrap the string.")
+ };
+ }
+
+ private static (char, int) EscapedChar(string commandLine, int index)
+ {
+ index++; // next char after \
+ if (index >= commandLine.Length)
+ {
+ throw new ArgumentException("Invalid escape sequence. The command line ends with a backslash character.");
+ }
+
+ if (commandLine[index] == 'x')
+ {
+ if (index + 2 >= commandLine.Length)
+ throw new ArgumentException("Invalid escape sequence. Too few hex digits after \\x");
+
+ if (!byte.TryParse(commandLine.AsSpan(index + 1, 2), NumberStyles.AllowHexSpecifier, null, out var ch))
+ {
+ throw new ArgumentException($"Invalid hex digits after \\x. " +
+ "If you don't want to use escape character, please use backtick(`) to wrap the string.");
+ }
+
+ return new((char)ch, 1 + 2);
+ }
+
+ if (commandLine[index] == 'u')
+ {
+ if (index + 4 >= commandLine.Length)
+ throw new ArgumentException("Invalid escape sequence. Too few hex digits after \\u");
+
+ if (!ushort.TryParse(commandLine.AsSpan(index + 1, 4), NumberStyles.AllowHexSpecifier, null, out var ch))
+ {
+ throw new ArgumentException($"Invalid hex digits after \\u. " +
+ "If you don't want to use escape character, please use backtick(`) to wrap the string.");
+ }
+
+ // handle invalid surrogate pairs if needed, but good enough for a cli tool
+ return new((char)ch, 1 + 4);
+ }
+
+ return new(EscapedChar(commandLine[index]), 1);
+ }
+
+ ///
+ /// Tokenize a command line
+ ///
+ /// The command line to tokenize
+ /// The tokens
+ public static List Tokenize(this string commandLine)
+ {
+ var tokens = new List();
+ var token = new StringBuilder();
+ var quoteChar = CommandToken.NoQuoteChar;
+ var addToken = (int index, char quote) =>
+ {
+ var value = token.ToString();
+ tokens.Add(new CommandToken(index - value.Length, value, quote));
+ token.Clear();
+ };
+
+ for (var index = 0; index < commandLine.Length; index++)
+ {
+ var ch = commandLine[index];
+ if (ch == '\\' && quoteChar != CommandToken.NoEscapedChar)
+ {
+ (var escapedChar, var length) = EscapedChar(commandLine, index);
+ token.Append(escapedChar);
+ index += length;
+ }
+ else if (quoteChar != CommandToken.NoQuoteChar)
+ {
+ if (ch == quoteChar)
+ {
+ addToken(index, quoteChar);
+ quoteChar = CommandToken.NoQuoteChar;
+ }
+ else
+ {
+ token.Append(ch);
+ }
+ }
+ else if (ch == '"' || ch == '\'' || ch == CommandToken.NoEscapedChar)
+ {
+ if (token.Length == 0) // If ch is the first char. To keep consistency with legacy behavior
+ {
+ quoteChar = ch;
+ }
+ else
+ {
+ token.Append(ch); // If ch is not the first char, append it as a normal char
+ }
+ }
+ else if (char.IsWhiteSpace(ch))
+ {
+ if (token.Length > 0) addToken(index, quoteChar);
+
+ token.Append(ch);
+ while (index + 1 < commandLine.Length && char.IsWhiteSpace(commandLine[index + 1]))
+ {
+ token.Append(commandLine[++index]);
+ }
+ addToken(index, quoteChar);
+ }
+ else
+ {
+ token.Append(ch);
+ }
+ }
+
+ if (quoteChar != CommandToken.NoQuoteChar) // uncompleted quote
+ throw new ArgumentException($"Unmatched quote({quoteChar})");
+ if (token.Length > 0) addToken(commandLine.Length, quoteChar);
+ return tokens;
+ }
+
+ ///
+ /// Join the raw token values into a single string without prefix and suffix white spaces
+ ///
+ /// The list of tokens
+ /// The joined string
+ public static string JoinRaw(this IList tokens)
+ {
+ return string.Join("", tokens.Trim().Select(t => t.RawValue));
+ }
+
+ ///
+ /// Consume the first token from the list without prefix and suffix white spaces
+ ///
+ /// The list of tokens
+ /// The value of the first non-white space token
+ public static string Consume(this IList tokens)
+ {
+ tokens.Trim();
+ if (tokens.Count == 0) return "";
+
+ var token = tokens[0];
+ tokens.RemoveAt(0);
+ return token.Value;
+ }
+
+ ///
+ /// Consume all tokens from the list and join them without prefix and suffix white spaces
+ ///
+ /// The list of tokens
+ /// The joined value of all tokens without prefix and suffix white spaces
+ public static string ConsumeAll(this IList tokens)
+ {
+ var result = tokens.Trim().JoinRaw();
+ tokens.Clear();
+ return result;
+ }
+
+ ///
+ /// Remove the prefix and suffix white spaces from the list of tokens
+ ///
+ /// The list of tokens
+ /// The trimmed list of tokens
+ public static IList Trim(this IList tokens)
+ {
+ while (tokens.Count > 0 && tokens[0].IsWhiteSpace) tokens.RemoveAt(0);
+ while (tokens.Count > 0 && tokens[^1].IsWhiteSpace) tokens.RemoveAt(tokens.Count - 1);
+ return tokens;
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleCommandMethod.cs b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
index 4df5469c10..2fbdf9c29c 100644
--- a/src/Neo.ConsoleService/ConsoleCommandMethod.cs
+++ b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
@@ -64,58 +64,20 @@ public ConsoleCommandMethod(object instance, MethodInfo method, ConsoleCommandAt
}
///
- /// Is this command
+ /// Match this command or not
///
/// Tokens
- /// Consumed Arguments
- /// True if is this command
- public bool IsThisCommand(CommandToken[] tokens, out int consumedArgs)
+ /// Tokens consumed, 0 if not match
+ public int IsThisCommand(IReadOnlyList tokens)
{
- int checks = Verbs.Length;
- bool quoted = false;
- var tokenList = new List(tokens);
-
- while (checks > 0 && tokenList.Count > 0)
- {
- switch (tokenList[0])
- {
- case CommandSpaceToken _:
- {
- tokenList.RemoveAt(0);
- break;
- }
- case CommandQuoteToken _:
- {
- quoted = !quoted;
- tokenList.RemoveAt(0);
- break;
- }
- case CommandStringToken str:
- {
- if (Verbs[^checks] != str.Value.ToLowerInvariant())
- {
- consumedArgs = 0;
- return false;
- }
-
- checks--;
- tokenList.RemoveAt(0);
- break;
- }
- }
- }
-
- if (quoted && tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Quote)
+ int matched = 0, consumed = 0;
+ for (; matched < Verbs.Length && consumed < tokens.Count; consumed++)
{
- tokenList.RemoveAt(0);
+ if (tokens[consumed].IsWhiteSpace) continue;
+ if (tokens[consumed].Value != Verbs[matched]) return 0;
+ matched++;
}
-
- // Trim start
-
- while (tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Space) tokenList.RemoveAt(0);
-
- consumedArgs = tokens.Length - tokenList.Count;
- return checks == 0;
+ return matched == Verbs.Length ? consumed : 0;
}
}
}
diff --git a/src/Neo.ConsoleService/ConsoleHelper.cs b/src/Neo.ConsoleService/ConsoleHelper.cs
index 52267fcbd0..6e9768de79 100644
--- a/src/Neo.ConsoleService/ConsoleHelper.cs
+++ b/src/Neo.ConsoleService/ConsoleHelper.cs
@@ -17,6 +17,9 @@ namespace Neo.ConsoleService
{
public static class ConsoleHelper
{
+ private const string PrintableASCIIChars =
+ " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
+
private static readonly ConsoleColorSet InfoColor = new(ConsoleColor.Cyan);
private static readonly ConsoleColorSet WarningColor = new(ConsoleColor.Yellow);
private static readonly ConsoleColorSet ErrorColor = new(ConsoleColor.Red);
@@ -79,9 +82,6 @@ private static void Log(string tag, ConsoleColorSet colorSet, string msg)
public static string ReadUserInput(string prompt, bool password = false)
{
- const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
- var sb = new StringBuilder();
-
if (!string.IsNullOrEmpty(prompt))
{
Console.Write(prompt + ": ");
@@ -91,6 +91,7 @@ public static string ReadUserInput(string prompt, bool password = false)
var prevForeground = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
+ var sb = new StringBuilder();
if (Console.IsInputRedirected)
{
// neo-gui Console require it
@@ -102,7 +103,7 @@ public static string ReadUserInput(string prompt, bool password = false)
do
{
key = Console.ReadKey(true);
- if (t.IndexOf(key.KeyChar) != -1)
+ if (PrintableASCIIChars.Contains(key.KeyChar))
{
sb.Append(key.KeyChar);
Console.Write(password ? '*' : key.KeyChar);
@@ -123,7 +124,6 @@ public static string ReadUserInput(string prompt, bool password = false)
public static SecureString ReadSecureString(string prompt)
{
- const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
SecureString securePwd = new SecureString();
ConsoleKeyInfo key;
@@ -138,7 +138,7 @@ public static SecureString ReadSecureString(string prompt)
do
{
key = Console.ReadKey(true);
- if (t.IndexOf(key.KeyChar) != -1)
+ if (PrintableASCIIChars.Contains(key.KeyChar))
{
securePwd.AppendChar(key.KeyChar);
Console.Write('*');
diff --git a/src/Neo.ConsoleService/ConsoleServiceBase.cs b/src/Neo.ConsoleService/ConsoleServiceBase.cs
index 79af19bee3..15a574053f 100644
--- a/src/Neo.ConsoleService/ConsoleServiceBase.cs
+++ b/src/Neo.ConsoleService/ConsoleServiceBase.cs
@@ -35,52 +35,116 @@ public abstract class ConsoleServiceBase
protected bool ShowPrompt { get; set; } = true;
+ protected bool IsBackground { get; set; } = false;
+
private bool _running;
private readonly CancellationTokenSource _shutdownTokenSource = new();
private readonly CountdownEvent _shutdownAcknowledged = new(1);
private readonly Dictionary> _verbs = new();
private readonly Dictionary _instances = new();
- private readonly Dictionary, bool, object>> _handlers = new();
+ private readonly Dictionary, bool, object>> _handlers = new();
private readonly List _commandHistory = new();
- private bool OnCommand(string commandLine)
+ ///
+ /// Parse sequential arguments.
+ /// For example, if a method defined as `void Method(string arg1, int arg2, bool arg3)`,
+ /// the arguments will be parsed as `"arg1" 2 true`.
+ ///
+ /// the MethodInfo of the called method
+ /// the raw arguments
+ /// the parsed arguments
+ /// Missing argument
+ internal object?[] ParseSequentialArguments(MethodInfo method, IList args)
+ {
+ var parameters = method.GetParameters();
+ var arguments = new List