diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..07843745db
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,14 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
+{
+ "name": "C# (.NET)",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:1-7.0-jammy",
+ "postCreateCommand": "dotnet restore && dotnet build",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-dotnettools.csdevkit"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 175ec7b14e..269a7e444b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -6,14 +6,297 @@
# dotnet tool update -g dotnet-format
# remember to have: git config --global core.autocrlf false #(which is usually default)
+# top-most EditorConfig file
root = true
-# Every file
-
+# Don't use tabs for indentation.
[*]
+indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
end_of_line = lf
+# (Please don't specify an indent_size here; that has too many unintended consequences.)
+spelling_exclusion_path = SpellingExclusions.dic
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# Powershell files
+[*.ps1]
+indent_size = 2
+
+# Shell script files
+[*.sh]
+end_of_line = lf
+indent_size = 2
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+# Member can be made 'readonly'
+csharp_style_prefer_readonly_struct_member = true
+dotnet_diagnostic.IDE0251.severity = warning
+dotnet_diagnostic.IDE0044.severity = warning
+
dotnet_diagnostic.CS1591.severity = silent
+
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = false
+dotnet_separate_import_directive_groups = false
+# Avoid "this." and "Me." if not necessary
+dotnet_style_qualification_for_field = false:refactoring
+dotnet_style_qualification_for_property = false:refactoring
+dotnet_style_qualification_for_method = false:refactoring
+dotnet_style_qualification_for_event = false:refactoring
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+
+# Whitespace options
+dotnet_style_allow_multiple_blank_lines_experimental = false
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+
+# Non-private readonly fields are PascalCase
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
+
+dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
+
+dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
+
+# Constants are PascalCase
+dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
+dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
+
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+dotnet_naming_style.constant_style.capitalization = pascal_case
+
+# Static fields are camelCase and start with s_
+dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
+dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
+
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+dotnet_naming_style.static_field_style.capitalization = camel_case
+dotnet_naming_style.static_field_style.required_prefix = s_
+
+# Instance fields are camelCase and start with _
+dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+dotnet_naming_style.instance_field_style.required_prefix = _
+
+# Locals and parameters are camelCase
+dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
+dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
+
+dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+
+# By default, name items with PascalCase
+dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
+dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
+
+dotnet_naming_symbols.all_members.applicable_kinds = *
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+file_header_template = Copyright (C) 2015-2024 The Neo Project.\n\n{fileName} file belongs to the neo project and is free\nsoftware distributed under the MIT software license, see the\naccompanying file LICENSE in the main directory of the\nrepository or http://www.opensource.org/licenses/mit-license.php\nfor more details.\n\nRedistribution and use in source and binary forms with or without\nmodifications are permitted.
+
+# Require file header
+dotnet_diagnostic.IDE0073.severity = error
+
+# RS0016: Only enable if API files are present
+dotnet_public_api_analyzer.require_api_files = true
+
+# IDE0055: Fix formatting
+# Workaround for https://github.com/dotnet/roslyn/issues/70570
+dotnet_diagnostic.IDE0055.severity = warning
+
+# CSharp code style settings:
+[*.cs]
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Whitespace options
+csharp_style_allow_embedded_statements_on_same_line_experimental = false
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
+
+# Prefer "var" everywhere
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = false:none
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_operators = false:none
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Blocks are allowed
+csharp_prefer_braces = true:silent
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+# IDE0060: Remove unused parameter
+dotnet_diagnostic.IDE0060.severity = none
+
+[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStudio}/**/*.{cs,vb}]
+
+# IDE0011: Add braces
+csharp_prefer_braces = when_multiline:warning
+# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201
+dotnet_diagnostic.IDE0011.severity = warning
+
+# IDE0040: Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = warning
+
+# IDE0052: Remove unread private member
+dotnet_diagnostic.IDE0052.severity = warning
+
+# IDE0059: Unnecessary assignment to a value
+dotnet_diagnostic.IDE0059.severity = warning
+
+# CA1012: Abstract types should not have public constructors
+dotnet_diagnostic.CA1012.severity = warning
+
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = warning
+
+# Prefer "var" everywhere
+dotnet_diagnostic.IDE0007.severity = warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = true:warning
+
+# csharp_style_allow_embedded_statements_on_same_line_experimental
+dotnet_diagnostic.IDE2001.severity = warning
+
+# csharp_style_allow_blank_lines_between_consecutive_braces_experimental
+dotnet_diagnostic.IDE2002.severity = warning
+
+# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental
+dotnet_diagnostic.IDE2004.severity = warning
+
+# csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental
+dotnet_diagnostic.IDE2005.severity = warning
+
+# csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental
+dotnet_diagnostic.IDE2006.severity = warning
+
+[src/{VisualStudio}/**/*.{cs,vb}]
+# CA1822: Make member static
+# There is a risk of accidentally breaking an internal API that partners rely on though IVT.
+dotnet_code_quality.CA1822.api_surface = private
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..0c37abbc2d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,64 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text eol=lf
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+*.sln text eol=crlf
+#*.csproj text eol=crlf
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+*.jpg binary
+*.png binary
+*.gif binary
+*.ico binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md
index 4c2e7a8977..9b98d24f99 100644
--- a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md
+++ b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md
@@ -12,10 +12,6 @@ A summary of the problem you want to solve or metric you want to improve
**Do you have any solution you want to propose?**
A clear and concise description of what you expect with this change.
-**Neo Version**
-- Neo 2
-- Neo 3
-
**Where in the software does this update applies to?**
- Compiler
- Consensus
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..7b043f14a7
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,35 @@
+# Description
+
+
+
+Fixes # (issue)
+
+## Type of change
+
+
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] This change requires a documentation update
+
+# How Has This Been Tested?
+
+
+
+- [ ] Test A
+- [ ] Test B
+
+**Test Configuration**:
+
+
+# Checklist:
+
+- [ ] My code follows the style guidelines of this project
+- [ ] I have performed a self-review of my code
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
+- [ ] I have added tests that prove my fix is effective or that my feature works
+- [ ] New and existing unit tests pass locally with my changes
+- [ ] Any dependent changes have been merged and published in downstream modules
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 743c8febe6..da9e47f20b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -2,77 +2,100 @@ name: .NET Core Test and Publish
on:
push:
- branches: master
+ branches: [master]
pull_request:
env:
- DOTNET_VERSION: 6.0.x
+ DOTNET_VERSION: 7.0.x
jobs:
Test:
- runs-on: ubuntu-latest
+ timeout-minutes: 15
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
steps:
- name: Checkout
- uses: actions/checkout@v2
- - name: Setup .NET Core
- uses: actions/setup-dotnet@v1
+ uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Check format
+ if: matrix.os == 'ubuntu-latest'
run: |
dotnet format --verify-no-changes --verbosity diagnostic
- name: Test
+ if: matrix.os != 'ubuntu-latest'
+ run: |
+ dotnet test
+ - name: Test for coverall
+ if: matrix.os == 'ubuntu-latest'
run: |
find tests -name *.csproj | xargs -I % dotnet add % package coverlet.msbuild
- dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage/lcov
+ dotnet test ./tests/Neo.Cryptography.BLS12_381.Tests /p:CollectCoverage=true /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage-join/
+ dotnet test ./tests/Neo.ConsoleService.Tests /p:CollectCoverage=true /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage-join/ /p:MergeWith=${GITHUB_WORKSPACE}/coverage-join/coverage.json
+ dotnet test ./tests/Neo.UnitTests /p:CollectCoverage=true /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage-join/ /p:MergeWith=${GITHUB_WORKSPACE}/coverage-join/coverage.net7.0.json
+ dotnet test ./tests/Neo.VM.Tests /p:CollectCoverage=true /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage-join/ /p:MergeWith=${GITHUB_WORKSPACE}/coverage-join/coverage.net7.0.json
+ dotnet test ./tests/Neo.Json.UnitTests /p:CollectCoverage=true /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage/lcov /p:MergeWith=${GITHUB_WORKSPACE}/coverage-join/coverage.net7.0.json /p:CoverletOutputFormat=lcov
- name: Coveralls
- uses: coverallsapp/github-action@master
+ if: matrix.os == 'ubuntu-latest'
+ uses: coverallsapp/github-action@v2.2.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+ format: lcov
+ file: ${GITHUB_WORKSPACE}/coverage/lcov.net7.0.info
- PublishGithub:
- # Because sometimes this action is not working as expected we will disable it until determine that it's more stable
- if: false && github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/')
- needs: Test
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - name: Setup .NET Core
- uses: actions/setup-dotnet@v1
- with:
- dotnet-version: ${{ env.DOTNET_VERSION }}
- - name: Setup NuGet.exe for use with actions
- uses: NuGet/setup-nuget@v1
- - name: Pack with dotnet
- run: git rev-list --count HEAD |xargs printf "CI%05d" |xargs dotnet pack -c Debug -o out --include-source --version-suffix
- - name: Publish to Github Packages
- run: |
- nuget source Add -Name "GitHub" -Source "https://nuget.pkg.github.com/neo-project/index.json" -UserName neo-project -Password ${GITHUB_TOKEN}
- nuget push out/*.nupkg -Source "GitHub"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- PublishMyGet:
+ PublishPackage:
if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/')
needs: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
+
- name: Setup .NET Core
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- - name: Pack with dotnet
- run: git rev-list --count HEAD |xargs printf "CI%05d" |xargs dotnet pack -c Debug -o out --include-source --version-suffix
- - name: Publish to MyGet
- run: dotnet nuget push out/*.nupkg -s https://www.myget.org/F/neo/api/v2/package -k ${MYGET_TOKEN} -ss https://www.myget.org/F/neo/symbols/api/v2/package -sk ${MYGET_TOKEN}
- env:
- MYGET_TOKEN: ${{ secrets.MYGET_TOKEN }}
+
+ - name: Set Version
+ run: git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX={}' >> $GITHUB_ENV
+
+ - name : Pack (Neo)
+ run: |
+ dotnet pack \
+ --configuration Release \
+ --output ./out \
+ --version-suffix ${{ env.VERSION_SUFFIX }}
+
+ - name: Remove Unwanted Files
+ working-directory: ./out
+ run: |
+ rm -v Neo.CLI*
+ rm -v Neo.GUI*
+
+ - name: Publish to Github Packages
+ working-directory: ./out
+ run: |
+ dotnet nuget push * \
+ --source https://nuget.pkg.github.com/neo-project/index.json \
+ --api-key "${{ secrets.GITHUB_TOKEN }}" \
+ --disable-buffering \
+ --no-service-endpoint;
+
+ - name: Publish to myGet
+ working-directory: ./out
+ run: |
+ dotnet nuget push * \
+ --source https://www.myget.org/F/neo/api/v3/index.json \
+ --api-key "${{ secrets.MYGET_TOKEN }}" \
+ --disable-buffering \
+ --no-service-endpoint;
Release:
if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/')
@@ -80,12 +103,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
sudo apt install xmlstarlet
- find src -name *.csproj | xargs xmlstarlet sel -t -v "concat('::set-output name=version::v',//VersionPrefix/text())" | xargs echo
+ find src -name Directory.Build.props | xargs xmlstarlet sel -N i=http://schemas.microsoft.com/developer/msbuild/2003 -t -v "concat('::set-output name=version::v',//i:VersionPrefix/text())" | xargs echo
- name: Check tag
id: check_tag
run: curl -s -I ${{ format('https://github.com/{0}/releases/tag/{1}', github.repository, steps.get_version.outputs.version) }} | head -n 1 | cut -d$' ' -f2 | xargs printf "::set-output name=statusCode::%s" | xargs echo
@@ -99,15 +122,56 @@ jobs:
tag_name: ${{ steps.get_version.outputs.version }}
release_name: ${{ steps.get_version.outputs.version }}
prerelease: ${{ contains(steps.get_version.outputs.version, '-') }}
- - name: Setup .NET Core
+ - name: Setup .NET
if: steps.check_tag.outputs.statusCode == '404'
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
+ - name : Pack (Neo)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo \
+ --configuration Release \
+ --output ./out
+ - name : Pack (Neo.IO)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo.IO \
+ --configuration Release \
+ --output ./out
+ - name : Pack (Neo.Extensions)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo.Extensions \
+ --configuration Release \
+ --output ./out
+ - name : Pack (Neo.Json)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo.Json \
+ --configuration Release \
+ --output ./out
+ - name : Pack (Neo.VM)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo.VM \
+ --configuration Release \
+ --output ./out
+ - name : Pack (Neo.ConsoleService)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo.ConsoleService \
+ --configuration Release \
+ --output ./out
+ - name : Pack (Neo.Cryptography.BLS12_381)
+ if: steps.check_tag.outputs.statusCode == '404'
+ run: |
+ dotnet pack ./src/Neo.Cryptography.BLS12_381 \
+ --configuration Release \
+ --output ./out
- name: Publish to NuGet
if: steps.check_tag.outputs.statusCode == '404'
run: |
- dotnet pack -o out -c Release
dotnet nuget push out/*.nupkg -s https://api.nuget.org/v3/index.json -k ${NUGET_TOKEN}
env:
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml
new file mode 100644
index 0000000000..cf3471b551
--- /dev/null
+++ b/.github/workflows/pkgs-delete.yml
@@ -0,0 +1,128 @@
+name: Package Cleanup
+
+on:
+ schedule:
+ - cron: '0 0 * * *' # Run every day at 24:00
+
+jobs:
+
+ delete-myget-pkgs:
+ name: Delete Old MyGet Packages
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+ - name: Install Requests
+ run: pip install requests
+ - name: Install Packaging
+ run: pip install packaging
+ - name: Delete versions below 3.6.1
+ env:
+ MYGET_FEED: 'neo'
+ PACKAGE_NAMES: 'Neo.VM,Neo.Json,Neo.IO,Neo,Neo.ConsoleService,Neo.Extensions' # Neo.Cryptography.BLS12_381 version is 0.x
+ MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }}
+ run: |
+ import requests
+ from packaging import version
+ import os
+
+ def get_versions(feed, package_name, api_key):
+ url = f"https://www.myget.org/F/{feed}/api/v2/Packages?$select=Version&$filter=Id eq '{package_name}'&$format=json"
+ headers = {'Accept': 'application/json'}
+ response = requests.get(url, headers=headers)
+ if response.status_code == 200:
+ versions = response.json()['d']['results']
+ return [ver['Version'] for ver in versions]
+ else:
+ return []
+
+ def delete_version(feed, package_name, ver, api_key):
+ url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}?hardDelete=true"
+ headers = {"X-NuGet-ApiKey": api_key}
+ response = requests.delete(url, headers=headers)
+ return response.status_code == 200 # Success
+
+ feed = os.environ['MYGET_FEED']
+ package_names = os.environ['PACKAGE_NAMES'].split(',')
+ api_key = os.environ['MYGET_API_KEY']
+
+ for package_name in package_names:
+ versions_to_delete = get_versions(feed, package_name, api_key)
+ for ver in versions_to_delete:
+ if version.parse(ver.split("-", 1)[0]) >= version.Version("3.6.1"):
+ print(f"Omited {ver} of package {package_name}.")
+ continue
+ if delete_version(feed, package_name, ver, api_key):
+ print(f"Deleted version {ver} of package {package_name}.")
+ else:
+ print(f"Failed to delete version {ver} of package {package_name}.")
+
+ shell: python
+
+ delete-git-pkgs:
+ name: Delete Old Nuget Packages
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Delete Neo.Cryptography.BLS12_381 Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo.Cryptography.BLS12_381
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
+
+ - name: Delete Neo.VM Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo.VM
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
+
+ - name: Delete Neo.Json Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo.Json
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
+
+ - name: Delete Neo.IO Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo.IO
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
+
+ - name: Delete Neo Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Delete Neo.ConsoleService Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo.ConsoleService
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Delete Neo.Extensions Package
+ uses: actions/delete-package-versions@v4
+ with:
+ package-name: Neo.Extensions
+ package-type: nuget
+ min-versions-to-keep: 3
+ delete-only-pre-release-versions: "true"
+ token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.gitignore b/.gitignore
index cb49b769de..1358af3bde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -256,3 +256,4 @@ paket-files/
PublishProfiles
/.vscode
+launchSettings.json
diff --git a/NuGet.Config b/NuGet.Config
index c06788942f..53fc635420 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -2,7 +2,8 @@
+
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 14557da887..37d1f00827 100644
--- a/README.md
+++ b/README.md
@@ -16,13 +16,9 @@
Neo
·
- Neo VM
- ·
Neo Modules
·
Neo DevPack
- ·
- Neo Node
@@ -81,9 +77,6 @@
-
-
-
@@ -95,7 +88,11 @@
-
+
+
+
+
+
## Table of Contents
@@ -112,10 +109,6 @@ This repository contain main classes of the
Visit the [documentation](https://docs.neo.org/docs/en-us/index.html) to get started.
-*Note: This is Neo 3 branch, currently under development. For the current stable version, please [click here.](https://github.com/neo-project/neo/tree/master-2.x)*
-
-
-
## Project structure
An overview of the project folders can be seen below.
@@ -137,8 +130,6 @@ An overview of the project folders can be seen below.
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-vm:](https://github.com/neo-project/neo-vm/) Neo Virtual Machine is a decoupled VM that Neo uses to execute its scripts. It also uses the `InteropService` layer to extend its functionalities.
-* [neo-node:](https://github.com/neo-project/neo-node/) Executable version of the Neo library, exposing features using a command line application or GUI.
* [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*.
diff --git a/SpellingExclusions.dic b/SpellingExclusions.dic
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/benchmarks/Neo.Benchmarks/Benchmarks.cs b/benchmarks/Neo.Benchmarks/Benchmarks.cs
new file mode 100644
index 0000000000..081f806622
--- /dev/null
+++ b/benchmarks/Neo.Benchmarks/Benchmarks.cs
@@ -0,0 +1,79 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// Benchmarks.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 Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.VM;
+using System.Diagnostics;
+
+namespace Neo;
+
+static class Benchmarks
+{
+ private static readonly ProtocolSettings protocol = ProtocolSettings.Load("config.json");
+ private static readonly NeoSystem system = new(protocol, (string)null);
+
+ public static void NeoIssue2725()
+ {
+ // https://github.com/neo-project/neo/issues/2725
+ // L00: INITSSLOT 1
+ // L01: NEWARRAY0
+ // L02: PUSHDATA1 6161616161 //"aaaaa"
+ // L03: PUSHINT16 500
+ // L04: STSFLD0
+ // L05: OVER
+ // L06: OVER
+ // L07: SYSCALL 95016f61 //System.Runtime.Notify
+ // L08: LDSFLD0
+ // L09: DEC
+ // L10: DUP
+ // L11: STSFLD0
+ // L12: JMPIF L05
+ // L13: CLEAR
+ // L14: SYSCALL dbfea874 //System.Runtime.GetExecutingScriptHash
+ // L15: PUSHINT16 8000
+ // L16: STSFLD0
+ // L17: DUP
+ // L18: SYSCALL 274335f1 //System.Runtime.GetNotifications
+ // L19: DROP
+ // L20: LDSFLD0
+ // L21: DEC
+ // L22: DUP
+ // L23: STSFLD0
+ // L24: JMPIF L17
+ Run(nameof(NeoIssue2725), "VgHCDAVhYWFhYQH0AWBLS0GVAW9hWJ1KYCT1SUHb/qh0AUAfYEpBJ0M18UVYnUpgJPU=");
+ }
+
+ private static void Run(string name, string poc)
+ {
+ Random random = new();
+ Transaction tx = new()
+ {
+ Version = 0,
+ Nonce = (uint)random.Next(),
+ SystemFee = 20_00000000,
+ NetworkFee = 1_00000000,
+ ValidUntilBlock = ProtocolSettings.Default.MaxTraceableBlocks,
+ Signers = Array.Empty(),
+ Attributes = Array.Empty(),
+ Script = Convert.FromBase64String(poc),
+ Witnesses = Array.Empty()
+ };
+ using var snapshot = system.GetSnapshot();
+ using var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshot, system.GenesisBlock, protocol, tx.SystemFee);
+ engine.LoadScript(tx.Script);
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ engine.Execute();
+ stopwatch.Stop();
+ Debug.Assert(engine.State == VMState.FAULT);
+ Console.WriteLine($"Benchmark: {name},\tTime: {stopwatch.Elapsed}");
+ }
+}
diff --git a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
new file mode 100644
index 0000000000..17698eed98
--- /dev/null
+++ b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net7.0
+ Neo
+ enable
+ false
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
diff --git a/benchmarks/Neo.Benchmarks/Program.cs b/benchmarks/Neo.Benchmarks/Program.cs
new file mode 100644
index 0000000000..9d4125bb9f
--- /dev/null
+++ b/benchmarks/Neo.Benchmarks/Program.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2024 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 Neo;
+using System.Reflection;
+
+foreach (var method in typeof(Benchmarks).GetMethods(BindingFlags.Public | BindingFlags.Static))
+{
+ method.CreateDelegate().Invoke();
+}
diff --git a/benchmarks/Neo.Benchmarks/config.json b/benchmarks/Neo.Benchmarks/config.json
new file mode 100644
index 0000000000..01471e4c76
--- /dev/null
+++ b/benchmarks/Neo.Benchmarks/config.json
@@ -0,0 +1,71 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore]
+ "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id
+ },
+ "P2P": {
+ "Port": 10333,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ },
+ "Contracts": {
+ "NeoNameService": "0x50ac1c37690cc2cfc594472833cf57505d5f46de"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 860833102,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 512,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "Hardforks": {
+ "HF_Aspidochelone": 1730000,
+ "HF_Basilisk": 4120000
+ },
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
+ "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093",
+ "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a",
+ "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554",
+ "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d",
+ "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e",
+ "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70",
+ "023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe",
+ "03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379",
+ "03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050",
+ "03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0",
+ "02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62",
+ "03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0",
+ "0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654",
+ "020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639",
+ "0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30",
+ "03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde",
+ "02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad",
+ "0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d",
+ "03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc",
+ "02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a"
+ ],
+ "SeedList": [
+ "seed1.neo.org:10333",
+ "seed2.neo.org:10333",
+ "seed3.neo.org:10333",
+ "seed4.neo.org:10333",
+ "seed5.neo.org:10333"
+ ]
+ }
+}
diff --git a/benchmarks/Neo.VM.Benchmarks/Benchmarks.cs b/benchmarks/Neo.VM.Benchmarks/Benchmarks.cs
new file mode 100644
index 0000000000..6eab691a7d
--- /dev/null
+++ b/benchmarks/Neo.VM.Benchmarks/Benchmarks.cs
@@ -0,0 +1,112 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// Benchmarks.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.Diagnostics;
+
+namespace Neo.VM
+{
+ public static class Benchmarks
+ {
+ public static void NeoIssue2528()
+ {
+ // https://github.com/neo-project/neo/issues/2528
+ // L01: INITSLOT 1, 0
+ // L02: NEWARRAY0
+ // L03: DUP
+ // L04: DUP
+ // L05: PUSHINT16 2043
+ // L06: STLOC 0
+ // L07: PUSH1
+ // L08: PACK
+ // L09: LDLOC 0
+ // L10: DEC
+ // L11: STLOC 0
+ // L12: LDLOC 0
+ // L13: JMPIF_L L07
+ // L14: PUSH1
+ // L15: PACK
+ // L16: APPEND
+ // L17: PUSHINT32 38000
+ // L18: STLOC 0
+ // L19: PUSH0
+ // L20: PICKITEM
+ // L21: LDLOC 0
+ // L22: DEC
+ // L23: STLOC 0
+ // L24: LDLOC 0
+ // L25: JMPIF_L L19
+ // L26: DROP
+ Run(nameof(NeoIssue2528), "VwEAwkpKAfsHdwARwG8AnXcAbwAl9////xHAzwJwlAAAdwAQzm8AnXcAbwAl9////0U=");
+ }
+
+ public static void NeoVMIssue418()
+ {
+ // https://github.com/neo-project/neo-vm/issues/418
+ // L00: NEWARRAY0
+ // L01: PUSH0
+ // L02: PICK
+ // L03: PUSH1
+ // L04: PACK
+ // L05: PUSH1
+ // L06: PICK
+ // L07: PUSH1
+ // L08: PACK
+ // L09: INITSSLOT 1
+ // L10: PUSHINT16 510
+ // L11: DEC
+ // L12: STSFLD0
+ // L13: PUSH1
+ // L14: PICK
+ // L15: PUSH1
+ // L16: PICK
+ // L17: PUSH2
+ // L18: PACK
+ // L19: REVERSE3
+ // L20: PUSH2
+ // L21: PACK
+ // L22: LDSFLD0
+ // L23: DUP
+ // L24: JMPIF L11
+ // L25: DROP
+ // L26: ROT
+ // L27: DROP
+ Run(nameof(NeoVMIssue418), "whBNEcARTRHAVgEB/gGdYBFNEU0SwFMSwFhKJPNFUUU=");
+ }
+
+ public static void NeoIssue2723()
+ {
+ // L00: INITSSLOT 1
+ // L01: PUSHINT32 130000
+ // L02: STSFLD 0
+ // L03: PUSHINT32 1048576
+ // L04: NEWBUFFER
+ // L05: DROP
+ // L06: LDSFLD 0
+ // L07: DEC
+ // L08: DUP
+ // L09: STSFLD 0
+ // L10: JMPIF L03
+ Run(nameof(NeoIssue2723), "VgEC0PsBAGcAAgAAEACIRV8AnUpnACTz");
+ }
+
+ private static void Run(string name, string poc)
+ {
+ byte[] script = Convert.FromBase64String(poc);
+ using ExecutionEngine engine = new();
+ engine.LoadScript(script);
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ engine.Execute();
+ stopwatch.Stop();
+ Debug.Assert(engine.State == VMState.HALT);
+ Console.WriteLine($"Benchmark: {name},\tTime: {stopwatch.Elapsed}");
+ }
+ }
+}
diff --git a/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj b/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
new file mode 100644
index 0000000000..bdca77ae89
--- /dev/null
+++ b/benchmarks/Neo.VM.Benchmarks/Neo.VM.Benchmarks.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net7.0
+ Neo.VM
+ enable
+ enable
+ false
+
+
+
+
+
+
+
diff --git a/benchmarks/Neo.VM.Benchmarks/Program.cs b/benchmarks/Neo.VM.Benchmarks/Program.cs
new file mode 100644
index 0000000000..a9c86f405d
--- /dev/null
+++ b/benchmarks/Neo.VM.Benchmarks/Program.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2015-2024 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 Neo.VM;
+using System.Reflection;
+
+foreach (var method in typeof(Benchmarks).GetMethods(BindingFlags.Public | BindingFlags.Static))
+{
+ method.CreateDelegate().Invoke();
+}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000000..423c2e2269
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "7.0.404",
+ "rollForward": "latestFeature",
+ "allowPrerelease": false
+ }
+}
diff --git a/neo.sln b/neo.sln
index 79e13f0185..b62de06a6e 100644
--- a/neo.sln
+++ b/neo.sln
@@ -1,15 +1,45 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29519.87
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32516.85
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo", "src\neo\neo.csproj", "{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo", "src\Neo\Neo.csproj", "{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo.UnitTests", "tests\neo.UnitTests\neo.UnitTests.csproj", "{5B783B30-B422-4C2F-AC22-187A8D1993F4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Json", "src\Neo.Json\Neo.Json.csproj", "{6B709ED6-64C0-451D-B07F-8F49185AE191}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.UnitTests", "tests\Neo.UnitTests\Neo.UnitTests.csproj", "{5B783B30-B422-4C2F-AC22-187A8D1993F4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Json.UnitTests", "tests\Neo.Json.UnitTests\Neo.Json.UnitTests.csproj", "{AE6C32EE-8447-4E01-8187-2AE02BB64251}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Benchmarks", "benchmarks\Neo.Benchmarks\Neo.Benchmarks.csproj", "{BCD03521-5F8F-4775-9ADF-FA361480804F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B5339DF7-5D1D-43BA-B332-74B825E1770E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EDE05FA8-8E73-4924-BC63-DD117127EEE1}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.VM.Benchmarks", "benchmarks\Neo.VM.Benchmarks\Neo.VM.Benchmarks.csproj", "{E83633BA-FCF0-4A1A-B5BC-42000E24D437}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.VM", "src\Neo.VM\Neo.VM.csproj", "{0603710E-E0BA-494C-AA0F-6FB0C8A8C754}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.VM.Tests", "tests\Neo.VM.Tests\Neo.VM.Tests.csproj", "{005F84EB-EA2E-449F-930A-7B4173DDC7EC}"
+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}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.BLS12_381", "src\Neo.Cryptography.BLS12_381\Neo.Cryptography.BLS12_381.csproj", "{D48C1FAB-3471-4CA0-8688-25E6F43F2C25}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.BLS12_381.Tests", "tests\Neo.Cryptography.BLS12_381.Tests\Neo.Cryptography.BLS12_381.Tests.csproj", "{387CCF6C-9A26-43F6-A639-0A82E91E10D8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.IO", "src\Neo.IO\Neo.IO.csproj", "{4CDAC1AA-45C6-4157-8D8E-199050433048}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions", "src\Neo.Extensions\Neo.Extensions.csproj", "{9C5213D6-3833-4570-8AE2-47E9F9017A8F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -20,17 +50,87 @@ Global
{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B709ED6-64C0-451D-B07F-8F49185AE191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B709ED6-64C0-451D-B07F-8F49185AE191}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B709ED6-64C0-451D-B07F-8F49185AE191}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B709ED6-64C0-451D-B07F-8F49185AE191}.Release|Any CPU.Build.0 = Release|Any CPU
{5B783B30-B422-4C2F-AC22-187A8D1993F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B783B30-B422-4C2F-AC22-187A8D1993F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B783B30-B422-4C2F-AC22-187A8D1993F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B783B30-B422-4C2F-AC22-187A8D1993F4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE6C32EE-8447-4E01-8187-2AE02BB64251}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BCD03521-5F8F-4775-9ADF-FA361480804F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BCD03521-5F8F-4775-9ADF-FA361480804F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BCD03521-5F8F-4775-9ADF-FA361480804F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BCD03521-5F8F-4775-9ADF-FA361480804F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E83633BA-FCF0-4A1A-B5BC-42000E24D437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E83633BA-FCF0-4A1A-B5BC-42000E24D437}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E83633BA-FCF0-4A1A-B5BC-42000E24D437}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E83633BA-FCF0-4A1A-B5BC-42000E24D437}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0603710E-E0BA-494C-AA0F-6FB0C8A8C754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0603710E-E0BA-494C-AA0F-6FB0C8A8C754}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0603710E-E0BA-494C-AA0F-6FB0C8A8C754}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0603710E-E0BA-494C-AA0F-6FB0C8A8C754}.Release|Any CPU.Build.0 = Release|Any CPU
+ {005F84EB-EA2E-449F-930A-7B4173DDC7EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {005F84EB-EA2E-449F-930A-7B4173DDC7EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {005F84EB-EA2E-449F-930A-7B4173DDC7EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {005F84EB-EA2E-449F-930A-7B4173DDC7EC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9E886812-7243-48D8-BEAF-47AADC11C054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
+ {BDFBE455-4C1F-4FC4-B5FC-1387B93A8687}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B40F8584-5AFB-452C-AEFA-009C80CC23A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B40F8584-5AFB-452C-AEFA-009C80CC23A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B40F8584-5AFB-452C-AEFA-009C80CC23A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B40F8584-5AFB-452C-AEFA-009C80CC23A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D48C1FAB-3471-4CA0-8688-25E6F43F2C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D48C1FAB-3471-4CA0-8688-25E6F43F2C25}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D48C1FAB-3471-4CA0-8688-25E6F43F2C25}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D48C1FAB-3471-4CA0-8688-25E6F43F2C25}.Release|Any CPU.Build.0 = Release|Any CPU
+ {387CCF6C-9A26-43F6-A639-0A82E91E10D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {387CCF6C-9A26-43F6-A639-0A82E91E10D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {387CCF6C-9A26-43F6-A639-0A82E91E10D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {387CCF6C-9A26-43F6-A639-0A82E91E10D8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4CDAC1AA-45C6-4157-8D8E-199050433048}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4CDAC1AA-45C6-4157-8D8E-199050433048}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4CDAC1AA-45C6-4157-8D8E-199050433048}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4CDAC1AA-45C6-4157-8D8E-199050433048}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9C5213D6-3833-4570-8AE2-47E9F9017A8F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{36447A9B-0311-4D4D-A3D5-AECBE9C15BBC} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
+ {6B709ED6-64C0-451D-B07F-8F49185AE191} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
{5B783B30-B422-4C2F-AC22-187A8D1993F4} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
+ {AE6C32EE-8447-4E01-8187-2AE02BB64251} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
+ {BCD03521-5F8F-4775-9ADF-FA361480804F} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
+ {E83633BA-FCF0-4A1A-B5BC-42000E24D437} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC}
+ {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}
+ {387CCF6C-9A26-43F6-A639-0A82E91E10D8} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1}
+ {4CDAC1AA-45C6-4157-8D8E-199050433048} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
+ {9C5213D6-3833-4570-8AE2-47E9F9017A8F} = {B5339DF7-5D1D-43BA-B332-74B825E1770E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000000..35a8fa289c
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,29 @@
+
+
+
+
+ 2015-2023 The Neo Project
+ 3.6.2
+ 11.0
+ The Neo Project
+ neo.png
+ https://github.com/neo-project/neo
+ MIT
+ README.md
+ git
+ https://github.com/neo-project/neo.git
+ true
+ snupkg
+ The Neo Project
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/IsExternalInit.cs b/src/IsExternalInit.cs
new file mode 100644
index 0000000000..cf4ea93da2
--- /dev/null
+++ b/src/IsExternalInit.cs
@@ -0,0 +1,28 @@
+// Copyright (C) 2015-2024 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
new file mode 100644
index 0000000000..617856cee9
--- /dev/null
+++ b/src/Neo.CLI/CLI/CommandLineOption.cs
@@ -0,0 +1,36 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// CommandLineOption.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.
+
+namespace Neo.CLI
+{
+ public class CommandLineOptions
+ {
+ public string? Config { get; init; }
+ public string? Wallet { get; init; }
+ public string? Password { get; init; }
+ public string[]? Plugins { get; set; }
+ public string? DBEngine { get; init; }
+ public string? DBPath { get; init; }
+ public bool? NoVerify { get; init; }
+
+ ///
+ /// Check if CommandLineOptions was configured
+ ///
+ public bool IsValid =>
+ !string.IsNullOrEmpty(Config) ||
+ !string.IsNullOrEmpty(Wallet) ||
+ !string.IsNullOrEmpty(Password) ||
+ !string.IsNullOrEmpty(DBEngine) ||
+ !string.IsNullOrEmpty(DBPath) ||
+ (Plugins?.Length > 0) ||
+ NoVerify is not null;
+ }
+}
diff --git a/src/Neo.CLI/CLI/ConsolePercent.cs b/src/Neo.CLI/CLI/ConsolePercent.cs
new file mode 100644
index 0000000000..703ca38740
--- /dev/null
+++ b/src/Neo.CLI/CLI/ConsolePercent.cs
@@ -0,0 +1,146 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// ConsolePercent.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;
+
+namespace Neo.CLI
+{
+ public class ConsolePercent : IDisposable
+ {
+ #region Variables
+
+ private readonly long _maxValue;
+ private long _value;
+ private decimal _lastFactor;
+ private string? _lastPercent;
+
+ private readonly int _x, _y;
+
+ private readonly bool _inputRedirected;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Value
+ ///
+ public long Value
+ {
+ get => _value;
+ set
+ {
+ if (value == _value) return;
+
+ _value = Math.Min(value, _maxValue);
+ Invalidate();
+ }
+ }
+
+ ///
+ /// Maximum value
+ ///
+ public long MaxValue
+ {
+ get => _maxValue;
+ init
+ {
+ if (value == _maxValue) return;
+
+ _maxValue = value;
+
+ if (_value > _maxValue)
+ _value = _maxValue;
+
+ Invalidate();
+ }
+ }
+
+ ///
+ /// Percent
+ ///
+ public decimal Percent
+ {
+ get
+ {
+ if (_maxValue == 0) return 0;
+ return (_value * 100M) / _maxValue;
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Constructor
+ ///
+ /// Value
+ /// Maximum value
+ public ConsolePercent(long value = 0, long maxValue = 100)
+ {
+ _inputRedirected = Console.IsInputRedirected;
+ _lastFactor = -1;
+ _x = _inputRedirected ? 0 : Console.CursorLeft;
+ _y = _inputRedirected ? 0 : Console.CursorTop;
+
+ MaxValue = maxValue;
+ Value = value;
+ Invalidate();
+ }
+
+ ///
+ /// Invalidate
+ ///
+ public void Invalidate()
+ {
+ var factor = Math.Round(Percent / 100M, 1);
+ var percent = Percent.ToString("0.0").PadLeft(5, ' ');
+
+ if (_lastFactor == factor && _lastPercent == percent)
+ {
+ return;
+ }
+
+ _lastFactor = factor;
+ _lastPercent = percent;
+
+ var fill = string.Empty.PadLeft((int)(10 * factor), '■');
+ var clean = string.Empty.PadLeft(10 - fill.Length, _inputRedirected ? '□' : '■');
+
+ if (_inputRedirected)
+ {
+ Console.WriteLine("[" + fill + clean + "] (" + percent + "%)");
+ }
+ else
+ {
+ Console.SetCursorPosition(_x, _y);
+
+ var prevColor = Console.ForegroundColor;
+
+ Console.ForegroundColor = ConsoleColor.White;
+ Console.Write("[");
+ Console.ForegroundColor = Percent > 50 ? ConsoleColor.Green : ConsoleColor.DarkGreen;
+ Console.Write(fill);
+ Console.ForegroundColor = ConsoleColor.White;
+ Console.Write(clean + "] (" + percent + "%)");
+
+ Console.ForegroundColor = prevColor;
+ }
+ }
+
+ ///
+ /// Free console
+ ///
+ public void Dispose()
+ {
+ Console.WriteLine("");
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/Helper.cs b/src/Neo.CLI/CLI/Helper.cs
new file mode 100644
index 0000000000..e33f075539
--- /dev/null
+++ b/src/Neo.CLI/CLI/Helper.cs
@@ -0,0 +1,42 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// Helper.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 Neo.SmartContract.Manifest;
+using System;
+
+namespace Neo.CLI
+{
+ internal static class Helper
+ {
+ public static bool IsYes(this string input)
+ {
+ if (input == null) return false;
+
+ input = input.ToLowerInvariant();
+
+ return input == "yes" || input == "y";
+ }
+
+ public static string ToBase64String(this byte[] input) => System.Convert.ToBase64String(input);
+
+ public static void IsScriptValid(this ReadOnlyMemory script, ContractAbi abi)
+ {
+ try
+ {
+ SmartContract.Helper.Check(script.ToArray(), abi);
+ }
+ catch (Exception e)
+ {
+ throw new FormatException($"Bad Script or Manifest Format: {e.Message}");
+ }
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Blockchain.cs b/src/Neo.CLI/CLI/MainService.Blockchain.cs
new file mode 100644
index 0000000000..4f896d63e3
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Blockchain.cs
@@ -0,0 +1,318 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Blockchain.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 Neo.ConsoleService;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using System;
+using System.Linq;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "export blocks" command
+ ///
+ /// Start
+ /// Number of blocks
+ /// Path
+ [ConsoleCommand("export blocks", Category = "Blockchain Commands")]
+ private void OnExportBlocksStartCountCommand(uint start, uint count = uint.MaxValue, string? path = null)
+ {
+ uint height = NativeContract.Ledger.CurrentIndex(NeoSystem.StoreView);
+ if (height < start)
+ {
+ ConsoleHelper.Error("invalid start height.");
+ return;
+ }
+
+ count = Math.Min(count, height - start + 1);
+
+ if (string.IsNullOrEmpty(path))
+ {
+ path = $"chain.{start}.acc";
+ }
+
+ WriteBlocks(start, count, path, true);
+ }
+
+ [ConsoleCommand("show block", Category = "Blockchain Commands")]
+ private void OnShowBlockCommand(string indexOrHash)
+ {
+ lock (syncRoot)
+ {
+ Block? block = null;
+
+ if (uint.TryParse(indexOrHash, out var index))
+ block = NativeContract.Ledger.GetBlock(NeoSystem.StoreView, index);
+ else if (UInt256.TryParse(indexOrHash, out var hash))
+ block = NativeContract.Ledger.GetBlock(NeoSystem.StoreView, hash);
+ else
+ {
+ ConsoleHelper.Error("Enter a valid block index or hash.");
+ return;
+ }
+
+ if (block is null)
+ {
+ ConsoleHelper.Error($"Block {indexOrHash} doesn't exist.");
+ return;
+ }
+
+ DateTime blockDatetime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+ blockDatetime = blockDatetime.AddMilliseconds(block.Timestamp).ToLocalTime();
+
+ ConsoleHelper.Info("", "-------------", "Block", "-------------");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", " Timestamp: ", $"{blockDatetime}");
+ ConsoleHelper.Info("", " Index: ", $"{block.Index}");
+ ConsoleHelper.Info("", " Hash: ", $"{block.Hash}");
+ ConsoleHelper.Info("", " Nonce: ", $"{block.Nonce}");
+ ConsoleHelper.Info("", " MerkleRoot: ", $"{block.MerkleRoot}");
+ ConsoleHelper.Info("", " PrevHash: ", $"{block.PrevHash}");
+ ConsoleHelper.Info("", " NextConsensus: ", $"{block.NextConsensus}");
+ ConsoleHelper.Info("", " PrimaryIndex: ", $"{block.PrimaryIndex}");
+ ConsoleHelper.Info("", " PrimaryPubKey: ", $"{NativeContract.NEO.GetCommittee(NeoSystem.GetSnapshot())[block.PrimaryIndex]}");
+ ConsoleHelper.Info("", " Version: ", $"{block.Version}");
+ ConsoleHelper.Info("", " Size: ", $"{block.Size} Byte(s)");
+ ConsoleHelper.Info();
+
+ ConsoleHelper.Info("", "-------------", "Witness", "-------------");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", " Invocation Script: ", $"{Convert.ToBase64String(block.Witness.InvocationScript.Span)}");
+ ConsoleHelper.Info("", " Verification Script: ", $"{Convert.ToBase64String(block.Witness.VerificationScript.Span)}");
+ ConsoleHelper.Info("", " ScriptHash: ", $"{block.Witness.ScriptHash}");
+ ConsoleHelper.Info("", " Size: ", $"{block.Witness.Size} Byte(s)");
+ ConsoleHelper.Info();
+
+ ConsoleHelper.Info("", "-------------", "Transactions", "-------------");
+ ConsoleHelper.Info();
+
+ if (block.Transactions.Length == 0)
+ {
+ ConsoleHelper.Info("", " No Transaction(s)");
+ }
+ else
+ {
+ foreach (var tx in block.Transactions)
+ ConsoleHelper.Info($" {tx.Hash}");
+ }
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", "--------------------------------------");
+ }
+ }
+
+ [ConsoleCommand("show tx", Category = "Blockchain Commands")]
+ public void OnShowTransactionCommand(UInt256 hash)
+ {
+ lock (syncRoot)
+ {
+ var tx = NativeContract.Ledger.GetTransactionState(NeoSystem.StoreView, hash);
+
+ if (tx is null)
+ {
+ ConsoleHelper.Error($"Transaction {hash} doesn't exist.");
+ return;
+ }
+
+ var block = NativeContract.Ledger.GetHeader(NeoSystem.StoreView, tx.BlockIndex);
+
+ DateTime transactionDatetime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+ transactionDatetime = transactionDatetime.AddMilliseconds(block.Timestamp).ToLocalTime();
+
+ ConsoleHelper.Info("", "-------------", "Transaction", "-------------");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", " Timestamp: ", $"{transactionDatetime}");
+ ConsoleHelper.Info("", " Hash: ", $"{tx.Transaction.Hash}");
+ ConsoleHelper.Info("", " Nonce: ", $"{tx.Transaction.Nonce}");
+ ConsoleHelper.Info("", " Sender: ", $"{tx.Transaction.Sender}");
+ ConsoleHelper.Info("", " ValidUntilBlock: ", $"{tx.Transaction.ValidUntilBlock}");
+ ConsoleHelper.Info("", " FeePerByte: ", $"{tx.Transaction.FeePerByte}");
+ ConsoleHelper.Info("", " NetworkFee: ", $"{tx.Transaction.NetworkFee}");
+ ConsoleHelper.Info("", " SystemFee: ", $"{tx.Transaction.SystemFee}");
+ ConsoleHelper.Info("", " Script: ", $"{Convert.ToBase64String(tx.Transaction.Script.Span)}");
+ ConsoleHelper.Info("", " Version: ", $"{tx.Transaction.Version}");
+ ConsoleHelper.Info("", " BlockIndex: ", $"{block.Index}");
+ ConsoleHelper.Info("", " BlockHash: ", $"{block.Hash}");
+ ConsoleHelper.Info("", " Size: ", $"{tx.Transaction.Size} Byte(s)");
+ ConsoleHelper.Info();
+
+ ConsoleHelper.Info("", "-------------", "Signers", "-------------");
+ ConsoleHelper.Info();
+
+ foreach (var signer in tx.Transaction.Signers)
+ {
+ if (signer.Rules.Length == 0)
+ ConsoleHelper.Info("", " Rules: ", "[]");
+ else
+ ConsoleHelper.Info("", " Rules: ", $"[{string.Join(", ", signer.Rules.Select(s => $"\"{s.ToJson()}\""))}]");
+ ConsoleHelper.Info("", " Account: ", $"{signer.Account}");
+ ConsoleHelper.Info("", " Scopes: ", $"{signer.Scopes}");
+ if (signer.AllowedContracts.Length == 0)
+ ConsoleHelper.Info("", " AllowedContracts: ", "[]");
+ else
+ ConsoleHelper.Info("", " AllowedContracts: ", $"[{string.Join(", ", signer.AllowedContracts.Select(s => s.ToString()))}]");
+ if (signer.AllowedGroups.Length == 0)
+ ConsoleHelper.Info("", " AllowedGroups: ", "[]");
+ else
+ ConsoleHelper.Info("", " AllowedGroups: ", $"[{string.Join(", ", signer.AllowedGroups.Select(s => s.ToString()))}]");
+ ConsoleHelper.Info("", " Size: ", $"{signer.Size} Byte(s)");
+ ConsoleHelper.Info();
+ }
+
+ ConsoleHelper.Info("", "-------------", "Witnesses", "-------------");
+ ConsoleHelper.Info();
+ foreach (var witness in tx.Transaction.Witnesses)
+ {
+ ConsoleHelper.Info("", " InvocationScript: ", $"{Convert.ToBase64String(witness.InvocationScript.Span)}");
+ ConsoleHelper.Info("", " VerificationScript: ", $"{Convert.ToBase64String(witness.VerificationScript.Span)}");
+ ConsoleHelper.Info("", " ScriptHash: ", $"{witness.ScriptHash}");
+ ConsoleHelper.Info("", " Size: ", $"{witness.Size} Byte(s)");
+ ConsoleHelper.Info();
+ }
+
+ ConsoleHelper.Info("", "-------------", "Attributes", "-------------");
+ ConsoleHelper.Info();
+ if (tx.Transaction.Attributes.Length == 0)
+ {
+ ConsoleHelper.Info("", " No Attribute(s).");
+ }
+ else
+ {
+ foreach (var attribute in tx.Transaction.Attributes)
+ {
+ switch (attribute)
+ {
+ case Conflicts c:
+ ConsoleHelper.Info("", " Type: ", $"{c.Type}");
+ ConsoleHelper.Info("", " Hash: ", $"{c.Hash}");
+ ConsoleHelper.Info("", " Size: ", $"{c.Size} Byte(s)");
+ break;
+ case OracleResponse o:
+ ConsoleHelper.Info("", " Type: ", $"{o.Type}");
+ ConsoleHelper.Info("", " Id: ", $"{o.Id}");
+ ConsoleHelper.Info("", " Code: ", $"{o.Code}");
+ ConsoleHelper.Info("", " Result: ", $"{Convert.ToBase64String(o.Result.Span)}");
+ ConsoleHelper.Info("", " Size: ", $"{o.Size} Byte(s)");
+ break;
+ case HighPriorityAttribute p:
+ ConsoleHelper.Info("", " Type: ", $"{p.Type}");
+ break;
+ case NotValidBefore n:
+ ConsoleHelper.Info("", " Type: ", $"{n.Type}");
+ ConsoleHelper.Info("", " Height: ", $"{n.Height}");
+ break;
+ default:
+ ConsoleHelper.Info("", " Type: ", $"{attribute.Type}");
+ ConsoleHelper.Info("", " Size: ", $"{attribute.Size} Byte(s)");
+ break;
+ }
+ }
+ }
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", "--------------------------------------");
+ }
+ }
+
+ [ConsoleCommand("show contract", Category = "Blockchain Commands")]
+ public void OnShowContractCommand(string nameOrHash)
+ {
+ lock (syncRoot)
+ {
+ ContractState? contract = null;
+
+ if (UInt160.TryParse(nameOrHash, out var scriptHash))
+ contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
+ else
+ {
+ var nativeContract = NativeContract.Contracts.SingleOrDefault(s => s.Name.Equals(nameOrHash, StringComparison.InvariantCultureIgnoreCase));
+
+ if (nativeContract != null)
+ contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, nativeContract.Hash);
+ }
+
+ if (contract is null)
+ {
+ ConsoleHelper.Error($"Contract {nameOrHash} doesn't exist.");
+ return;
+ }
+
+ ConsoleHelper.Info("", "-------------", "Contract", "-------------");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", " Name: ", $"{contract.Manifest.Name}");
+ ConsoleHelper.Info("", " Hash: ", $"{contract.Hash}");
+ ConsoleHelper.Info("", " Id: ", $"{contract.Id}");
+ ConsoleHelper.Info("", " UpdateCounter: ", $"{contract.UpdateCounter}");
+ ConsoleHelper.Info("", " SupportedStandards: ", $"{string.Join(" ", contract.Manifest.SupportedStandards)}");
+ ConsoleHelper.Info("", " Checksum: ", $"{contract.Nef.CheckSum}");
+ ConsoleHelper.Info("", " Compiler: ", $"{contract.Nef.Compiler}");
+ ConsoleHelper.Info("", " SourceCode: ", $"{contract.Nef.Source}");
+ ConsoleHelper.Info("", " Trusts: ", $"[{string.Join(", ", contract.Manifest.Trusts.Select(s => s.ToJson()?.GetString()))}]");
+ if (contract.Manifest.Extra is not null)
+ {
+ foreach (var extra in contract.Manifest.Extra.Properties)
+ {
+ ConsoleHelper.Info("", $" {extra.Key,18}: ", $"{extra.Value?.GetString()}");
+ }
+ }
+ ConsoleHelper.Info();
+
+ ConsoleHelper.Info("", "-------------", "Groups", "-------------");
+ ConsoleHelper.Info();
+ if (contract.Manifest.Groups.Length == 0)
+ {
+ ConsoleHelper.Info("", " No Group(s).");
+ }
+ else
+ {
+ foreach (var group in contract.Manifest.Groups)
+ {
+ ConsoleHelper.Info("", " PubKey: ", $"{group.PubKey}");
+ ConsoleHelper.Info("", " Signature: ", $"{Convert.ToBase64String(group.Signature)}");
+ }
+ }
+ ConsoleHelper.Info();
+
+ ConsoleHelper.Info("", "-------------", "Permissions", "-------------");
+ ConsoleHelper.Info();
+ foreach (var permission in contract.Manifest.Permissions)
+ {
+ ConsoleHelper.Info("", " Contract: ", $"{permission.Contract.ToJson()?.GetString()}");
+ if (permission.Methods.IsWildcard)
+ ConsoleHelper.Info("", " Methods: ", "*");
+ else
+ ConsoleHelper.Info("", " Methods: ", $"{string.Join(", ", permission.Methods)}");
+ ConsoleHelper.Info();
+ }
+
+ ConsoleHelper.Info("", "-------------", "Methods", "-------------");
+ ConsoleHelper.Info();
+ foreach (var method in contract.Manifest.Abi.Methods)
+ {
+ ConsoleHelper.Info("", " Name: ", $"{method.Name}");
+ ConsoleHelper.Info("", " Safe: ", $"{method.Safe}");
+ ConsoleHelper.Info("", " Offset: ", $"{method.Offset}");
+ ConsoleHelper.Info("", " Parameters: ", $"[{string.Join(", ", method.Parameters.Select(s => s.Type.ToString()))}]");
+ ConsoleHelper.Info("", " ReturnType: ", $"{method.ReturnType}");
+ ConsoleHelper.Info();
+ }
+
+ ConsoleHelper.Info("", "-------------", "Script", "-------------");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info($" {Convert.ToBase64String(contract.Nef.Script.Span)}");
+ ConsoleHelper.Info();
+ ConsoleHelper.Info("", "--------------------------------");
+ }
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.CommandLine.cs b/src/Neo.CLI/CLI/MainService.CommandLine.cs
new file mode 100644
index 0000000000..074f252f35
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.CommandLine.cs
@@ -0,0 +1,93 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.CommandLine.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 Microsoft.Extensions.Configuration;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.CommandLine.NamingConventionBinder;
+using System.Reflection;
+
+namespace Neo.CLI
+{
+ public partial class MainService
+ {
+ public int OnStartWithCommandLine(string[] args)
+ {
+ RootCommand rootCommand = new(Assembly.GetExecutingAssembly().GetCustomAttribute()!.Title)
+ {
+ new Option(new[] { "-c", "--config","/config" }, "Specifies the config file."),
+ new Option(new[] { "-w", "--wallet","/wallet" }, "The path of the neo3 wallet [*.json]."),
+ new Option(new[] { "-p", "--password" ,"/password" }, "Password to decrypt the wallet, either from the command line or config file."),
+ new Option(new[] { "--db-engine","/db-engine" }, "Specify the db engine."),
+ new Option(new[] { "--db-path","/db-path" }, "Specify the db path."),
+ new Option(new[] { "--noverify","/noverify" }, "Indicates whether the blocks need to be verified when importing."),
+ new Option(new[] { "--plugins","/plugins" }, "The list of plugins, if not present, will be installed [plugin1 plugin2]."),
+ };
+
+ rootCommand.Handler = CommandHandler.Create(Handle);
+ return rootCommand.Invoke(args);
+ }
+
+ private void Handle(RootCommand command, CommandLineOptions options, InvocationContext context)
+ {
+ Start(options);
+ }
+
+ private static void CustomProtocolSettings(CommandLineOptions options, ProtocolSettings settings)
+ {
+ var tempSetting = settings;
+ // if specified config, then load the config and check the network
+ if (!string.IsNullOrEmpty(options.Config))
+ {
+ tempSetting = ProtocolSettings.Load(options.Config);
+ }
+
+ var customSetting = new ProtocolSettings
+ {
+ Network = tempSetting.Network,
+ AddressVersion = tempSetting.AddressVersion,
+ StandbyCommittee = tempSetting.StandbyCommittee,
+ ValidatorsCount = tempSetting.ValidatorsCount,
+ SeedList = tempSetting.SeedList,
+ MillisecondsPerBlock = tempSetting.MillisecondsPerBlock,
+ MaxTransactionsPerBlock = tempSetting.MaxTransactionsPerBlock,
+ MemoryPoolMaxTransactions = tempSetting.MemoryPoolMaxTransactions,
+ MaxTraceableBlocks = tempSetting.MaxTraceableBlocks,
+ InitialGasDistribution = tempSetting.InitialGasDistribution,
+ Hardforks = tempSetting.Hardforks
+ };
+
+ if (!string.IsNullOrEmpty(options.Config)) ProtocolSettings.Custom = customSetting;
+ }
+
+ 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 customSetting = new Settings
+ {
+ Logger = tempSetting.Logger,
+ Storage = new StorageSettings
+ {
+ Engine = options.DBEngine ?? tempSetting.Storage.Engine,
+ Path = options.DBPath ?? tempSetting.Storage.Path
+ },
+ P2P = tempSetting.P2P,
+ UnlockWallet = new UnlockWalletSettings
+ {
+ Path = options.Wallet ?? tempSetting.UnlockWallet.Path,
+ Password = options.Password ?? tempSetting.UnlockWallet.Password
+ },
+ Contracts = tempSetting.Contracts
+ };
+ if (options.IsValid) Settings.Custom = customSetting;
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Contracts.cs b/src/Neo.CLI/CLI/MainService.Contracts.cs
new file mode 100644
index 0000000000..94fbe32f89
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Contracts.cs
@@ -0,0 +1,189 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Contracts.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 Neo.ConsoleService;
+using Neo.Json;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using System;
+using System.Linq;
+using System.Numerics;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "deploy" command
+ ///
+ /// File path
+ /// Manifest path
+ /// Extra data for deploy
+ [ConsoleCommand("deploy", Category = "Contract Commands")]
+ private void OnDeployCommand(string filePath, string? manifestPath = null, JObject? data = null)
+ {
+ if (NoWallet()) return;
+ byte[] script = LoadDeploymentScript(filePath, manifestPath, data, out var nef, out var manifest);
+ Transaction tx;
+ try
+ {
+ tx = CurrentWallet!.MakeTransaction(NeoSystem.StoreView, script);
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ return;
+ }
+ UInt160 hash = SmartContract.Helper.GetContractHash(tx.Sender, nef.CheckSum, manifest.Name);
+
+ ConsoleHelper.Info("Contract hash: ", $"{hash}");
+ ConsoleHelper.Info("Gas consumed: ", $"{new BigDecimal((BigInteger)tx.SystemFee, NativeContract.GAS.Decimals)}");
+ ConsoleHelper.Info("Network fee: ", $"{new BigDecimal((BigInteger)tx.NetworkFee, NativeContract.GAS.Decimals)}");
+ ConsoleHelper.Info("Total fee: ", $"{new BigDecimal((BigInteger)(tx.SystemFee + tx.NetworkFee), NativeContract.GAS.Decimals)} GAS");
+ if (!ConsoleHelper.ReadUserInput("Relay tx? (no|yes)").IsYes()) // Add this in case just want to get hash but not relay
+ {
+ return;
+ }
+ SignAndSendTx(NeoSystem.StoreView, tx);
+ }
+
+ ///
+ /// Process "update" command
+ ///
+ /// Script hash
+ /// File path
+ /// Manifest path
+ /// Sender
+ /// Signer Accounts
+ /// Extra data for update
+ [ConsoleCommand("update", Category = "Contract Commands")]
+ private void OnUpdateCommand(UInt160 scriptHash, string filePath, string manifestPath, UInt160 sender, UInt160[]? signerAccounts = null, JObject? data = null)
+ {
+ Signer[] signers = Array.Empty();
+
+ if (NoWallet()) return;
+ if (sender != null)
+ {
+ if (signerAccounts == null)
+ signerAccounts = new[] { sender };
+ else if (signerAccounts.Contains(sender) && signerAccounts[0] != sender)
+ {
+ var signersList = signerAccounts.ToList();
+ signersList.Remove(sender);
+ signerAccounts = signersList.Prepend(sender).ToArray();
+ }
+ else if (!signerAccounts.Contains(sender))
+ {
+ signerAccounts = signerAccounts.Prepend(sender).ToArray();
+ }
+ signers = signerAccounts.Select(p => new Signer() { Account = p, Scopes = WitnessScope.CalledByEntry }).ToArray();
+ }
+
+ Transaction tx;
+ try
+ {
+ byte[] script = LoadUpdateScript(scriptHash, filePath, manifestPath, data, out var nef, out var manifest);
+ tx = CurrentWallet!.MakeTransaction(NeoSystem.StoreView, script, sender, signers);
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ return;
+ }
+ ContractState contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
+ if (contract == null)
+ {
+ ConsoleHelper.Warning($"Can't upgrade, contract hash not exist: {scriptHash}");
+ }
+ else
+ {
+ ConsoleHelper.Info("Contract hash: ", $"{scriptHash}");
+ ConsoleHelper.Info("Updated times: ", $"{contract.UpdateCounter}");
+ ConsoleHelper.Info("Gas consumed: ", $"{new BigDecimal((BigInteger)tx.SystemFee, NativeContract.GAS.Decimals)}");
+ ConsoleHelper.Info("Network fee: ", $"{new BigDecimal((BigInteger)tx.NetworkFee, NativeContract.GAS.Decimals)}");
+ ConsoleHelper.Info("Total fee: ", $"{new BigDecimal((BigInteger)(tx.SystemFee + tx.NetworkFee), NativeContract.GAS.Decimals)} GAS");
+ if (!ConsoleHelper.ReadUserInput("Relay tx? (no|yes)").IsYes()) // Add this in case just want to get hash but not relay
+ {
+ return;
+ }
+ SignAndSendTx(NeoSystem.StoreView, tx);
+ }
+ }
+
+ ///
+ /// Process "invoke" command
+ ///
+ /// Script hash
+ /// Operation
+ /// Contract parameters
+ /// Transaction's sender
+ /// Signer's accounts
+ /// Max fee for running the script
+ [ConsoleCommand("invoke", Category = "Contract Commands")]
+ private void OnInvokeCommand(UInt160 scriptHash, string operation, JArray? contractParameters = null, UInt160? sender = null, UInt160[]? signerAccounts = null, decimal maxGas = 20)
+ {
+ var gas = new BigDecimal(maxGas, NativeContract.GAS.Decimals);
+ Signer[] signers = Array.Empty();
+ if (!NoWallet())
+ {
+ if (sender == null)
+ sender = CurrentWallet!.GetDefaultAccount()?.ScriptHash;
+
+ if (sender != null)
+ {
+ if (signerAccounts == null)
+ signerAccounts = new UInt160[1] { sender };
+ else if (signerAccounts.Contains(sender) && signerAccounts[0] != sender)
+ {
+ var signersList = signerAccounts.ToList();
+ signersList.Remove(sender);
+ signerAccounts = signersList.Prepend(sender).ToArray();
+ }
+ else if (!signerAccounts.Contains(sender))
+ {
+ signerAccounts = signerAccounts.Prepend(sender).ToArray();
+ }
+ signers = signerAccounts.Select(p => new Signer() { Account = p, Scopes = WitnessScope.CalledByEntry }).ToArray();
+ }
+ }
+
+ Transaction tx = new Transaction
+ {
+ Signers = signers,
+ Attributes = Array.Empty(),
+ Witnesses = Array.Empty(),
+ };
+
+ if (!OnInvokeWithResult(scriptHash, operation, out _, tx, contractParameters, gas: (long)gas.Value)) return;
+
+ if (NoWallet()) return;
+ try
+ {
+ tx = CurrentWallet!.MakeTransaction(NeoSystem.StoreView, tx.Script, sender, signers, maxGas: (long)gas.Value);
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ return;
+ }
+ ConsoleHelper.Info("Network fee: ",
+ $"{new BigDecimal((BigInteger)tx.NetworkFee, NativeContract.GAS.Decimals)}\t",
+ "Total fee: ",
+ $"{new BigDecimal((BigInteger)(tx.SystemFee + tx.NetworkFee), NativeContract.GAS.Decimals)} GAS");
+ if (!ConsoleHelper.ReadUserInput("Relay tx? (no|yes)").IsYes())
+ {
+ return;
+ }
+ SignAndSendTx(NeoSystem.StoreView, tx);
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Logger.cs b/src/Neo.CLI/CLI/MainService.Logger.cs
new file mode 100644
index 0000000000..80624779c7
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Logger.cs
@@ -0,0 +1,176 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Logger.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 Neo.ConsoleService;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using static System.IO.Path;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ private static readonly ConsoleColorSet DebugColor = new(ConsoleColor.Cyan);
+ private static readonly ConsoleColorSet InfoColor = new(ConsoleColor.White);
+ private static readonly ConsoleColorSet WarningColor = new(ConsoleColor.Yellow);
+ private static readonly ConsoleColorSet ErrorColor = new(ConsoleColor.Red);
+ private static readonly ConsoleColorSet FatalColor = new(ConsoleColor.Red);
+
+ private readonly object syncRoot = new();
+ private bool _showLog = Settings.Default.Logger.ConsoleOutput;
+
+ private void Initialize_Logger()
+ {
+ Utility.Logging += OnLog;
+ }
+
+ private void Dispose_Logger()
+ {
+ Utility.Logging -= OnLog;
+ }
+
+ ///
+ /// Process "console log off" command to turn off console log
+ ///
+ [ConsoleCommand("console log off", Category = "Log Commands")]
+ private void OnLogOffCommand()
+ {
+ _showLog = false;
+ }
+
+ ///
+ /// Process "console log on" command to turn on the console log
+ ///
+ [ConsoleCommand("console log on", Category = "Log Commands")]
+ private void OnLogOnCommand()
+ {
+ _showLog = true;
+ }
+
+ private static void GetErrorLogs(StringBuilder sb, Exception ex)
+ {
+ sb.AppendLine(ex.GetType().ToString());
+ sb.AppendLine(ex.Message);
+ sb.AppendLine(ex.StackTrace);
+ if (ex is AggregateException ex2)
+ {
+ foreach (Exception inner in ex2.InnerExceptions)
+ {
+ sb.AppendLine();
+ GetErrorLogs(sb, inner);
+ }
+ }
+ else if (ex.InnerException != null)
+ {
+ sb.AppendLine();
+ GetErrorLogs(sb, ex.InnerException);
+ }
+ }
+
+ private void OnLog(string source, LogLevel level, object message)
+ {
+ if (!Settings.Default.Logger.Active)
+ return;
+
+ if (message is Exception ex)
+ {
+ var sb = new StringBuilder();
+ GetErrorLogs(sb, ex);
+ message = sb.ToString();
+ }
+
+ lock (syncRoot)
+ {
+ DateTime now = DateTime.Now;
+ var log = $"[{now.TimeOfDay:hh\\:mm\\:ss\\.fff}]";
+ if (_showLog)
+ {
+ var currentColor = new ConsoleColorSet();
+ var messages = message is string msg ? Parse(msg) : new[] { message.ToString() };
+ ConsoleColorSet logColor;
+ string logLevel;
+ switch (level)
+ {
+ case LogLevel.Debug: logColor = DebugColor; logLevel = "DEBUG"; break;
+ case LogLevel.Error: logColor = ErrorColor; logLevel = "ERROR"; break;
+ case LogLevel.Fatal: logColor = FatalColor; logLevel = "FATAL"; break;
+ case LogLevel.Info: logColor = InfoColor; logLevel = "INFO"; break;
+ case LogLevel.Warning: logColor = WarningColor; logLevel = "WARN"; break;
+ default: logColor = InfoColor; logLevel = "INFO"; break;
+ }
+ logColor.Apply();
+ Console.Write($"{logLevel} {log} \t{messages[0],-20}");
+ for (var i = 1; i < messages.Length; i++)
+ {
+ if (messages[i]?.Length > 20)
+ {
+ messages[i] = $"{messages[i]![..10]}...{messages[i]![(messages[i]!.Length - 10)..]}";
+ }
+ Console.Write(i % 2 == 0 ? $"={messages[i]} " : $" {messages[i]}");
+ }
+ currentColor.Apply();
+ Console.WriteLine();
+ }
+
+ if (string.IsNullOrEmpty(Settings.Default.Logger.Path)) return;
+ var sb = new StringBuilder(source);
+ foreach (var c in GetInvalidFileNameChars())
+ sb.Replace(c, '-');
+ var path = Combine(Settings.Default.Logger.Path, sb.ToString());
+ Directory.CreateDirectory(path);
+ path = Combine(path, $"{now:yyyy-MM-dd}.log");
+ try
+ {
+ File.AppendAllLines(path, new[] { $"[{level}]{log} {message}" });
+ }
+ catch (IOException)
+ {
+ Console.WriteLine("Error writing the log file: " + path);
+ }
+ }
+ }
+
+ ///
+ /// Parse the log message
+ ///
+ /// expected format [key1 = msg1 key2 = msg2]
+ ///
+ private static string[] Parse(string message)
+ {
+ var equals = message.Trim().Split('=');
+
+ if (equals.Length == 1) return new[] { message };
+
+ var messages = new List();
+ foreach (var t in @equals)
+ {
+ var msg = t.Trim();
+ var parts = msg.Split(' ');
+ var d = parts.Take(parts.Length - 1);
+
+ if (parts.Length > 1)
+ {
+ messages.Add(string.Join(" ", d));
+ }
+ var last = parts.LastOrDefault();
+ if (last is not null)
+ {
+ messages.Add(last);
+ }
+ }
+
+ return messages.ToArray();
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.NEP17.cs b/src/Neo.CLI/CLI/MainService.NEP17.cs
new file mode 100644
index 0000000000..131d171dc6
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.NEP17.cs
@@ -0,0 +1,140 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.NEP17.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 Neo.ConsoleService;
+using Neo.Json;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM.Types;
+using Neo.Wallets;
+using System;
+using System.Linq;
+using Array = System.Array;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "transfer" command
+ ///
+ /// Script hash
+ /// To
+ /// Amount
+ /// From
+ /// Data
+ /// Signer's accounts
+ [ConsoleCommand("transfer", Category = "NEP17 Commands")]
+ private void OnTransferCommand(UInt160 tokenHash, UInt160 to, decimal amount, UInt160? from = null, string? data = null, UInt160[]? signersAccounts = null)
+ {
+ var snapshot = NeoSystem.StoreView;
+ var asset = new AssetDescriptor(snapshot, NeoSystem.Settings, tokenHash);
+ var value = new BigDecimal(amount, asset.Decimals);
+
+ if (NoWallet()) return;
+
+ Transaction tx;
+ try
+ {
+ tx = CurrentWallet!.MakeTransaction(snapshot, new[]
+ {
+ new TransferOutput
+ {
+ AssetId = tokenHash,
+ Value = value,
+ ScriptHash = to,
+ Data = data
+ }
+ }, from: from, cosigners: signersAccounts?.Select(p => new Signer
+ {
+ // default access for transfers should be valid only for first invocation
+ Scopes = WitnessScope.CalledByEntry,
+ Account = p
+ })
+ .ToArray() ?? Array.Empty());
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ return;
+ }
+ if (!ConsoleHelper.ReadUserInput("Relay tx(no|yes)").IsYes())
+ {
+ return;
+ }
+ SignAndSendTx(snapshot, tx);
+ }
+
+ ///
+ /// Process "balanceOf" command
+ ///
+ /// Script hash
+ /// Address
+ [ConsoleCommand("balanceOf", Category = "NEP17 Commands")]
+ private void OnBalanceOfCommand(UInt160 tokenHash, UInt160 address)
+ {
+ var arg = new JObject
+ {
+ ["type"] = "Hash160",
+ ["value"] = address.ToString()
+ };
+
+ var asset = new AssetDescriptor(NeoSystem.StoreView, NeoSystem.Settings, tokenHash);
+
+ if (!OnInvokeWithResult(tokenHash, "balanceOf", out StackItem balanceResult, null, new JArray(arg))) return;
+
+ var balance = new BigDecimal(((PrimitiveType)balanceResult).GetInteger(), asset.Decimals);
+
+ Console.WriteLine();
+ ConsoleHelper.Info($"{asset.AssetName} balance: ", $"{balance}");
+ }
+
+ ///
+ /// Process "name" command
+ ///
+ /// Script hash
+ [ConsoleCommand("name", Category = "NEP17 Commands")]
+ private void OnNameCommand(UInt160 tokenHash)
+ {
+ ContractState contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, tokenHash);
+ if (contract == null) Console.WriteLine($"Contract hash not exist: {tokenHash}");
+ else ConsoleHelper.Info("Result: ", contract.Manifest.Name);
+ }
+
+ ///
+ /// Process "decimals" command
+ ///
+ /// Script hash
+ [ConsoleCommand("decimals", Category = "NEP17 Commands")]
+ private void OnDecimalsCommand(UInt160 tokenHash)
+ {
+ if (!OnInvokeWithResult(tokenHash, "decimals", out StackItem result)) return;
+
+ ConsoleHelper.Info("Result: ", $"{((PrimitiveType)result).GetInteger()}");
+ }
+
+ ///
+ /// Process "totalSupply" command
+ ///
+ /// Script hash
+ [ConsoleCommand("totalSupply", Category = "NEP17 Commands")]
+ private void OnTotalSupplyCommand(UInt160 tokenHash)
+ {
+ if (!OnInvokeWithResult(tokenHash, "totalSupply", out StackItem result)) return;
+
+ var asset = new AssetDescriptor(NeoSystem.StoreView, NeoSystem.Settings, tokenHash);
+ var totalSupply = new BigDecimal(((PrimitiveType)result).GetInteger(), asset.Decimals);
+
+ ConsoleHelper.Info("Result: ", $"{totalSupply}");
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Native.cs b/src/Neo.CLI/CLI/MainService.Native.cs
new file mode 100644
index 0000000000..b7562f3368
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Native.cs
@@ -0,0 +1,29 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Native.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 Neo.ConsoleService;
+using Neo.SmartContract.Native;
+using System.Linq;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "list nativecontract" command
+ ///
+ [ConsoleCommand("list nativecontract", Category = "Native Contract")]
+ private void OnListNativeContract()
+ {
+ NativeContract.Contracts.ToList().ForEach(p => ConsoleHelper.Info($"\t{p.Name,-20}", $"{p.Hash}"));
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Network.cs b/src/Neo.CLI/CLI/MainService.Network.cs
new file mode 100644
index 0000000000..38d6fd1d28
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Network.cs
@@ -0,0 +1,165 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Network.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 Akka.Actor;
+using Neo.ConsoleService;
+using Neo.IO;
+using Neo.Json;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Capabilities;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using System;
+using System.Net;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "broadcast addr" command
+ ///
+ /// Payload
+ /// Port
+ [ConsoleCommand("broadcast addr", Category = "Network Commands")]
+ private void OnBroadcastAddressCommand(IPAddress payload, ushort port)
+ {
+ if (payload == null)
+ {
+ ConsoleHelper.Warning("You must input the payload to relay.");
+ return;
+ }
+
+ OnBroadcastCommand(MessageCommand.Addr,
+ AddrPayload.Create(
+ NetworkAddressWithTime.Create(
+ payload, DateTime.UtcNow.ToTimestamp(),
+ new FullNodeCapability(),
+ new ServerCapability(NodeCapabilityType.TcpServer, port))
+ ));
+ }
+
+ ///
+ /// Process "broadcast block" command
+ ///
+ /// Hash
+ [ConsoleCommand("broadcast block", Category = "Network Commands")]
+ private void OnBroadcastGetBlocksByHashCommand(UInt256 hash)
+ {
+ OnBroadcastCommand(MessageCommand.Block, NativeContract.Ledger.GetBlock(NeoSystem.StoreView, hash));
+ }
+
+ ///
+ /// Process "broadcast block" command
+ ///
+ /// Block index
+ [ConsoleCommand("broadcast block", Category = "Network Commands")]
+ private void OnBroadcastGetBlocksByHeightCommand(uint height)
+ {
+ OnBroadcastCommand(MessageCommand.Block, NativeContract.Ledger.GetBlock(NeoSystem.StoreView, height));
+ }
+
+ ///
+ /// Process "broadcast getblocks" command
+ ///
+ /// Hash
+ [ConsoleCommand("broadcast getblocks", Category = "Network Commands")]
+ private void OnBroadcastGetBlocksCommand(UInt256 hash)
+ {
+ OnBroadcastCommand(MessageCommand.GetBlocks, GetBlocksPayload.Create(hash));
+ }
+
+ ///
+ /// Process "broadcast getheaders" command
+ ///
+ /// Index
+ [ConsoleCommand("broadcast getheaders", Category = "Network Commands")]
+ private void OnBroadcastGetHeadersCommand(uint index)
+ {
+ OnBroadcastCommand(MessageCommand.GetHeaders, GetBlockByIndexPayload.Create(index));
+ }
+
+ ///
+ /// Process "broadcast getdata" command
+ ///
+ /// Type
+ /// Payload
+ [ConsoleCommand("broadcast getdata", Category = "Network Commands")]
+ private void OnBroadcastGetDataCommand(InventoryType type, UInt256[] payload)
+ {
+ OnBroadcastCommand(MessageCommand.GetData, InvPayload.Create(type, payload));
+ }
+
+ ///
+ /// Process "broadcast inv" command
+ ///
+ /// Type
+ /// Payload
+ [ConsoleCommand("broadcast inv", Category = "Network Commands")]
+ private void OnBroadcastInvCommand(InventoryType type, UInt256[] payload)
+ {
+ OnBroadcastCommand(MessageCommand.Inv, InvPayload.Create(type, payload));
+ }
+
+ ///
+ /// Process "broadcast transaction" command
+ ///
+ /// Hash
+ [ConsoleCommand("broadcast transaction", Category = "Network Commands")]
+ private void OnBroadcastTransactionCommand(UInt256 hash)
+ {
+ if (NeoSystem.MemPool.TryGetValue(hash, out Transaction tx))
+ OnBroadcastCommand(MessageCommand.Transaction, tx);
+ }
+
+ private void OnBroadcastCommand(MessageCommand command, ISerializable ret)
+ {
+ NeoSystem.LocalNode.Tell(Message.Create(command, ret));
+ }
+
+ ///
+ /// Process "relay" command
+ ///
+ /// Json object
+ [ConsoleCommand("relay", Category = "Network Commands")]
+ private void OnRelayCommand(JObject jsonObjectToRelay)
+ {
+ if (jsonObjectToRelay == null)
+ {
+ ConsoleHelper.Warning("You must input JSON object to relay.");
+ return;
+ }
+
+ try
+ {
+ ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToRelay.ToString(), NeoSystem.StoreView);
+ if (!context.Completed)
+ {
+ ConsoleHelper.Error("The signature is incomplete.");
+ return;
+ }
+ if (!(context.Verifiable is Transaction tx))
+ {
+ ConsoleHelper.Warning("Only support to relay transaction.");
+ return;
+ }
+ tx.Witnesses = context.GetWitnesses();
+ NeoSystem.Blockchain.Tell(tx);
+ Console.WriteLine($"Data relay success, the hash is shown as follows: {Environment.NewLine}{tx.Hash}");
+ }
+ catch (Exception e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ }
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Node.cs b/src/Neo.CLI/CLI/MainService.Node.cs
new file mode 100644
index 0000000000..5dd16f53e8
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Node.cs
@@ -0,0 +1,119 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Node.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 Akka.Actor;
+using Neo.ConsoleService;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract.Native;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "show pool" command
+ ///
+ [ConsoleCommand("show pool", Category = "Node Commands", Description = "Show the current state of the mempool")]
+ private void OnShowPoolCommand(bool verbose = false)
+ {
+ int verifiedCount, unverifiedCount;
+ if (verbose)
+ {
+ NeoSystem.MemPool.GetVerifiedAndUnverifiedTransactions(
+ out IEnumerable verifiedTransactions,
+ out IEnumerable unverifiedTransactions);
+ ConsoleHelper.Info("Verified Transactions:");
+ foreach (Transaction tx in verifiedTransactions)
+ Console.WriteLine($" {tx.Hash} {tx.GetType().Name} {tx.NetworkFee} GAS_NetFee");
+ ConsoleHelper.Info("Unverified Transactions:");
+ foreach (Transaction tx in unverifiedTransactions)
+ Console.WriteLine($" {tx.Hash} {tx.GetType().Name} {tx.NetworkFee} GAS_NetFee");
+
+ verifiedCount = verifiedTransactions.Count();
+ unverifiedCount = unverifiedTransactions.Count();
+ }
+ else
+ {
+ verifiedCount = NeoSystem.MemPool.VerifiedCount;
+ unverifiedCount = NeoSystem.MemPool.UnVerifiedCount;
+ }
+ Console.WriteLine($"total: {NeoSystem.MemPool.Count}, verified: {verifiedCount}, unverified: {unverifiedCount}");
+ }
+
+ ///
+ /// Process "show state" command
+ ///
+ [ConsoleCommand("show state", Category = "Node Commands", Description = "Show the current state of the node")]
+ private void OnShowStateCommand()
+ {
+ var cancel = new CancellationTokenSource();
+
+ Console.CursorVisible = false;
+ Console.Clear();
+
+ Task broadcast = Task.Run(async () =>
+ {
+ while (!cancel.Token.IsCancellationRequested)
+ {
+ NeoSystem.LocalNode.Tell(Message.Create(MessageCommand.Ping, PingPayload.Create(NativeContract.Ledger.CurrentIndex(NeoSystem.StoreView))));
+ await Task.Delay(NeoSystem.Settings.TimePerBlock, cancel.Token);
+ }
+ });
+ Task task = Task.Run(async () =>
+ {
+ int maxLines = 0;
+ while (!cancel.Token.IsCancellationRequested)
+ {
+ uint height = NativeContract.Ledger.CurrentIndex(NeoSystem.StoreView);
+ uint headerHeight = NeoSystem.HeaderCache.Last?.Index ?? height;
+
+ Console.SetCursorPosition(0, 0);
+ WriteLineWithoutFlicker($"block: {height}/{headerHeight} connected: {LocalNode.ConnectedCount} unconnected: {LocalNode.UnconnectedCount}", Console.WindowWidth - 1);
+
+ int linesWritten = 1;
+ foreach (RemoteNode node in LocalNode.GetRemoteNodes().OrderByDescending(u => u.LastBlockIndex).Take(Console.WindowHeight - 2).ToArray())
+ {
+ ConsoleHelper.Info(" ip: ",
+ $"{node.Remote.Address,-15}\t",
+ "port: ",
+ $"{node.Remote.Port,-5}\t",
+ "listen: ",
+ $"{node.ListenerTcpPort,-5}\t",
+ "height: ",
+ $"{node.LastBlockIndex,-7}");
+ linesWritten++;
+ }
+
+ maxLines = Math.Max(maxLines, linesWritten);
+
+ while (linesWritten < maxLines)
+ {
+ WriteLineWithoutFlicker("", Console.WindowWidth - 1);
+ maxLines--;
+ }
+
+ await Task.Delay(500, cancel.Token);
+ }
+ });
+ ReadLine();
+ cancel.Cancel();
+ try { Task.WaitAll(task, broadcast); } catch { }
+ Console.WriteLine();
+ Console.CursorVisible = true;
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Plugins.cs b/src/Neo.CLI/CLI/MainService.Plugins.cs
new file mode 100644
index 0000000000..5b346fc33c
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Plugins.cs
@@ -0,0 +1,272 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Plugins.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 Akka.Util.Internal;
+using Microsoft.Extensions.Configuration;
+using Neo.ConsoleService;
+using Neo.Plugins;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Reflection;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "install" command
+ ///
+ /// Plugin name
+ [ConsoleCommand("install", Category = "Plugin Commands")]
+ private void OnInstallCommand(string pluginName)
+ {
+ if (PluginExists(pluginName))
+ {
+ ConsoleHelper.Warning($"Plugin already exist.");
+ return;
+ }
+
+ var result = InstallPluginAsync(pluginName).GetAwaiter().GetResult();
+ if (result)
+ {
+ var asmName = Assembly.GetExecutingAssembly().GetName().Name;
+ ConsoleHelper.Info("", $"Install successful, please restart \"{asmName}\".");
+ }
+ }
+
+ ///
+ /// Force to install a plugin again. This will overwrite
+ /// existing plugin files, in case of any file missing or
+ /// damage to the old version.
+ ///
+ /// name of the plugin
+ [ConsoleCommand("reinstall", Category = "Plugin Commands", Description = "Overwrite existing plugin by force.")]
+ private void OnReinstallCommand(string pluginName)
+ {
+ var result = InstallPluginAsync(pluginName, overWrite: true).GetAwaiter().GetResult();
+ if (result)
+ {
+ var asmName = Assembly.GetExecutingAssembly().GetName().Name;
+ ConsoleHelper.Info("", $"Reinstall successful, please restart \"{asmName}\".");
+ }
+ }
+
+ ///
+ /// Download plugin from github release
+ /// The function of download and install are divided
+ /// for the consideration of `update` command that
+ /// might be added in the future.
+ ///
+ /// name of the plugin
+ ///
+ ///
+ /// Downloaded content
+ private static async Task DownloadPluginAsync(string pluginName, Version pluginVersion, bool prerelease = false)
+ {
+ using var httpClient = new HttpClient();
+
+ var asmName = Assembly.GetExecutingAssembly().GetName();
+ 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}");
+ 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}");
+
+ var jsonAssets = jsonRelease
+ .AsObject()
+ .SingleOrDefault(s => s.Key == "assets").Value ?? throw new Exception("Could not find any Plugins");
+
+ var jsonPlugin = jsonAssets
+ .AsArray()
+ .SingleOrDefault(s =>
+ Path.GetFileNameWithoutExtension(
+ s!["name"]!.GetValue()).Equals(pluginName, StringComparison.InvariantCultureIgnoreCase))
+ ?? throw new Exception($"Could not find {pluginName}");
+
+ var downloadUrl = jsonPlugin["browser_download_url"]!.GetValue();
+
+ return await httpClient.GetStreamAsync(downloadUrl);
+ }
+
+ ///
+ /// Install plugin from stream
+ ///
+ /// Name of the plugin
+ /// Dependency set
+ /// Install by force for `update`
+ private async Task InstallPluginAsync(
+ string pluginName,
+ HashSet? installed = null,
+ bool overWrite = false)
+ {
+ installed ??= new HashSet();
+ if (!installed.Add(pluginName)) return false;
+ if (!overWrite && PluginExists(pluginName)) return false;
+
+ try
+ {
+
+ using var stream = await DownloadPluginAsync(pluginName, Settings.Default.Plugins.Version, Settings.Default.Plugins.Prerelease);
+
+ using var zip = new ZipArchive(stream, ZipArchiveMode.Read);
+ var entry = zip.Entries.FirstOrDefault(p => p.Name == "config.json");
+ if (entry is not null)
+ {
+ await using var es = entry.Open();
+ await InstallDependenciesAsync(es, installed);
+ }
+ zip.ExtractToDirectory("./", true);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ ConsoleHelper.Error(ex?.InnerException?.Message ?? ex!.Message);
+ }
+ return false;
+ }
+
+ ///
+ /// Install the dependency of the plugin
+ ///
+ /// plugin config path in temp
+ /// Dependency set
+ private async Task InstallDependenciesAsync(Stream config, HashSet installed)
+ {
+ var dependency = new ConfigurationBuilder()
+ .AddJsonStream(config)
+ .Build()
+ .GetSection("Dependency");
+
+ if (!dependency.Exists()) return;
+ var dependencies = dependency.GetChildren().Select(p => p.Get()).ToArray();
+ if (dependencies.Length == 0) return;
+
+ foreach (var plugin in dependencies.Where(p => p is not null && !PluginExists(p)))
+ {
+ ConsoleHelper.Info($"Installing dependency: {plugin}");
+ await InstallPluginAsync(plugin!, installed);
+ }
+ }
+
+ ///
+ /// Check that the plugin has all necessary files
+ ///
+ /// Name of the plugin
+ ///
+ private static bool PluginExists(string pluginName)
+ {
+ return Plugin.Plugins.Any(p => p.Name.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ /// Process "uninstall" command
+ ///
+ /// Plugin name
+ [ConsoleCommand("uninstall", Category = "Plugin Commands")]
+ private void OnUnInstallCommand(string pluginName)
+ {
+ if (!PluginExists(pluginName))
+ {
+ ConsoleHelper.Error("Plugin not found");
+ return;
+ }
+
+ foreach (var p in Plugin.Plugins)
+ {
+ try
+ {
+ using var reader = File.OpenRead($"Plugins/{p.Name}/config.json");
+ if (new ConfigurationBuilder()
+ .AddJsonStream(reader)
+ .Build()
+ .GetSection("Dependency")
+ .GetChildren()
+ .Select(s => s.Get())
+ .Any(a => a is not null && a.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)))
+ {
+ ConsoleHelper.Error($"{pluginName} is required by other plugins.");
+ ConsoleHelper.Info("Info: ", $"If plugin is damaged try to reinstall.");
+ return;
+ }
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+ try
+ {
+ Directory.Delete($"Plugins/{pluginName}", true);
+ }
+ catch (IOException) { }
+ ConsoleHelper.Info("", "Uninstall successful, please restart neo-cli.");
+ }
+
+ ///
+ /// Process "plugins" command
+ ///
+ [ConsoleCommand("plugins", Category = "Plugin Commands")]
+ private void OnPluginsCommand()
+ {
+ try
+ {
+ var plugins = GetPluginListAsync().GetAwaiter().GetResult();
+ if (plugins == null) return;
+ plugins
+ .Order()
+ .ForEach(f =>
+ {
+ var installedPlugin = Plugin.Plugins.SingleOrDefault(pp => string.Equals(pp.Name, f, StringComparison.CurrentCultureIgnoreCase));
+ if (installedPlugin != null)
+ {
+ var maxLength = plugins.Select(s => s.Length).OrderDescending().First();
+ string tabs = string.Empty;
+ if (f.Length < maxLength)
+ tabs = "\t";
+ ConsoleHelper.Info("", $"[Installed]\t {f,6}{tabs}", " @", $"{installedPlugin.Version.ToString(3)} {installedPlugin.Description}");
+ }
+ else
+ ConsoleHelper.Info($"[Not Installed]\t {f}");
+ });
+ }
+ catch (Exception ex)
+ {
+ ConsoleHelper.Error(ex!.InnerException?.Message ?? ex!.Message);
+ }
+ }
+
+ private async Task> GetPluginListAsync()
+ {
+ using var httpClient = new HttpClient();
+
+ var asmName = Assembly.GetExecutingAssembly().GetName();
+ 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}");
+ return json.AsArray()
+ .Where(w =>
+ w != null &&
+ w["tag_name"]!.GetValue() == $"v{Settings.Default.Plugins.Version.ToString(3)}")
+ .SelectMany(s => s!["assets"]!.AsArray())
+ .Select(s => Path.GetFileNameWithoutExtension(s!["name"]!.GetValue()));
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Tools.cs b/src/Neo.CLI/CLI/MainService.Tools.cs
new file mode 100644
index 0000000000..f4a5b762a9
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Tools.cs
@@ -0,0 +1,463 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Tools.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 Neo.ConsoleService;
+using Neo.IO;
+using Neo.Wallets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "parse" command
+ ///
+ [ConsoleCommand("parse", Category = "Base Commands", Description = "Parse a value to its possible conversions.")]
+ private void OnParseCommand(string value)
+ {
+ var parseFunctions = new Dictionary>()
+ {
+ { "Address to ScriptHash", AddressToScripthash },
+ { "Address to Base64", AddressToBase64 },
+ { "ScriptHash to Address", ScripthashToAddress },
+ { "Base64 to Address", Base64ToAddress },
+ { "Base64 to String", Base64ToString },
+ { "Base64 to Big Integer", Base64ToNumber },
+ { "Big Integer to Hex String", NumberToHex },
+ { "Big Integer to Base64", NumberToBase64 },
+ { "Hex String to String", HexToString },
+ { "Hex String to Big Integer", HexToNumber },
+ { "String to Hex String", StringToHex },
+ { "String to Base64", StringToBase64 }
+ };
+
+ bool any = false;
+
+ foreach (var pair in parseFunctions)
+ {
+ var parseMethod = pair.Value;
+ var result = parseMethod(value);
+
+ if (result != null)
+ {
+ Console.WriteLine($"{pair.Key,-30}\t{result}");
+ any = true;
+ }
+ }
+
+ if (!any)
+ {
+ ConsoleHelper.Warning($"Was not possible to convert: '{value}'");
+ }
+ }
+
+ ///
+ /// Converts an hexadecimal value to an UTF-8 string
+ ///
+ ///
+ /// Hexadecimal value to be converted
+ ///
+ ///
+ /// Returns null when is not possible to parse the hexadecimal value to a UTF-8
+ /// string or when the converted string is not printable; otherwise, returns
+ /// the string represented by the hexadecimal value
+ ///
+ private string? HexToString(string hexString)
+ {
+ try
+ {
+ var clearHexString = ClearHexString(hexString);
+ var bytes = clearHexString.HexToBytes();
+ var utf8String = Utility.StrictUTF8.GetString(bytes);
+ return IsPrintable(utf8String) ? utf8String : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts an hex value to a big integer
+ ///
+ ///
+ /// Hexadecimal value to be converted
+ ///
+ ///
+ /// Returns null when is not possible to parse the hex value to big integer value;
+ /// otherwise, returns the string that represents the converted big integer.
+ ///
+ private string? HexToNumber(string hexString)
+ {
+ try
+ {
+ var clearHexString = ClearHexString(hexString);
+ var bytes = clearHexString.HexToBytes();
+ var number = new BigInteger(bytes);
+
+ return number.ToString();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Formats a string value to a default hexadecimal representation of a byte array
+ ///
+ ///
+ /// The string value to be formatted
+ ///
+ ///
+ /// Returns the formatted string.
+ ///
+ ///
+ /// Throw when is the string is not a valid hex representation of a byte array.
+ ///
+ private string ClearHexString(string hexString)
+ {
+ bool hasHexPrefix = hexString.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase);
+
+ try
+ {
+ if (hasHexPrefix)
+ {
+ hexString = hexString.Substring(2);
+ }
+
+ if (hexString.Length % 2 == 1)
+ {
+ // if the length is an odd number, it cannot be parsed to a byte array
+ // it may be a valid hex string, so include a leading zero to parse correctly
+ hexString = "0" + hexString;
+ }
+
+ if (hasHexPrefix)
+ {
+ // if the input value starts with '0x', the first byte is the less significant
+ // to parse correctly, reverse the byte array
+ return hexString.HexToBytes().Reverse().ToArray().ToHexString();
+ }
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException();
+ }
+
+ return hexString;
+ }
+
+ ///
+ /// Converts a string in a hexadecimal value
+ ///
+ ///
+ /// String value to be converted
+ ///
+ ///
+ /// Returns null when it is not possible to parse the string value to a hexadecimal
+ /// value; otherwise returns the hexadecimal value that represents the converted string
+ ///
+ private string? StringToHex(string strParam)
+ {
+ try
+ {
+ var bytesParam = Utility.StrictUTF8.GetBytes(strParam);
+ return bytesParam.ToHexString();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts a string in Base64 string
+ ///
+ ///
+ /// String value to be converted
+ ///
+ ///
+ /// Returns null when is not possible to parse the string value to a Base64 value;
+ /// otherwise returns the Base64 value that represents the converted string
+ ///
+ ///
+ /// Throw .
+ ///
+ private string? StringToBase64(string strParam)
+ {
+ try
+ {
+ byte[] bytearray = Utility.StrictUTF8.GetBytes(strParam);
+ string base64 = Convert.ToBase64String(bytearray.AsSpan());
+ return base64;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts a string number in hexadecimal format
+ ///
+ ///
+ /// String that represents the number to be converted
+ ///
+ ///
+ /// Returns null when the string does not represent a big integer value or when
+ /// it is not possible to parse the big integer value to hexadecimal; otherwise,
+ /// returns the string that represents the converted hexadecimal value
+ ///
+ private string? NumberToHex(string strParam)
+ {
+ try
+ {
+ if (!BigInteger.TryParse(strParam, out var numberParam))
+ {
+ return null;
+ }
+ return numberParam.ToByteArray().ToHexString();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts a string number in Base64 byte array
+ ///
+ ///
+ /// String that represents the number to be converted
+ ///
+ ///
+ /// Returns null when the string does not represent a big integer value or when
+ /// it is not possible to parse the big integer value to Base64 value; otherwise,
+ /// returns the string that represents the converted Base64 value
+ ///
+ private string? NumberToBase64(string strParam)
+ {
+ try
+ {
+ if (!BigInteger.TryParse(strParam, out var number))
+ {
+ return null;
+ }
+ byte[] bytearray = number.ToByteArray();
+ string base64 = Convert.ToBase64String(bytearray.AsSpan());
+
+ return base64;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts an address to its corresponding scripthash
+ ///
+ ///
+ /// String that represents the address to be converted
+ ///
+ ///
+ /// Returns null when the string does not represent an address or when
+ /// it is not possible to parse the address to scripthash; otherwise returns
+ /// the string that represents the converted scripthash
+ ///
+ private string? AddressToScripthash(string address)
+ {
+ try
+ {
+ var bigEndScript = address.ToScriptHash(NeoSystem.Settings.AddressVersion);
+
+ return bigEndScript.ToString();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts an address to Base64 byte array
+ ///
+ ///
+ /// String that represents the address to be converted
+ ///
+ ///
+ /// Returns null when the string does not represent an address or when it is
+ /// not possible to parse the address to Base64 value; otherwise returns
+ /// the string that represents the converted Base64 value.
+ ///
+ private string? AddressToBase64(string address)
+ {
+ try
+ {
+ var script = address.ToScriptHash(NeoSystem.Settings.AddressVersion);
+ string base64 = Convert.ToBase64String(script.ToArray().AsSpan());
+
+ return base64;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts a big end script hash to its equivalent address
+ ///
+ ///
+ /// String that represents the scripthash to be converted
+ ///
+ ///
+ /// Returns null when the string does not represent an scripthash;
+ /// otherwise, returns the string that represents the converted address
+ ///
+ private string? ScripthashToAddress(string script)
+ {
+ try
+ {
+ UInt160 scriptHash;
+ if (script.StartsWith("0x"))
+ {
+ if (!UInt160.TryParse(script, out scriptHash))
+ {
+ return null;
+ }
+ }
+ else
+ {
+ if (!UInt160.TryParse(script, out UInt160 littleEndScript))
+ {
+ return null;
+ }
+ string bigEndScript = littleEndScript.ToArray().ToHexString();
+ if (!UInt160.TryParse(bigEndScript, out scriptHash))
+ {
+ return null;
+ }
+ }
+
+ var hexScript = scriptHash.ToAddress(NeoSystem.Settings.AddressVersion);
+ return hexScript;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts an Base64 byte array to address
+ ///
+ ///
+ /// String that represents the Base64 value
+ ///
+ ///
+ /// Returns null when the string does not represent an Base64 value or when
+ /// it is not possible to parse the Base64 value to address; otherwise,
+ /// returns the string that represents the converted address
+ ///
+ private string? Base64ToAddress(string bytearray)
+ {
+ try
+ {
+ byte[] result = Convert.FromBase64String(bytearray).Reverse().ToArray();
+ string hex = result.ToHexString();
+
+ if (!UInt160.TryParse(hex, out var scripthash))
+ {
+ return null;
+ }
+
+ string address = scripthash.ToAddress(NeoSystem.Settings.AddressVersion);
+ return address;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts an Base64 hex string to string
+ ///
+ ///
+ /// String that represents the Base64 value
+ ///
+ ///
+ /// Returns null when the string does not represent an Base64 value or when
+ /// it is not possible to parse the Base64 value to string value or the converted
+ /// string is not printable; otherwise, returns the string that represents
+ /// the Base64 value.
+ ///
+ private string? Base64ToString(string bytearray)
+ {
+ try
+ {
+ byte[] result = Convert.FromBase64String(bytearray);
+ string utf8String = Utility.StrictUTF8.GetString(result);
+ return IsPrintable(utf8String) ? utf8String : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts an Base64 hex string to big integer value
+ ///
+ ///
+ /// String that represents the Base64 value
+ ///
+ ///
+ /// Returns null when the string does not represent an Base64 value or when
+ /// it is not possible to parse the Base64 value to big integer value; otherwise
+ /// returns the string that represents the converted big integer
+ ///
+ private string? Base64ToNumber(string bytearray)
+ {
+ try
+ {
+ var bytes = Convert.FromBase64String(bytearray);
+ var number = new BigInteger(bytes);
+ return number.ToString();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Checks if the string is null or cannot be printed.
+ ///
+ ///
+ /// The string to test
+ ///
+ ///
+ /// Returns false if the string is null, or if it is empty, or if each character cannot be printed;
+ /// otherwise, returns true.
+ ///
+ private bool IsPrintable(string value)
+ {
+ return !string.IsNullOrWhiteSpace(value) && value.Any(c => !char.IsControl(c));
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Vote.cs b/src/Neo.CLI/CLI/MainService.Vote.cs
new file mode 100644
index 0000000000..12cd48b3a9
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Vote.cs
@@ -0,0 +1,246 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Vote.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 Neo.ConsoleService;
+using Neo.Cryptography.ECC;
+using Neo.Json;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using Neo.VM.Types;
+using Neo.Wallets;
+using System;
+using System.Numerics;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "register candidate" command
+ ///
+ /// register account scriptHash
+ [ConsoleCommand("register candidate", Category = "Vote Commands")]
+ 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();
+ }
+
+ SendTransaction(script, account, (long)testGas);
+ }
+
+ ///
+ /// Process "unregister candidate" command
+ ///
+ /// unregister account scriptHash
+ [ConsoleCommand("unregister candidate", Category = "Vote Commands")]
+ 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();
+ }
+
+ SendTransaction(script, account);
+ }
+
+ ///
+ /// Process "vote" command
+ ///
+ /// Sender account
+ /// Voting publicKey
+ [ConsoleCommand("vote", Category = "Vote Commands")]
+ 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();
+ }
+
+ SendTransaction(script, senderAccount);
+ }
+
+ ///
+ /// Process "unvote" command
+ ///
+ /// Sender account
+ [ConsoleCommand("unvote", Category = "Vote Commands")]
+ 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();
+ }
+
+ SendTransaction(script, senderAccount);
+ }
+
+ ///
+ /// Process "get candidates"
+ ///
+ [ConsoleCommand("get candidates", Category = "Vote Commands")]
+ private void OnGetCandidatesCommand()
+ {
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getCandidates", out StackItem result, null, null, false)) return;
+
+ var resJArray = (VM.Types.Array)result;
+
+ if (resJArray.Count > 0)
+ {
+ Console.WriteLine();
+ ConsoleHelper.Info("Candidates:");
+
+ foreach (var item in resJArray)
+ {
+ var value = (VM.Types.Array)item;
+ if (value is null) continue;
+
+ Console.Write(((ByteString)value[0])?.GetSpan().ToHexString() + "\t");
+ Console.WriteLine(((Integer)value[1]).GetInteger());
+ }
+ }
+ }
+
+ ///
+ /// Process "get committee"
+ ///
+ [ConsoleCommand("get committee", Category = "Vote Commands")]
+ private void OnGetCommitteeCommand()
+ {
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getCommittee", out StackItem result, null, null, false)) return;
+
+ var resJArray = (VM.Types.Array)result;
+
+ if (resJArray.Count > 0)
+ {
+ Console.WriteLine();
+ ConsoleHelper.Info("Committee:");
+
+ foreach (var item in resJArray)
+ {
+ Console.WriteLine(((ByteString)item)?.GetSpan().ToHexString());
+ }
+ }
+ }
+
+ ///
+ /// Process "get next validators"
+ ///
+ [ConsoleCommand("get next validators", Category = "Vote Commands")]
+ private void OnGetNextBlockValidatorsCommand()
+ {
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getNextBlockValidators", out StackItem result, null, null, false)) return;
+
+ var resJArray = (VM.Types.Array)result;
+
+ if (resJArray.Count > 0)
+ {
+ Console.WriteLine();
+ ConsoleHelper.Info("Next validators:");
+
+ foreach (var item in resJArray)
+ {
+ Console.WriteLine(((ByteString)item)?.GetSpan().ToHexString());
+ }
+ }
+ }
+
+ ///
+ /// Process "get accountstate"
+ ///
+ [ConsoleCommand("get accountstate", Category = "Vote Commands")]
+ private void OnGetAccountState(UInt160 address)
+ {
+ string notice = "No vote record!";
+ var arg = new JObject
+ {
+ ["type"] = "Hash160",
+ ["value"] = address.ToString()
+ };
+
+ if (!OnInvokeWithResult(NativeContract.NEO.Hash, "getAccountState", out StackItem result, null, new JArray(arg))) return;
+ Console.WriteLine();
+ if (result.IsNull)
+ {
+ ConsoleHelper.Warning(notice);
+ return;
+ }
+ var resJArray = (VM.Types.Array)result;
+ if (resJArray is null)
+ {
+ ConsoleHelper.Warning(notice);
+ return;
+ }
+
+ foreach (StackItem value in resJArray)
+ {
+ if (value.IsNull)
+ {
+ ConsoleHelper.Warning(notice);
+ return;
+ }
+ }
+ var publickey = ECPoint.Parse(((ByteString)resJArray[2])?.GetSpan().ToHexString(), ECCurve.Secp256r1);
+ ConsoleHelper.Info("Voted: ", Contract.CreateSignatureRedeemScript(publickey).ToScriptHash().ToAddress(NeoSystem.Settings.AddressVersion));
+ ConsoleHelper.Info("Amount: ", new BigDecimal(((Integer)resJArray[0]).GetInteger(), NativeContract.NEO.Decimals).ToString());
+ ConsoleHelper.Info("Block: ", ((Integer)resJArray[1]).GetInteger().ToString());
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Wallet.cs b/src/Neo.CLI/CLI/MainService.Wallet.cs
new file mode 100644
index 0000000000..4f65390f85
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Wallet.cs
@@ -0,0 +1,749 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.Wallet.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 Akka.Actor;
+using Neo.ConsoleService;
+using Neo.Cryptography.ECC;
+using Neo.Json;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using Neo.Wallets;
+using Neo.Wallets.NEP6;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using static Neo.SmartContract.Helper;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "open wallet" command
+ ///
+ /// Path
+ [ConsoleCommand("open wallet", Category = "Wallet Commands")]
+ private void OnOpenWallet(string path)
+ {
+ if (!File.Exists(path))
+ {
+ ConsoleHelper.Error("File does not exist");
+ return;
+ }
+ string password = ConsoleHelper.ReadUserInput("password", true);
+ if (password.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ try
+ {
+ OpenWallet(path, password);
+ }
+ catch (System.Security.Cryptography.CryptographicException)
+ {
+ ConsoleHelper.Error($"Failed to open file \"{path}\"");
+ }
+ }
+
+ ///
+ /// Process "close wallet" command
+ ///
+ [ConsoleCommand("close wallet", Category = "Wallet Commands")]
+ private void OnCloseWalletCommand()
+ {
+ if (NoWallet()) return;
+ CurrentWallet = null;
+ ConsoleHelper.Info("Wallet is closed");
+ }
+
+ ///
+ /// Process "upgrade wallet" command
+ ///
+ [ConsoleCommand("upgrade wallet", Category = "Wallet Commands")]
+ private void OnUpgradeWalletCommand(string path)
+ {
+ if (Path.GetExtension(path).ToLowerInvariant() != ".db3")
+ {
+ ConsoleHelper.Warning("Can't upgrade the wallet file. Check if your wallet is in db3 format.");
+ return;
+ }
+ if (!File.Exists(path))
+ {
+ ConsoleHelper.Error("File does not exist.");
+ return;
+ }
+ string password = ConsoleHelper.ReadUserInput("password", true);
+ if (password.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ string pathNew = Path.ChangeExtension(path, ".json");
+ if (File.Exists(pathNew))
+ {
+ ConsoleHelper.Warning($"File '{pathNew}' already exists");
+ return;
+ }
+ NEP6Wallet.Migrate(pathNew, path, password, NeoSystem.Settings).Save();
+ Console.WriteLine($"Wallet file upgrade complete. New wallet file has been auto-saved at: {pathNew}");
+ }
+
+ ///
+ /// Process "create address" command
+ ///
+ /// Count
+ [ConsoleCommand("create address", Category = "Wallet Commands")]
+ private void OnCreateAddressCommand(ushort count = 1)
+ {
+ if (NoWallet()) return;
+ string path = "address.txt";
+ if (File.Exists(path))
+ {
+ if (!ConsoleHelper.ReadUserInput($"The file '{path}' already exists, do you want to overwrite it? (yes|no)", false).IsYes())
+ {
+ return;
+ }
+ }
+
+ List addresses = new List();
+ using (var percent = new ConsolePercent(0, count))
+ {
+ Parallel.For(0, count, (i) =>
+ {
+ WalletAccount account = CurrentWallet!.CreateAccount();
+ lock (addresses)
+ {
+ addresses.Add(account.Address);
+ percent.Value++;
+ }
+ });
+ }
+
+ if (CurrentWallet is NEP6Wallet wallet)
+ wallet.Save();
+
+ Console.WriteLine($"Export addresses to {path}");
+ File.WriteAllLines(path, addresses);
+ }
+
+ ///
+ /// Process "delete address" command
+ ///
+ /// Address
+ [ConsoleCommand("delete address", Category = "Wallet Commands")]
+ private void OnDeleteAddressCommand(UInt160 address)
+ {
+ if (NoWallet()) return;
+
+ if (ConsoleHelper.ReadUserInput($"Warning: Irrevocable operation!\nAre you sure to delete account {address.ToAddress(NeoSystem.Settings.AddressVersion)}? (no|yes)").IsYes())
+ {
+ if (CurrentWallet!.DeleteAccount(address))
+ {
+ if (CurrentWallet is NEP6Wallet wallet)
+ {
+ wallet.Save();
+ }
+ ConsoleHelper.Info($"Address {address} deleted.");
+ }
+ else
+ {
+ ConsoleHelper.Warning($"Address {address} doesn't exist.");
+ }
+ }
+ }
+
+ ///
+ /// Process "export key" command
+ ///
+ /// Path
+ /// ScriptHash
+ [ConsoleCommand("export key", Category = "Wallet Commands")]
+ private void OnExportKeyCommand(string? path = null, UInt160? scriptHash = null)
+ {
+ if (NoWallet()) return;
+ if (path != null && File.Exists(path))
+ {
+ ConsoleHelper.Error($"File '{path}' already exists");
+ return;
+ }
+ string password = ConsoleHelper.ReadUserInput("password", true);
+ if (password.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ if (!CurrentWallet!.VerifyPassword(password))
+ {
+ ConsoleHelper.Error("Incorrect password");
+ return;
+ }
+ IEnumerable keys;
+ if (scriptHash == null)
+ keys = CurrentWallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey());
+ else
+ {
+ var account = CurrentWallet.GetAccount(scriptHash);
+ keys = account?.HasKey != true ? Array.Empty() : new[] { account.GetKey() };
+ }
+ if (path == null)
+ foreach (KeyPair key in keys)
+ Console.WriteLine(key.Export());
+ else
+ File.WriteAllLines(path, keys.Select(p => p.Export()));
+ }
+
+ ///
+ /// Process "create wallet" command
+ ///
+ [ConsoleCommand("create wallet", Category = "Wallet Commands")]
+ private void OnCreateWalletCommand(string path, string? wifOrFile = null)
+ {
+ string password = ConsoleHelper.ReadUserInput("password", true);
+ if (password.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ string password2 = ConsoleHelper.ReadUserInput("repeat password", true);
+ if (password != password2)
+ {
+ ConsoleHelper.Error("Two passwords not match.");
+ return;
+ }
+ if (File.Exists(path))
+ {
+ Console.WriteLine("This wallet already exists, please create another one.");
+ return;
+ }
+ bool createDefaultAccount = wifOrFile is null;
+ CreateWallet(path, password, createDefaultAccount);
+ if (!createDefaultAccount) OnImportKeyCommand(wifOrFile!);
+ }
+
+ ///
+ /// Process "import multisigaddress" command
+ ///
+ /// Required signatures
+ /// Public keys
+ [ConsoleCommand("import multisigaddress", Category = "Wallet Commands")]
+ private void OnImportMultisigAddress(ushort m, ECPoint[] publicKeys)
+ {
+ if (NoWallet()) return;
+ int n = publicKeys.Length;
+
+ if (m < 1 || m > n || n > 1024)
+ {
+ ConsoleHelper.Error("Invalid parameters.");
+ return;
+ }
+
+ Contract multiSignContract = Contract.CreateMultiSigContract(m, publicKeys);
+ KeyPair? keyPair = CurrentWallet!.GetAccounts().FirstOrDefault(p => p.HasKey && publicKeys.Contains(p.GetKey().PublicKey))?.GetKey();
+
+ CurrentWallet.CreateAccount(multiSignContract, keyPair);
+ if (CurrentWallet is NEP6Wallet wallet)
+ wallet.Save();
+
+ ConsoleHelper.Info("Multisig. Addr.: ", multiSignContract.ScriptHash.ToAddress(NeoSystem.Settings.AddressVersion));
+ }
+
+ ///
+ /// Process "import key" command
+ ///
+ [ConsoleCommand("import key", Category = "Wallet Commands")]
+ private void OnImportKeyCommand(string wifOrFile)
+ {
+ if (NoWallet()) return;
+ byte[]? prikey = null;
+ try
+ {
+ prikey = Wallet.GetPrivateKeyFromWIF(wifOrFile);
+ }
+ catch (FormatException) { }
+ if (prikey == null)
+ {
+ var fileInfo = new FileInfo(wifOrFile);
+
+ if (!fileInfo.Exists)
+ {
+ ConsoleHelper.Error($"File '{fileInfo.FullName}' doesn't exists");
+ return;
+ }
+
+ if (wifOrFile.Length > 1024 * 1024)
+ {
+ if (!ConsoleHelper.ReadUserInput($"The file '{fileInfo.FullName}' is too big, do you want to continue? (yes|no)", false).IsYes())
+ {
+ return;
+ }
+ }
+
+ string[] lines = File.ReadAllLines(fileInfo.FullName).Where(u => !string.IsNullOrEmpty(u)).ToArray();
+ using (var percent = new ConsolePercent(0, lines.Length))
+ {
+ for (int i = 0; i < lines.Length; i++)
+ {
+ if (lines[i].Length == 64)
+ prikey = lines[i].HexToBytes();
+ else
+ prikey = Wallet.GetPrivateKeyFromWIF(lines[i]);
+ CurrentWallet!.CreateAccount(prikey);
+ Array.Clear(prikey, 0, prikey.Length);
+ percent.Value++;
+ }
+ }
+ }
+ else
+ {
+ WalletAccount account = CurrentWallet!.CreateAccount(prikey);
+ Array.Clear(prikey, 0, prikey.Length);
+ ConsoleHelper.Info("Address: ", account.Address);
+ ConsoleHelper.Info(" Pubkey: ", account.GetKey().PublicKey.EncodePoint(true).ToHexString());
+ }
+ if (CurrentWallet is NEP6Wallet wallet)
+ wallet.Save();
+ }
+
+ ///
+ /// Process "import watchonly" command
+ ///
+ [ConsoleCommand("import watchonly", Category = "Wallet Commands")]
+ private void OnImportWatchOnlyCommand(string addressOrFile)
+ {
+ if (NoWallet()) return;
+ UInt160? address = null;
+ try
+ {
+ address = StringToAddress(addressOrFile, NeoSystem.Settings.AddressVersion);
+ }
+ catch (FormatException) { }
+ if (address is null)
+ {
+ var fileInfo = new FileInfo(addressOrFile);
+
+ if (!fileInfo.Exists)
+ {
+ ConsoleHelper.Warning($"File '{fileInfo.FullName}' doesn't exists");
+ return;
+ }
+
+ if (fileInfo.Length > 1024 * 1024)
+ {
+ if (!ConsoleHelper.ReadUserInput($"The file '{fileInfo.FullName}' is too big, do you want to continue? (yes|no)", false).IsYes())
+ {
+ return;
+ }
+ }
+
+ string[] lines = File.ReadAllLines(fileInfo.FullName).Where(u => !string.IsNullOrEmpty(u)).ToArray();
+ using (var percent = new ConsolePercent(0, lines.Length))
+ {
+ for (int i = 0; i < lines.Length; i++)
+ {
+ address = StringToAddress(lines[i], NeoSystem.Settings.AddressVersion);
+ CurrentWallet!.CreateAccount(address);
+ percent.Value++;
+ }
+ }
+ }
+ else
+ {
+ WalletAccount account = CurrentWallet!.GetAccount(address);
+ if (account is not null)
+ {
+ ConsoleHelper.Warning("This address is already in your wallet");
+ }
+ else
+ {
+ account = CurrentWallet.CreateAccount(address);
+ ConsoleHelper.Info("Address: ", account.Address);
+ }
+ }
+ if (CurrentWallet is NEP6Wallet wallet)
+ wallet.Save();
+ }
+
+ ///
+ /// Process "list address" command
+ ///
+ [ConsoleCommand("list address", Category = "Wallet Commands")]
+ private void OnListAddressCommand()
+ {
+ if (NoWallet()) return;
+ var snapshot = NeoSystem.StoreView;
+ foreach (var account in CurrentWallet!.GetAccounts())
+ {
+ var contract = account.Contract;
+ var type = "Nonstandard";
+
+ if (account.WatchOnly)
+ {
+ type = "WatchOnly";
+ }
+ else if (IsMultiSigContract(contract.Script))
+ {
+ type = "MultiSignature";
+ }
+ else if (IsSignatureContract(contract.Script))
+ {
+ type = "Standard";
+ }
+ else if (NativeContract.ContractManagement.GetContract(snapshot, account.ScriptHash) != null)
+ {
+ type = "Deployed-Nonstandard";
+ }
+
+ ConsoleHelper.Info(" Address: ", $"{account.Address}\t{type}");
+ ConsoleHelper.Info("ScriptHash: ", $"{account.ScriptHash}\n");
+ }
+ }
+
+ ///
+ /// Process "list asset" command
+ ///
+ [ConsoleCommand("list asset", Category = "Wallet Commands")]
+ private void OnListAssetCommand()
+ {
+ var snapshot = NeoSystem.StoreView;
+ if (NoWallet()) return;
+ foreach (UInt160 account in CurrentWallet!.GetAccounts().Select(p => p.ScriptHash))
+ {
+ Console.WriteLine(account.ToAddress(NeoSystem.Settings.AddressVersion));
+ ConsoleHelper.Info("NEO: ", $"{CurrentWallet.GetBalance(snapshot, NativeContract.NEO.Hash, account)}");
+ ConsoleHelper.Info("GAS: ", $"{CurrentWallet.GetBalance(snapshot, NativeContract.GAS.Hash, account)}");
+ Console.WriteLine();
+ }
+ Console.WriteLine("----------------------------------------------------");
+ 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());
+ }
+
+ ///
+ /// Process "list key" command
+ ///
+ [ConsoleCommand("list key", Category = "Wallet Commands")]
+ private void OnListKeyCommand()
+ {
+ if (NoWallet()) return;
+ foreach (WalletAccount account in CurrentWallet!.GetAccounts().Where(p => p.HasKey))
+ {
+ ConsoleHelper.Info(" Address: ", account.Address);
+ ConsoleHelper.Info("ScriptHash: ", account.ScriptHash.ToString());
+ ConsoleHelper.Info(" PublicKey: ", account.GetKey().PublicKey.EncodePoint(true).ToHexString());
+ Console.WriteLine();
+ }
+ }
+
+ ///
+ /// Process "sign" command
+ ///
+ /// Json object to sign
+ [ConsoleCommand("sign", Category = "Wallet Commands")]
+ private void OnSignCommand(JObject jsonObjectToSign)
+ {
+ if (NoWallet()) return;
+
+ if (jsonObjectToSign == null)
+ {
+ ConsoleHelper.Warning("You must input JSON object pending signature data.");
+ return;
+ }
+ try
+ {
+ var snapshot = NeoSystem.StoreView;
+ ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToSign.ToString(), snapshot);
+ if (context.Network != NeoSystem.Settings.Network)
+ {
+ ConsoleHelper.Warning("Network mismatch.");
+ return;
+ }
+ else if (!CurrentWallet!.Sign(context))
+ {
+ ConsoleHelper.Warning("Non-existent private key in wallet.");
+ return;
+ }
+ ConsoleHelper.Info("Signed Output: ", $"{Environment.NewLine}{context}");
+ }
+ catch (Exception e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ }
+ }
+
+ ///
+ /// Process "send" command
+ ///
+ /// Asset id
+ /// To
+ /// Amount
+ /// From
+ /// Data
+ /// Signer's accounts
+ [ConsoleCommand("send", Category = "Wallet Commands")]
+ private void OnSendCommand(UInt160 asset, UInt160 to, string amount, UInt160? from = null, string? data = null, UInt160[]? signerAccounts = null)
+ {
+ if (NoWallet()) return;
+ string password = ConsoleHelper.ReadUserInput("password", true);
+ if (password.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ if (!CurrentWallet!.VerifyPassword(password))
+ {
+ ConsoleHelper.Error("Incorrect password");
+ return;
+ }
+
+ var snapshot = NeoSystem.StoreView;
+ Transaction tx;
+ AssetDescriptor descriptor = new(snapshot, NeoSystem.Settings, asset);
+ if (!BigDecimal.TryParse(amount, descriptor.Decimals, out BigDecimal decimalAmount) || decimalAmount.Sign <= 0)
+ {
+ ConsoleHelper.Error("Incorrect Amount Format");
+ return;
+ }
+ try
+ {
+ tx = CurrentWallet.MakeTransaction(snapshot, new[]
+ {
+ new TransferOutput
+ {
+ AssetId = asset,
+ Value = decimalAmount,
+ ScriptHash = to,
+ Data = data
+ }
+ }, from: from, cosigners: signerAccounts?.Select(p => new Signer
+ {
+ // default access for transfers should be valid only for first invocation
+ Scopes = WitnessScope.CalledByEntry,
+ Account = p
+ })
+ .ToArray() ?? Array.Empty());
+ }
+ catch (Exception e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ return;
+ }
+
+ if (tx == null)
+ {
+ ConsoleHelper.Warning("Insufficient funds");
+ return;
+ }
+
+ ConsoleHelper.Info(
+ "Send To: ", $"{to.ToAddress(NeoSystem.Settings.AddressVersion)}\n",
+ "Network fee: ", $"{new BigDecimal((BigInteger)tx.NetworkFee, NativeContract.GAS.Decimals)}\t",
+ "Total fee: ", $"{new BigDecimal((BigInteger)(tx.SystemFee + tx.NetworkFee), NativeContract.GAS.Decimals)} GAS");
+ if (!ConsoleHelper.ReadUserInput("Relay tx? (no|yes)").IsYes())
+ {
+ return;
+ }
+ SignAndSendTx(NeoSystem.StoreView, tx);
+ }
+
+ ///
+ /// Process "cancel" command
+ ///
+ /// conflict txid
+ /// Transaction's sender
+ /// Signer's accounts
+ [ConsoleCommand("cancel", Category = "Wallet Commands")]
+ private void OnCancelCommand(UInt256 txid, UInt160? sender = null, UInt160[]? signerAccounts = null)
+ {
+ if (NoWallet()) return;
+
+ TransactionState state = NativeContract.Ledger.GetTransactionState(NeoSystem.StoreView, txid);
+ if (state != null)
+ {
+ ConsoleHelper.Error("This tx is already confirmed, can't be cancelled.");
+ return;
+ }
+
+ var conflict = new TransactionAttribute[] { new Conflicts() { Hash = txid } };
+ Signer[] signers = Array.Empty();
+ if (sender != null)
+ {
+ if (signerAccounts == null)
+ signerAccounts = new UInt160[1] { sender };
+ else if (signerAccounts.Contains(sender) && signerAccounts[0] != sender)
+ {
+ var signersList = signerAccounts.ToList();
+ signersList.Remove(sender);
+ signerAccounts = signersList.Prepend(sender).ToArray();
+ }
+ else if (!signerAccounts.Contains(sender))
+ {
+ signerAccounts = signerAccounts.Prepend(sender).ToArray();
+ }
+ signers = signerAccounts.Select(p => new Signer() { Account = p, Scopes = WitnessScope.None }).ToArray();
+ }
+
+ Transaction tx = new()
+ {
+ Signers = signers,
+ Attributes = conflict,
+ Witnesses = Array.Empty(),
+ };
+
+ try
+ {
+ using ScriptBuilder scriptBuilder = new();
+ scriptBuilder.Emit(OpCode.RET);
+ tx = CurrentWallet!.MakeTransaction(NeoSystem.StoreView, scriptBuilder.ToArray(), sender, signers, conflict);
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ return;
+ }
+
+ if (NeoSystem.MemPool.TryGetValue(txid, out Transaction conflictTx))
+ {
+ tx.NetworkFee = Math.Max(tx.NetworkFee, conflictTx.NetworkFee) + 1;
+ }
+ else
+ {
+ var snapshot = NeoSystem.StoreView;
+ AssetDescriptor descriptor = new(snapshot, NeoSystem.Settings, NativeContract.GAS.Hash);
+ string extracFee = ConsoleHelper.ReadUserInput("This tx is not in mempool, please input extra fee manually");
+ if (!BigDecimal.TryParse(extracFee, descriptor.Decimals, out BigDecimal decimalExtraFee) || decimalExtraFee.Sign <= 0)
+ {
+ ConsoleHelper.Error("Incorrect Amount Format");
+ return;
+ }
+ tx.NetworkFee += (long)decimalExtraFee.Value;
+ };
+
+ ConsoleHelper.Info("Network fee: ",
+ $"{new BigDecimal((BigInteger)tx.NetworkFee, NativeContract.GAS.Decimals)}\t",
+ "Total fee: ",
+ $"{new BigDecimal((BigInteger)(tx.SystemFee + tx.NetworkFee), NativeContract.GAS.Decimals)} GAS");
+ if (!ConsoleHelper.ReadUserInput("Relay tx? (no|yes)").IsYes())
+ {
+ return;
+ }
+ SignAndSendTx(NeoSystem.StoreView, tx);
+ }
+
+ ///
+ /// Process "show gas" command
+ ///
+ [ConsoleCommand("show gas", Category = "Wallet Commands")]
+ private void OnShowGasCommand()
+ {
+ if (NoWallet()) return;
+ BigInteger gas = BigInteger.Zero;
+ var snapshot = NeoSystem.StoreView;
+ uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1;
+ foreach (UInt160 account in CurrentWallet!.GetAccounts().Select(p => p.ScriptHash))
+ gas += NativeContract.NEO.UnclaimedGas(snapshot, account, height);
+ ConsoleHelper.Info("Unclaimed gas: ", new BigDecimal(gas, NativeContract.GAS.Decimals).ToString());
+ }
+
+ ///
+ /// Process "change password" command
+ ///
+ [ConsoleCommand("change password", Category = "Wallet Commands")]
+ private void OnChangePasswordCommand()
+ {
+ if (NoWallet()) return;
+ string oldPassword = ConsoleHelper.ReadUserInput("password", true);
+ if (oldPassword.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ if (!CurrentWallet!.VerifyPassword(oldPassword))
+ {
+ ConsoleHelper.Error("Incorrect password");
+ return;
+ }
+ string newPassword = ConsoleHelper.ReadUserInput("New password", true);
+ string newPasswordReEntered = ConsoleHelper.ReadUserInput("Re-Enter Password", true);
+ if (!newPassword.Equals(newPasswordReEntered))
+ {
+ ConsoleHelper.Error("Two passwords entered are inconsistent!");
+ return;
+ }
+
+ if (CurrentWallet is NEP6Wallet wallet)
+ {
+ string backupFile = wallet.Path + ".bak";
+ if (!File.Exists(wallet.Path) || File.Exists(backupFile))
+ {
+ ConsoleHelper.Error("Wallet backup fail");
+ return;
+ }
+ try
+ {
+ File.Copy(wallet.Path, backupFile);
+ }
+ catch (IOException)
+ {
+ ConsoleHelper.Error("Wallet backup fail");
+ return;
+ }
+ }
+
+ bool succeed = CurrentWallet.ChangePassword(oldPassword, newPassword);
+ if (succeed)
+ {
+ if (CurrentWallet is NEP6Wallet nep6Wallet)
+ nep6Wallet.Save();
+ Console.WriteLine("Password changed successfully");
+ }
+ else
+ {
+ ConsoleHelper.Error("Failed to change password");
+ }
+ }
+
+ private void SignAndSendTx(DataCache snapshot, Transaction tx)
+ {
+ if (NoWallet()) return;
+
+ ContractParametersContext context;
+ try
+ {
+ context = new ContractParametersContext(snapshot, tx, NeoSystem.Settings.Network);
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error("Failed creating contract params: " + GetExceptionMessage(e));
+ throw;
+ }
+ CurrentWallet!.Sign(context);
+ if (context.Completed)
+ {
+ tx.Witnesses = context.GetWitnesses();
+ NeoSystem.Blockchain.Tell(tx);
+ ConsoleHelper.Info("Signed and relayed transaction with hash:\n", $"{tx.Hash}");
+ }
+ else
+ {
+ ConsoleHelper.Info("Incomplete signature:\n", $"{context}");
+ }
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.cs b/src/Neo.CLI/CLI/MainService.cs
new file mode 100644
index 0000000000..b8d52a6a02
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.cs
@@ -0,0 +1,686 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// MainService.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 Akka.Actor;
+using Neo.ConsoleService;
+using Neo.Cryptography.ECC;
+using Neo.IO;
+using Neo.Json;
+using Neo.Ledger;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.Plugins;
+using Neo.SmartContract;
+using Neo.SmartContract.Manifest;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using Neo.VM.Types;
+using Neo.Wallets;
+using System;
+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.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Array = System.Array;
+
+namespace Neo.CLI
+{
+ public partial class MainService : ConsoleServiceBase, IWalletProvider
+ {
+ public event EventHandler? WalletChanged = null;
+
+ public const long TestModeGas = 20_00000000;
+
+ private Wallet? _currentWallet;
+
+ public Wallet? CurrentWallet
+ {
+ get => _currentWallet;
+ private set
+ {
+ _currentWallet = value;
+ WalletChanged?.Invoke(this, value);
+ }
+ }
+
+ private NeoSystem? _neoSystem;
+ public NeoSystem NeoSystem
+ {
+ get => _neoSystem!;
+ private set => _neoSystem = value;
+ }
+
+ private LocalNode? _localNode;
+
+ public LocalNode LocalNode
+ {
+ get => _localNode!;
+ private set => _localNode = value;
+ }
+
+ protected override string Prompt => "neo";
+ public override string ServiceName => "NEO-CLI";
+
+ ///
+ /// Constructor
+ ///
+ public MainService() : base()
+ {
+ RegisterCommandHandler(false, str => StringToAddress(str, NeoSystem.Settings.AddressVersion));
+ RegisterCommandHandler(false, UInt256.Parse);
+ RegisterCommandHandler(str => str.Select(u => UInt256.Parse(u.Trim())).ToArray());
+ RegisterCommandHandler(arr => arr.Select(str => StringToAddress(str, NeoSystem.Settings.AddressVersion)).ToArray());
+ RegisterCommandHandler(str => ECPoint.Parse(str.Trim(), ECCurve.Secp256r1));
+ RegisterCommandHandler(str => str.Select(u => ECPoint.Parse(u.Trim(), ECCurve.Secp256r1)).ToArray());
+ RegisterCommandHandler(str => JToken.Parse(str)!);
+ RegisterCommandHandler(str => (JObject)JToken.Parse(str)!);
+ RegisterCommandHandler(str => decimal.Parse(str, CultureInfo.InvariantCulture));
+ RegisterCommandHandler(obj => (JArray)obj);
+
+ RegisterCommand(this);
+
+ Initialize_Logger();
+ }
+
+ internal UInt160 StringToAddress(string input, byte version)
+ {
+ switch (input.ToLowerInvariant())
+ {
+ case "neo": return NativeContract.NEO.Hash;
+ case "gas": return NativeContract.GAS.Hash;
+ }
+
+ if (input.IndexOf('.') > 0 && input.LastIndexOf('.') < input.Length)
+ {
+ return ResolveNeoNameServiceAddress(input);
+ }
+
+ // Try to parse as UInt160
+
+ if (UInt160.TryParse(input, out var addr))
+ {
+ return addr;
+ }
+
+ // Accept wallet format
+
+ return input.ToScriptHash(version);
+ }
+
+ Wallet? IWalletProvider.GetWallet()
+ {
+ return CurrentWallet;
+ }
+
+ public override void RunConsole()
+ {
+ Console.ForegroundColor = ConsoleColor.DarkGreen;
+
+ var cliV = Assembly.GetAssembly(typeof(Program))!.GetVersion();
+ var neoV = Assembly.GetAssembly(typeof(NeoSystem))!.GetVersion();
+ var vmV = Assembly.GetAssembly(typeof(ExecutionEngine))!.GetVersion();
+ Console.WriteLine($"{ServiceName} v{cliV} - NEO v{neoV} - NEO-VM v{vmV}");
+ Console.WriteLine();
+
+ base.RunConsole();
+ }
+
+ public void CreateWallet(string path, string password, bool createDefaultAccount = true)
+ {
+ Wallet wallet = Wallet.Create(null, path, password, NeoSystem.Settings);
+ if (wallet == null)
+ {
+ ConsoleHelper.Warning("Wallet files in that format are not supported, please use a .json or .db3 file extension.");
+ return;
+ }
+ if (createDefaultAccount)
+ {
+ WalletAccount account = wallet.CreateAccount();
+ ConsoleHelper.Info(" Address: ", account.Address);
+ ConsoleHelper.Info(" Pubkey: ", account.GetKey().PublicKey.EncodePoint(true).ToHexString());
+ ConsoleHelper.Info("ScriptHash: ", $"{account.ScriptHash}");
+ }
+ wallet.Save();
+ CurrentWallet = wallet;
+ }
+
+ private IEnumerable GetBlocks(Stream stream, bool read_start = false)
+ {
+ using BinaryReader r = new BinaryReader(stream);
+ uint start = read_start ? r.ReadUInt32() : 0;
+ uint count = r.ReadUInt32();
+ uint end = start + count - 1;
+ uint currentHeight = NativeContract.Ledger.CurrentIndex(NeoSystem.StoreView);
+ if (end <= currentHeight) yield break;
+ for (uint height = start; height <= end; height++)
+ {
+ var size = r.ReadInt32();
+ if (size > Message.PayloadMaxSize)
+ throw new ArgumentException($"Block {height} exceeds the maximum allowed size");
+
+ byte[] array = r.ReadBytes(size);
+ if (height > currentHeight)
+ {
+ Block block = array.AsSerializable();
+ yield return block;
+ }
+ }
+ }
+
+ private IEnumerable GetBlocksFromFile()
+ {
+ const string pathAcc = "chain.acc";
+ if (File.Exists(pathAcc))
+ using (FileStream fs = new(pathAcc, FileMode.Open, FileAccess.Read, FileShare.Read))
+ foreach (var block in GetBlocks(fs))
+ yield return block;
+
+ const string pathAccZip = pathAcc + ".zip";
+ if (File.Exists(pathAccZip))
+ using (FileStream fs = new(pathAccZip, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (ZipArchive zip = new(fs, ZipArchiveMode.Read))
+ using (Stream? zs = zip.GetEntry(pathAcc)?.Open())
+ {
+ if (zs is not null)
+ {
+ foreach (var block in GetBlocks(zs))
+ yield return block;
+ }
+ }
+
+ var paths = Directory.EnumerateFiles(".", "chain.*.acc", SearchOption.TopDirectoryOnly).Concat(Directory.EnumerateFiles(".", "chain.*.acc.zip", SearchOption.TopDirectoryOnly)).Select(p => new
+ {
+ FileName = Path.GetFileName(p),
+ Start = uint.Parse(Regex.Match(p, @"\d+").Value),
+ IsCompressed = p.EndsWith(".zip")
+ }).OrderBy(p => p.Start);
+
+ uint height = NativeContract.Ledger.CurrentIndex(NeoSystem.StoreView);
+ foreach (var path in paths)
+ {
+ if (path.Start > height + 1) break;
+ if (path.IsCompressed)
+ using (FileStream fs = new(path.FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (ZipArchive zip = new(fs, ZipArchiveMode.Read))
+ using (Stream? zs = zip.GetEntry(Path.GetFileNameWithoutExtension(path.FileName))?.Open())
+ {
+ if (zs is not null)
+ {
+ foreach (var block in GetBlocks(zs, true))
+ yield return block;
+ }
+ }
+ else
+ using (FileStream fs = new(path.FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
+ foreach (var block in GetBlocks(fs, true))
+ yield return block;
+ }
+ }
+
+ private bool NoWallet()
+ {
+ if (CurrentWallet != null) return false;
+ ConsoleHelper.Error("You have to open the wallet first.");
+ return true;
+ }
+
+ private byte[] LoadDeploymentScript(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);
+
+ // Build script
+
+ using (ScriptBuilder sb = new ScriptBuilder())
+ {
+ if (dataParameter is not null)
+ sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nef.ToArray(), manifest.ToJson().ToString(), dataParameter);
+ else
+ sb.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", nef.ToArray(), manifest.ToJson().ToString());
+ return sb.ToArray();
+ }
+ }
+
+ 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);
+
+ // Build script
+
+ using (ScriptBuilder sb = new ScriptBuilder())
+ {
+ if (dataParameter is null)
+ sb.EmitDynamicCall(scriptHash, "update", nef.ToArray(), manifest.ToJson().ToString());
+ else
+ sb.EmitDynamicCall(scriptHash, "update", nef.ToArray(), manifest.ToJson().ToString(), dataParameter);
+ return sb.ToArray();
+ }
+ }
+
+ public override bool OnStart(string[] args)
+ {
+ if (!base.OnStart(args)) return false;
+ return OnStartWithCommandLine(args) != 1;
+ }
+
+ public override void OnStop()
+ {
+ base.OnStop();
+ Stop();
+ }
+
+ public void OpenWallet(string path, string password)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException();
+ }
+
+ CurrentWallet = Wallet.Open(path, password, NeoSystem.Settings) ?? throw new NotSupportedException();
+ }
+
+ public async void Start(CommandLineOptions options)
+ {
+ if (NeoSystem != null) return;
+ bool verifyImport = !(options.NoVerify ?? false);
+
+ ProtocolSettings protocol = ProtocolSettings.Load("config.json");
+ CustomProtocolSettings(options, protocol);
+ CustomApplicationSettings(options, Settings.Default);
+ NeoSystem = new NeoSystem(protocol, Settings.Default.Storage.Engine, string.Format(Settings.Default.Storage.Path, protocol.Network.ToString("X8")));
+ NeoSystem.AddService(this);
+
+ 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));
+ if (installTasks is not null)
+ {
+ await Task.WhenAll(installTasks);
+ }
+ foreach (var plugin in Plugin.Plugins)
+ {
+ // Register plugins commands
+
+ RegisterCommand(plugin, plugin.Name);
+ }
+
+ using (IEnumerator blocksBeingImported = GetBlocksFromFile().GetEnumerator())
+ {
+ while (true)
+ {
+ List blocksToImport = new List();
+ for (int i = 0; i < 10; i++)
+ {
+ if (!blocksBeingImported.MoveNext()) break;
+ blocksToImport.Add(blocksBeingImported.Current);
+ }
+ if (blocksToImport.Count == 0) break;
+ await NeoSystem.Blockchain.Ask(new Blockchain.Import
+ {
+ Blocks = blocksToImport,
+ Verify = verifyImport
+ });
+ if (NeoSystem is null) return;
+ }
+ }
+ NeoSystem.StartNode(new ChannelsConfig
+ {
+ Tcp = new IPEndPoint(IPAddress.Any, Settings.Default.P2P.Port),
+ MinDesiredConnections = Settings.Default.P2P.MinDesiredConnections,
+ MaxConnections = Settings.Default.P2P.MaxConnections,
+ MaxConnectionsPerAddress = Settings.Default.P2P.MaxConnectionsPerAddress
+ });
+
+ if (Settings.Default.UnlockWallet.IsActive)
+ {
+ try
+ {
+ if (Settings.Default.UnlockWallet.Path is null)
+ {
+ throw new InvalidOperationException("UnlockWallet.Path must be defined");
+ }
+ else if (Settings.Default.UnlockWallet.Password is null)
+ {
+ throw new InvalidOperationException("UnlockWallet.Password must be defined");
+ }
+
+ OpenWallet(Settings.Default.UnlockWallet.Path, Settings.Default.UnlockWallet.Password);
+ }
+ catch (FileNotFoundException)
+ {
+ ConsoleHelper.Warning($"wallet file \"{Settings.Default.UnlockWallet.Path}\" not found.");
+ }
+ catch (System.Security.Cryptography.CryptographicException)
+ {
+ ConsoleHelper.Error($"Failed to open file \"{Settings.Default.UnlockWallet.Path}\"");
+ }
+ catch (Exception ex)
+ {
+ ConsoleHelper.Error(ex.GetBaseException().Message);
+ }
+ }
+ }
+
+ public void Stop()
+ {
+ Dispose_Logger();
+ Interlocked.Exchange(ref _neoSystem, null)?.Dispose();
+ }
+
+ private void WriteBlocks(uint start, uint count, string path, bool writeStart)
+ {
+ uint end = start + count - 1;
+ using FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.WriteThrough);
+ if (fs.Length > 0)
+ {
+ byte[] buffer = new byte[sizeof(uint)];
+ if (writeStart)
+ {
+ fs.Seek(sizeof(uint), SeekOrigin.Begin);
+ fs.Read(buffer, 0, buffer.Length);
+ start += BitConverter.ToUInt32(buffer, 0);
+ fs.Seek(sizeof(uint), SeekOrigin.Begin);
+ }
+ else
+ {
+ fs.Read(buffer, 0, buffer.Length);
+ start = BitConverter.ToUInt32(buffer, 0);
+ fs.Seek(0, SeekOrigin.Begin);
+ }
+ }
+ else
+ {
+ if (writeStart)
+ {
+ fs.Write(BitConverter.GetBytes(start), 0, sizeof(uint));
+ }
+ }
+ if (start <= end)
+ fs.Write(BitConverter.GetBytes(count), 0, sizeof(uint));
+ fs.Seek(0, SeekOrigin.End);
+ Console.WriteLine("Export block from " + start + " to " + end);
+
+ using (var percent = new ConsolePercent(start, end))
+ {
+ for (uint i = start; i <= end; i++)
+ {
+ Block block = NativeContract.Ledger.GetBlock(NeoSystem.StoreView, i);
+ byte[] array = block.ToArray();
+ fs.Write(BitConverter.GetBytes(array.Length), 0, sizeof(int));
+ fs.Write(array, 0, array.Length);
+ percent.Value = i;
+ }
+ }
+ }
+
+ 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
+ ///
+ /// script
+ /// sender
+ /// Max fee for running the script
+ private void SendTransaction(byte[] script, UInt160? account = null, long gas = TestModeGas)
+ {
+ if (NoWallet()) return;
+
+ Signer[] signers = Array.Empty();
+ var snapshot = NeoSystem.StoreView;
+
+ if (account != null)
+ {
+ signers = CurrentWallet!.GetAccounts()
+ .Where(p => !p.Lock && !p.WatchOnly && p.ScriptHash == account && NativeContract.GAS.BalanceOf(snapshot, p.ScriptHash).Sign > 0)
+ .Select(p => new Signer { Account = p.ScriptHash, Scopes = WitnessScope.CalledByEntry })
+ .ToArray();
+ }
+
+ try
+ {
+ Transaction tx = CurrentWallet!.MakeTransaction(snapshot, script, account, signers, maxGas: gas);
+ ConsoleHelper.Info("Invoking script with: ", $"'{Convert.ToBase64String(tx.Script.Span)}'");
+
+ using (ApplicationEngine engine = ApplicationEngine.Run(tx.Script, snapshot, container: tx, settings: NeoSystem.Settings, gas: gas))
+ {
+ PrintExecutionOutput(engine, true);
+ if (engine.State == VMState.FAULT) return;
+ }
+
+ if (!ConsoleHelper.ReadUserInput("Relay tx(no|yes)").IsYes())
+ {
+ return;
+ }
+
+ SignAndSendTx(NeoSystem.StoreView, tx);
+ }
+ catch (InvalidOperationException e)
+ {
+ ConsoleHelper.Error(GetExceptionMessage(e));
+ }
+ }
+
+ ///
+ /// Process "invoke" command
+ ///
+ /// Script hash
+ /// Operation
+ /// Result
+ /// Transaction
+ /// Contract parameters
+ /// Show result stack if it is true
+ /// Max fee for running the script
+ /// 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 gas = TestModeGas)
+ {
+ List parameters = new();
+
+ if (contractParameters != null)
+ {
+ foreach (var contractParameter in contractParameters)
+ {
+ if (contractParameter is not null)
+ {
+ parameters.Add(ContractParameter.FromJson((JObject)contractParameter));
+ }
+ }
+ }
+
+ ContractState contract = NativeContract.ContractManagement.GetContract(NeoSystem.StoreView, scriptHash);
+ if (contract == null)
+ {
+ ConsoleHelper.Error("Contract does not exist.");
+ result = StackItem.Null;
+ return false;
+ }
+ else
+ {
+ if (contract.Manifest.Abi.GetMethod(operation, parameters.Count) == null)
+ {
+ ConsoleHelper.Error("This method does not not exist in this contract.");
+ result = StackItem.Null;
+ return false;
+ }
+ }
+
+ byte[] script;
+
+ using (ScriptBuilder scriptBuilder = new ScriptBuilder())
+ {
+ scriptBuilder.EmitDynamicCall(scriptHash, operation, parameters.ToArray());
+ script = scriptBuilder.ToArray();
+ ConsoleHelper.Info("Invoking script with: ", $"'{script.ToBase64String()}'");
+ }
+
+ if (verifiable is Transaction tx)
+ {
+ tx.Script = script;
+ }
+
+ using ApplicationEngine engine = ApplicationEngine.Run(script, NeoSystem.StoreView, container: verifiable, settings: NeoSystem.Settings, gas: gas);
+ PrintExecutionOutput(engine, showStack);
+ result = engine.State == VMState.FAULT ? StackItem.Null : engine.ResultStack.Peek();
+ return engine.State != VMState.FAULT;
+ }
+
+ private void PrintExecutionOutput(ApplicationEngine engine, bool showStack = true)
+ {
+ ConsoleHelper.Info("VM State: ", engine.State.ToString());
+ ConsoleHelper.Info("Gas Consumed: ", new BigDecimal((BigInteger)engine.GasConsumed, NativeContract.GAS.Decimals).ToString());
+
+ if (showStack)
+ ConsoleHelper.Info("Result Stack: ", new JArray(engine.ResultStack.Select(p => p.ToJson())).ToString());
+
+ if (engine.State == VMState.FAULT)
+ ConsoleHelper.Error(GetExceptionMessage(engine.FaultException));
+ }
+
+ static string GetExceptionMessage(Exception exception)
+ {
+ if (exception == null) return "Engine faulted.";
+
+ if (exception.InnerException != null)
+ {
+ return GetExceptionMessage(exception.InnerException);
+ }
+
+ return exception.Message;
+ }
+
+ public UInt160 ResolveNeoNameServiceAddress(string domain)
+ {
+ if (Settings.Default.Contracts.NeoNameService == UInt160.Zero)
+ throw new Exception("Neo Name Service (NNS): is disabled on this network.");
+
+ using var sb = new ScriptBuilder();
+ sb.EmitDynamicCall(Settings.Default.Contracts.NeoNameService, "resolve", CallFlags.ReadOnly, domain, 16);
+
+ using var appEng = ApplicationEngine.Run(sb.ToArray(), NeoSystem.StoreView, settings: NeoSystem.Settings);
+ if (appEng.State == VMState.HALT)
+ {
+ var data = appEng.ResultStack.Pop();
+ if (data is ByteString)
+ {
+ try
+ {
+ var addressData = data.GetString();
+ if (UInt160.TryParse(addressData, out var address))
+ return address;
+ else
+ return addressData.ToScriptHash(NeoSystem.Settings.AddressVersion);
+ }
+ catch { }
+ }
+ else if (data is Null)
+ {
+ throw new Exception($"Neo Name Service (NNS): \"{domain}\" domain not found.");
+ }
+ throw new Exception("Neo Name Service (NNS): Record invalid address 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): \"{domain}\" domain not found.");
+ }
+ }
+}
diff --git a/src/Neo.CLI/Dockerfile b/src/Neo.CLI/Dockerfile
new file mode 100644
index 0000000000..7b67a6812d
--- /dev/null
+++ b/src/Neo.CLI/Dockerfile
@@ -0,0 +1,20 @@
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0 AS Build
+
+# Run this from the repository root folder
+COPY src .
+COPY NuGet.Config /Neo.CLI
+
+WORKDIR /Neo.CLI
+RUN dotnet restore && dotnet publish -f net7.0 -c Release -o /app
+
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:7.0 AS Final
+RUN apt-get update && apt-get install -y \
+ screen \
+ libleveldb-dev \
+ sqlite3
+RUN rm -rf /var/lib/apt/lists/*
+
+WORKDIR /Neo.CLI
+COPY --from=Build /app .
+
+ENTRYPOINT ["screen","-DmS","node","dotnet","neo-cli.dll","-r"]
diff --git a/src/Neo.CLI/Extensions.cs b/src/Neo.CLI/Extensions.cs
new file mode 100644
index 0000000000..b8331201be
--- /dev/null
+++ b/src/Neo.CLI/Extensions.cs
@@ -0,0 +1,29 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// Extensions.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.Linq;
+using System.Reflection;
+
+namespace Neo
+{
+ ///
+ /// Extension methods
+ ///
+ internal static class Extensions
+ {
+ public static string GetVersion(this Assembly assembly)
+ {
+ CustomAttributeData? attribute = assembly.CustomAttributes.FirstOrDefault(p => p.AttributeType == typeof(AssemblyInformationalVersionAttribute));
+ if (attribute == null) return assembly.GetName().Version?.ToString(3) ?? string.Empty;
+ return (string)attribute.ConstructorArguments[0].Value!;
+ }
+ }
+}
diff --git a/src/Neo.CLI/Neo.CLI.csproj b/src/Neo.CLI/Neo.CLI.csproj
new file mode 100644
index 0000000000..0fa76ee2c9
--- /dev/null
+++ b/src/Neo.CLI/Neo.CLI.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net7.0
+ Neo.CLI
+ neo-cli
+ Exe
+ Neo.CLI
+ Neo
+ Neo.CLI
+ neo.ico
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Neo.CLI/Program.cs b/src/Neo.CLI/Program.cs
new file mode 100644
index 0000000000..b2a0f1b45c
--- /dev/null
+++ b/src/Neo.CLI/Program.cs
@@ -0,0 +1,24 @@
+// Copyright (C) 2015-2024 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 Neo.CLI;
+
+namespace Neo
+{
+ static class Program
+ {
+ static void Main(string[] args)
+ {
+ var mainService = new MainService();
+ mainService.Run(args);
+ }
+ }
+}
diff --git a/src/Neo.CLI/Settings.cs b/src/Neo.CLI/Settings.cs
new file mode 100644
index 0000000000..ecc38115c3
--- /dev/null
+++ b/src/Neo.CLI/Settings.cs
@@ -0,0 +1,186 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// Settings.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 Microsoft.Extensions.Configuration;
+using Neo.Network.P2P;
+using Neo.Persistence;
+using System;
+using System.Reflection;
+using System.Threading;
+
+namespace Neo
+{
+ public class Settings
+ {
+ public LoggerSettings Logger { get; init; }
+ public StorageSettings Storage { get; init; }
+ public P2PSettings P2P { get; init; }
+ public UnlockWalletSettings UnlockWallet { get; init; }
+ public ContractsSettings Contracts { get; init; }
+ public PluginsSettings Plugins { get; init; }
+
+ static Settings? s_default;
+
+ static bool UpdateDefault(IConfiguration configuration)
+ {
+ var settings = new Settings(configuration.GetSection("ApplicationConfiguration"));
+ return null == Interlocked.CompareExchange(ref s_default, settings, null);
+ }
+
+ public static bool Initialize(IConfiguration configuration)
+ {
+ return UpdateDefault(configuration);
+ }
+
+ public static Settings Default
+ {
+ get
+ {
+ if (s_default == null)
+ {
+ var config = new ConfigurationBuilder().AddJsonFile("config.json", optional: true).Build();
+ Initialize(config);
+ }
+ return Custom ?? s_default!;
+ }
+ }
+
+ public static Settings? Custom { get; set; }
+
+ public Settings(IConfigurationSection section)
+ {
+ Contracts = new(section.GetSection(nameof(Contracts)));
+ Logger = new(section.GetSection(nameof(Logger)));
+ Storage = new(section.GetSection(nameof(Storage)));
+ P2P = new(section.GetSection(nameof(P2P)));
+ UnlockWallet = new(section.GetSection(nameof(UnlockWallet)));
+ Plugins = new(section.GetSection(nameof(Plugins)));
+ }
+
+ public Settings()
+ {
+ Logger = new LoggerSettings();
+ Storage = new StorageSettings();
+ P2P = new P2PSettings();
+ UnlockWallet = new UnlockWalletSettings();
+ Contracts = new ContractsSettings();
+ Plugins = new PluginsSettings();
+ }
+ }
+
+ public class LoggerSettings
+ {
+ public string Path { get; init; } = string.Empty;
+ public bool ConsoleOutput { get; init; }
+ public bool Active { get; init; }
+
+ public LoggerSettings(IConfigurationSection section)
+ {
+ Path = section.GetValue(nameof(Path), "Logs")!;
+ ConsoleOutput = section.GetValue(nameof(ConsoleOutput), false);
+ Active = section.GetValue(nameof(Active), false);
+ }
+
+ public LoggerSettings() { }
+ }
+
+ public class StorageSettings
+ {
+ public string Engine { get; init; } = nameof(MemoryStore);
+ public string Path { get; init; } = string.Empty;
+
+ public StorageSettings(IConfigurationSection section)
+ {
+ Engine = section.GetValue(nameof(Engine), nameof(MemoryStore))!;
+ Path = section.GetValue(nameof(Path), string.Empty)!;
+ }
+
+ public StorageSettings() { }
+ }
+
+ public class P2PSettings
+ {
+ public ushort Port { get; }
+ public int MinDesiredConnections { get; }
+ public int MaxConnections { get; }
+ public int MaxConnectionsPerAddress { get; }
+
+ public P2PSettings(IConfigurationSection section)
+ {
+ Port = section.GetValue(nameof(Port), 10333);
+ MinDesiredConnections = section.GetValue(nameof(MinDesiredConnections), Peer.DefaultMinDesiredConnections);
+ MaxConnections = section.GetValue(nameof(MaxConnections), Peer.DefaultMaxConnections);
+ MaxConnectionsPerAddress = section.GetValue(nameof(MaxConnectionsPerAddress), 3);
+ }
+
+ public P2PSettings() { }
+ }
+
+ public class UnlockWalletSettings
+ {
+ public string? Path { get; init; } = string.Empty;
+ public string? Password { get; init; } = string.Empty;
+ public bool IsActive { get; init; } = false;
+
+ public UnlockWalletSettings(IConfigurationSection section)
+ {
+ if (section.Exists())
+ {
+ Path = section.GetValue(nameof(Path), string.Empty)!;
+ Password = section.GetValue(nameof(Password), string.Empty)!;
+ IsActive = section.GetValue(nameof(IsActive), false);
+ }
+ }
+
+ public UnlockWalletSettings() { }
+ }
+
+ public class ContractsSettings
+ {
+ public UInt160 NeoNameService { get; init; } = UInt160.Zero;
+
+ public ContractsSettings(IConfigurationSection section)
+ {
+ if (section.Exists())
+ {
+ if (UInt160.TryParse(section.GetValue(nameof(NeoNameService), string.Empty), out var hash))
+ {
+ NeoNameService = hash;
+ }
+ else
+ throw new ArgumentException("Neo Name Service (NNS): NeoNameService hash is invalid. Check your config.json.", nameof(NeoNameService));
+ }
+ }
+
+ public ContractsSettings() { }
+ }
+
+ public class PluginsSettings
+ {
+ public Uri DownloadUrl { get; init; } = new("https://api.github.com/repos/neo-project/neo-modules/releases");
+ public bool Prerelease { get; init; } = false;
+ public Version Version { get; init; } = Assembly.GetExecutingAssembly().GetName().Version!;
+
+ public PluginsSettings(IConfigurationSection section)
+ {
+ if (section.Exists())
+ {
+ DownloadUrl = section.GetValue(nameof(DownloadUrl), DownloadUrl)!;
+#if DEBUG
+ Prerelease = section.GetValue(nameof(Prerelease), Prerelease);
+ Version = section.GetValue(nameof(Version), Version)!;
+#endif
+ }
+ }
+
+ public PluginsSettings() { }
+ }
+}
diff --git a/src/Neo.CLI/config.fs.mainnet.json b/src/Neo.CLI/config.fs.mainnet.json
new file mode 100644
index 0000000000..fad987209a
--- /dev/null
+++ b/src/Neo.CLI/config.fs.mainnet.json
@@ -0,0 +1,58 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore]
+ "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id
+ },
+ "P2P": {
+ "Port": 40333,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ },
+ "Contracts": {
+ "NeoNameService": "0x7061fbd31562664b58f422c3dee4acfd70dba8af"
+ },
+ "Plugins": {
+ "DownloadUrl": "https://api.github.com/repos/neo-project/neo-modules/releases"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 91414437,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 512,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "026fa34ec057d74c2fdf1a18e336d0bd597ea401a0b2ad57340d5c220d09f44086",
+ "039a9db2a30942b1843db673aeb0d4fd6433f74cec1d879de6343cb9fcf7628fa4",
+ "0366d255e7ce23ea6f7f1e4bedf5cbafe598705b47e6ec213ef13b2f0819e8ab33",
+ "023f9cb7bbe154d529d5c719fdc39feaa831a43ae03d2a4280575b60f52fa7bc52",
+ "039ba959e0ab6dc616df8b803692f1c30ba9071b76b05535eb994bf5bbc402ad5f",
+ "035a2a18cddafa25ad353dea5e6730a1b9fcb4b918c4a0303c4387bb9c3b816adf",
+ "031f4d9c66f2ec348832c48fd3a16dfaeb59e85f557ae1e07f6696d0375c64f97b"
+ ],
+ "SeedList": [
+ "morph1.fs.neo.org:40333",
+ "morph2.fs.neo.org:40333",
+ "morph3.fs.neo.org:40333",
+ "morph4.fs.neo.org:40333",
+ "morph5.fs.neo.org:40333",
+ "morph6.fs.neo.org:40333",
+ "morph7.fs.neo.org:40333"
+ ]
+ }
+}
diff --git a/src/Neo.CLI/config.fs.testnet.json b/src/Neo.CLI/config.fs.testnet.json
new file mode 100644
index 0000000000..b167f35b81
--- /dev/null
+++ b/src/Neo.CLI/config.fs.testnet.json
@@ -0,0 +1,58 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore]
+ "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id
+ },
+ "P2P": {
+ "Port": 50333,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ },
+ "Contracts": {
+ "NeoNameService": "0xfb08ccf30ab534a871b7b092a49fe70c154ed678"
+ },
+ "Plugins": {
+ "DownloadUrl": "https://api.github.com/repos/neo-project/neo-modules/releases"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 91466898,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 512,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "02082828ec6efc92e5e7790da851be72d2091a961c1ac9a1772acbf181ac56b831",
+ "02b2bcf7e09c0237ab6ef21808e6f7546329823bc6b43488335bd357aea443fabe",
+ "03577029a5072ebbab12d2495b59e2cf27afb37f9640c1c1354f1bdd221e6fb82d",
+ "03e6ea086e4b42fa5f0535179862db7eea7e44644e5e9608d6131aa48868c12cfc",
+ "0379328ab4907ea7c47f61e5c9d2c78c39dc9d1c4341ca496376070a0a5e20131e",
+ "02f8af6440dfe0e676ae2bb6727e5cc31a6f2459e29f48e85428862b7577dbc203",
+ "02e19c0634c85d35937699cdeaa10595ec2e18bfe86ba0494cf6c5c6861c66b97d"
+ ],
+ "SeedList": [
+ "morph01.testnet.fs.neo.org:50333",
+ "morph02.testnet.fs.neo.org:50333",
+ "morph03.testnet.fs.neo.org:50333",
+ "morph04.testnet.fs.neo.org:50333",
+ "morph05.testnet.fs.neo.org:50333",
+ "morph06.testnet.fs.neo.org:50333",
+ "morph07.testnet.fs.neo.org:50333"
+ ]
+ }
+}
diff --git a/src/Neo.CLI/config.json b/src/Neo.CLI/config.json
new file mode 100644
index 0000000000..b4800b80ea
--- /dev/null
+++ b/src/Neo.CLI/config.json
@@ -0,0 +1,74 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore]
+ "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id
+ },
+ "P2P": {
+ "Port": 10333,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ },
+ "Contracts": {
+ "NeoNameService": "0x50ac1c37690cc2cfc594472833cf57505d5f46de"
+ },
+ "Plugins": {
+ "DownloadUrl": "https://api.github.com/repos/neo-project/neo-modules/releases"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 860833102,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 512,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "Hardforks": {
+ "HF_Aspidochelone": 1730000,
+ "HF_Basilisk": 4120000
+ },
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
+ "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093",
+ "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a",
+ "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554",
+ "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d",
+ "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e",
+ "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70",
+ "023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe",
+ "03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379",
+ "03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050",
+ "03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0",
+ "02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62",
+ "03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0",
+ "0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654",
+ "020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639",
+ "0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30",
+ "03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde",
+ "02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad",
+ "0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d",
+ "03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc",
+ "02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a"
+ ],
+ "SeedList": [
+ "seed1.neo.org:10333",
+ "seed2.neo.org:10333",
+ "seed3.neo.org:10333",
+ "seed4.neo.org:10333",
+ "seed5.neo.org:10333"
+ ]
+ }
+}
diff --git a/src/Neo.CLI/config.mainnet.json b/src/Neo.CLI/config.mainnet.json
new file mode 100644
index 0000000000..b4800b80ea
--- /dev/null
+++ b/src/Neo.CLI/config.mainnet.json
@@ -0,0 +1,74 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore]
+ "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id
+ },
+ "P2P": {
+ "Port": 10333,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ },
+ "Contracts": {
+ "NeoNameService": "0x50ac1c37690cc2cfc594472833cf57505d5f46de"
+ },
+ "Plugins": {
+ "DownloadUrl": "https://api.github.com/repos/neo-project/neo-modules/releases"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 860833102,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 512,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "Hardforks": {
+ "HF_Aspidochelone": 1730000,
+ "HF_Basilisk": 4120000
+ },
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c",
+ "02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093",
+ "03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a",
+ "02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554",
+ "024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d",
+ "02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e",
+ "02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70",
+ "023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe",
+ "03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379",
+ "03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050",
+ "03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0",
+ "02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62",
+ "03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0",
+ "0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654",
+ "020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639",
+ "0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30",
+ "03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde",
+ "02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad",
+ "0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d",
+ "03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc",
+ "02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a"
+ ],
+ "SeedList": [
+ "seed1.neo.org:10333",
+ "seed2.neo.org:10333",
+ "seed3.neo.org:10333",
+ "seed4.neo.org:10333",
+ "seed5.neo.org:10333"
+ ]
+ }
+}
diff --git a/src/Neo.CLI/config.testnet.json b/src/Neo.CLI/config.testnet.json
new file mode 100644
index 0000000000..19a9ca1442
--- /dev/null
+++ b/src/Neo.CLI/config.testnet.json
@@ -0,0 +1,74 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore", // Candidates [MemoryStore, LevelDBStore, RocksDBStore]
+ "Path": "Data_LevelDB_{0}" // {0} is a placeholder for the network id
+ },
+ "P2P": {
+ "Port": 20333,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ },
+ "Contracts": {
+ "NeoNameService": "0x50ac1c37690cc2cfc594472833cf57505d5f46de"
+ },
+ "Plugins": {
+ "DownloadUrl": "https://api.github.com/repos/neo-project/neo-modules/releases"
+ }
+ },
+ "ProtocolConfiguration": {
+ "Network": 894710606,
+ "AddressVersion": 53,
+ "MillisecondsPerBlock": 15000,
+ "MaxTransactionsPerBlock": 5000,
+ "MemoryPoolMaxTransactions": 50000,
+ "MaxTraceableBlocks": 2102400,
+ "Hardforks": {
+ "HF_Aspidochelone": 210000,
+ "HF_Basilisk": 2680000
+ },
+ "InitialGasDistribution": 5200000000000000,
+ "ValidatorsCount": 7,
+ "StandbyCommittee": [
+ "023e9b32ea89b94d066e649b124fd50e396ee91369e8e2a6ae1b11c170d022256d",
+ "03009b7540e10f2562e5fd8fac9eaec25166a58b26e412348ff5a86927bfac22a2",
+ "02ba2c70f5996f357a43198705859fae2cfea13e1172962800772b3d588a9d4abd",
+ "03408dcd416396f64783ac587ea1e1593c57d9fea880c8a6a1920e92a259477806",
+ "02a7834be9b32e2981d157cb5bbd3acb42cfd11ea5c3b10224d7a44e98c5910f1b",
+ "0214baf0ceea3a66f17e7e1e839ea25fd8bed6cd82e6bb6e68250189065f44ff01",
+ "030205e9cefaea5a1dfc580af20c8d5aa2468bb0148f1a5e4605fc622c80e604ba",
+ "025831cee3708e87d78211bec0d1bfee9f4c85ae784762f042e7f31c0d40c329b8",
+ "02cf9dc6e85d581480d91e88e8cbeaa0c153a046e89ded08b4cefd851e1d7325b5",
+ "03840415b0a0fcf066bcc3dc92d8349ebd33a6ab1402ef649bae00e5d9f5840828",
+ "026328aae34f149853430f526ecaa9cf9c8d78a4ea82d08bdf63dd03c4d0693be6",
+ "02c69a8d084ee7319cfecf5161ff257aa2d1f53e79bf6c6f164cff5d94675c38b3",
+ "0207da870cedb777fceff948641021714ec815110ca111ccc7a54c168e065bda70",
+ "035056669864feea401d8c31e447fb82dd29f342a9476cfd449584ce2a6165e4d7",
+ "0370c75c54445565df62cfe2e76fbec4ba00d1298867972213530cae6d418da636",
+ "03957af9e77282ae3263544b7b2458903624adc3f5dee303957cb6570524a5f254",
+ "03d84d22b8753cf225d263a3a782a4e16ca72ef323cfde04977c74f14873ab1e4c",
+ "02147c1b1d5728e1954958daff2f88ee2fa50a06890a8a9db3fa9e972b66ae559f",
+ "03c609bea5a4825908027e4ab217e7efc06e311f19ecad9d417089f14927a173d5",
+ "0231edee3978d46c335e851c76059166eb8878516f459e085c0dd092f0f1d51c21",
+ "03184b018d6b2bc093e535519732b3fd3f7551c8cffaf4621dd5a0b89482ca66c9"
+ ],
+ "SeedList": [
+ "seed1t5.neo.org:20333",
+ "seed2t5.neo.org:20333",
+ "seed3t5.neo.org:20333",
+ "seed4t5.neo.org:20333",
+ "seed5t5.neo.org:20333"
+ ]
+ }
+}
diff --git a/src/Neo.CLI/neo.ico b/src/Neo.CLI/neo.ico
new file mode 100644
index 0000000000..403aa7f376
Binary files /dev/null and b/src/Neo.CLI/neo.ico differ
diff --git a/src/Neo.ConsoleService/CommandQuoteToken.cs b/src/Neo.ConsoleService/CommandQuoteToken.cs
new file mode 100644
index 0000000000..865b7560d0
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandQuoteToken.cs
@@ -0,0 +1,54 @@
+// Copyright (C) 2015-2024 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
new file mode 100644
index 0000000000..87080d1983
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandSpaceToken.cs
@@ -0,0 +1,65 @@
+// Copyright (C) 2015-2024 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
new file mode 100644
index 0000000000..49a5702756
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandStringToken.cs
@@ -0,0 +1,91 @@
+// Copyright (C) 2015-2024 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
new file mode 100644
index 0000000000..10e8336ecc
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandToken.cs
@@ -0,0 +1,229 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// CommandToken.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.Text;
+
+namespace Neo.ConsoleService
+{
+ internal abstract class CommandToken
+ {
+ ///
+ /// Offset
+ ///
+ public int Offset { get; }
+
+ ///
+ /// Type
+ ///
+ public CommandTokenType Type { get; }
+
+ ///
+ /// Value
+ ///
+ public string Value { get; protected init; } = string.Empty;
+
+ ///
+ /// Constructor
+ ///
+ /// Type
+ /// Offset
+ protected CommandToken(CommandTokenType type, int offset)
+ {
+ Type = type;
+ Offset = offset;
+ }
+
+ ///
+ /// Parse command line
+ ///
+ /// 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;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Create string arguments
+ ///
+ /// 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();
+ }
+
+ ///
+ /// Create a string from token list
+ ///
+ /// Tokens
+ /// String
+ public static string ToString(IEnumerable tokens)
+ {
+ var sb = new StringBuilder();
+
+ foreach (var token in tokens)
+ {
+ sb.Append(token.Value);
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Trim
+ ///
+ /// 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);
+ }
+ }
+
+ ///
+ /// Read String
+ ///
+ /// 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();
+ }
+ case CommandSpaceToken _: throw new ArgumentException();
+ 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;
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/CommandTokenType.cs b/src/Neo.ConsoleService/CommandTokenType.cs
new file mode 100644
index 0000000000..2e522c000a
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandTokenType.cs
@@ -0,0 +1,20 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// CommandTokenType.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.
+
+namespace Neo.ConsoleService
+{
+ internal enum CommandTokenType : byte
+ {
+ String,
+ Space,
+ Quote,
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleColorSet.cs b/src/Neo.ConsoleService/ConsoleColorSet.cs
new file mode 100644
index 0000000000..8e5c7f7b45
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleColorSet.cs
@@ -0,0 +1,52 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// ConsoleColorSet.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;
+
+namespace Neo.ConsoleService
+{
+ public class ConsoleColorSet
+ {
+ public ConsoleColor Foreground;
+ public ConsoleColor Background;
+
+ ///
+ /// Create a new color set with the current console colors
+ ///
+ public ConsoleColorSet() : this(Console.ForegroundColor, Console.BackgroundColor) { }
+
+ ///
+ /// Create a new color set
+ ///
+ /// Foreground color
+ public ConsoleColorSet(ConsoleColor foreground) : this(foreground, Console.BackgroundColor) { }
+
+ ///
+ /// Create a new color set
+ ///
+ /// Foreground color
+ /// Background color
+ public ConsoleColorSet(ConsoleColor foreground, ConsoleColor background)
+ {
+ Foreground = foreground;
+ Background = background;
+ }
+
+ ///
+ /// Apply the current set
+ ///
+ public void Apply()
+ {
+ Console.ForegroundColor = Foreground;
+ Console.BackgroundColor = Background;
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleCommandAttribute.cs b/src/Neo.ConsoleService/ConsoleCommandAttribute.cs
new file mode 100644
index 0000000000..c7831d617d
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleCommandAttribute.cs
@@ -0,0 +1,46 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// ConsoleCommandAttribute.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;
+using System.Linq;
+
+namespace Neo.ConsoleService
+{
+ [DebuggerDisplay("Verbs={string.Join(' ',Verbs)}")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class ConsoleCommandAttribute : Attribute
+ {
+ ///
+ /// Verbs
+ ///
+ public string[] Verbs { get; }
+
+ ///
+ /// Category
+ ///
+ public string Category { get; set; } = string.Empty;
+
+ ///
+ /// Description
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Constructor
+ ///
+ /// Verbs
+ public ConsoleCommandAttribute(string verbs)
+ {
+ Verbs = verbs.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(u => u.ToLowerInvariant()).ToArray();
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleCommandMethod.cs b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
new file mode 100644
index 0000000000..3455d21bb4
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
@@ -0,0 +1,121 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// ConsoleCommandMethod.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.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+
+namespace Neo.ConsoleService
+{
+ [DebuggerDisplay("Key={Key}")]
+ internal class ConsoleCommandMethod
+ {
+ ///
+ /// Verbs
+ ///
+ public string[] Verbs { get; }
+
+ ///
+ /// Key
+ ///
+ public string Key => string.Join(' ', Verbs);
+
+ ///
+ /// Help category
+ ///
+ public string HelpCategory { get; set; }
+
+ ///
+ /// Help message
+ ///
+ public string HelpMessage { get; set; }
+
+ ///
+ /// Instance
+ ///
+ public object Instance { get; }
+
+ ///
+ /// Method
+ ///
+ public MethodInfo Method { get; }
+
+ ///
+ /// Set instance command
+ ///
+ /// Instance
+ /// Method
+ /// Attribute
+ public ConsoleCommandMethod(object instance, MethodInfo method, ConsoleCommandAttribute attribute)
+ {
+ Method = method;
+ Instance = instance;
+ Verbs = attribute.Verbs;
+ HelpCategory = attribute.Category;
+ HelpMessage = attribute.Description;
+ }
+
+ ///
+ /// Is this command
+ ///
+ /// Tokens
+ /// Consumed Arguments
+ /// True if is this command
+ public bool IsThisCommand(CommandToken[] tokens, out int consumedArgs)
+ {
+ 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)
+ {
+ tokenList.RemoveAt(0);
+ }
+
+ // Trim start
+
+ while (tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Space) tokenList.RemoveAt(0);
+
+ consumedArgs = tokens.Length - tokenList.Count;
+ return checks == 0;
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleHelper.cs b/src/Neo.ConsoleService/ConsoleHelper.cs
new file mode 100644
index 0000000000..78f170a3af
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleHelper.cs
@@ -0,0 +1,163 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// ConsoleHelper.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.Security;
+using System.Text;
+
+namespace Neo.ConsoleService
+{
+ public static class ConsoleHelper
+ {
+ private static readonly ConsoleColorSet InfoColor = new(ConsoleColor.Cyan);
+ private static readonly ConsoleColorSet WarningColor = new(ConsoleColor.Yellow);
+ private static readonly ConsoleColorSet ErrorColor = new(ConsoleColor.Red);
+
+ public static bool ReadingPassword { get; private set; } = false;
+
+ ///
+ /// Info handles message in the format of "[tag]:[message]",
+ /// avoid using Info if the `tag` is too long
+ ///
+ /// The log message in pairs of (tag, message)
+ public static void Info(params string[] values)
+ {
+ var currentColor = new ConsoleColorSet();
+
+ for (int i = 0; i < values.Length; i++)
+ {
+ if (i % 2 == 0)
+ InfoColor.Apply();
+ else
+ currentColor.Apply();
+ Console.Write(values[i]);
+ }
+ currentColor.Apply();
+ Console.WriteLine();
+ }
+
+ ///
+ /// Use warning if something unexpected happens
+ /// or the execution result is not correct.
+ /// Also use warning if you just want to remind
+ /// user of doing something.
+ ///
+ /// Warning message
+ public static void Warning(string msg)
+ {
+ Log("Warning", WarningColor, msg);
+ }
+
+ ///
+ /// Use Error if the verification or input format check fails
+ /// or exception that breaks the execution of interactive
+ /// command throws.
+ ///
+ /// Error message
+ public static void Error(string msg)
+ {
+ Log("Error", ErrorColor, msg);
+ }
+
+ private static void Log(string tag, ConsoleColorSet colorSet, string msg)
+ {
+ var currentColor = new ConsoleColorSet();
+
+ colorSet.Apply();
+ Console.Write($"{tag}: ");
+ currentColor.Apply();
+ Console.WriteLine(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 + ": ");
+ }
+
+ if (password) ReadingPassword = true;
+ var prevForeground = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.Yellow;
+
+ if (Console.IsInputRedirected)
+ {
+ // neo-gui Console require it
+ sb.Append(Console.ReadLine());
+ }
+ else
+ {
+ ConsoleKeyInfo key;
+ do
+ {
+ key = Console.ReadKey(true);
+
+ if (t.IndexOf(key.KeyChar) != -1)
+ {
+ sb.Append(key.KeyChar);
+ Console.Write(password ? '*' : key.KeyChar);
+ }
+ else if (key.Key == ConsoleKey.Backspace && sb.Length > 0)
+ {
+ sb.Length--;
+ Console.Write("\b \b");
+ }
+ } while (key.Key != ConsoleKey.Enter);
+ }
+
+ Console.ForegroundColor = prevForeground;
+ if (password) ReadingPassword = false;
+ Console.WriteLine();
+ return sb.ToString();
+ }
+
+ public static SecureString ReadSecureString(string prompt)
+ {
+ const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
+ SecureString securePwd = new SecureString();
+ ConsoleKeyInfo key;
+
+ if (!string.IsNullOrEmpty(prompt))
+ {
+ Console.Write(prompt + ": ");
+ }
+
+ ReadingPassword = true;
+ Console.ForegroundColor = ConsoleColor.Yellow;
+
+ do
+ {
+ key = Console.ReadKey(true);
+ if (t.IndexOf(key.KeyChar) != -1)
+ {
+ securePwd.AppendChar(key.KeyChar);
+ Console.Write('*');
+ }
+ else if (key.Key == ConsoleKey.Backspace && securePwd.Length > 0)
+ {
+ securePwd.RemoveAt(securePwd.Length - 1);
+ Console.Write(key.KeyChar);
+ Console.Write(' ');
+ Console.Write(key.KeyChar);
+ }
+ } while (key.Key != ConsoleKey.Enter);
+
+ Console.ForegroundColor = ConsoleColor.White;
+ ReadingPassword = false;
+ Console.WriteLine();
+ securePwd.MakeReadOnly();
+ return securePwd;
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleServiceBase.cs b/src/Neo.ConsoleService/ConsoleServiceBase.cs
new file mode 100644
index 0000000000..1da87354fb
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleServiceBase.cs
@@ -0,0 +1,562 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// ConsoleServiceBase.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.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.ServiceProcess;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Neo.ConsoleService
+{
+ public abstract class ConsoleServiceBase
+ {
+ protected virtual string? Depends => null;
+ protected virtual string Prompt => "service";
+
+ public abstract string ServiceName { get; }
+
+ protected bool ShowPrompt { get; set; } = true;
+
+ 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 bool OnCommand(string commandLine)
+ {
+ if (string.IsNullOrEmpty(commandLine))
+ {
+ return true;
+ }
+
+ string? possibleHelp = null;
+ var commandArgs = CommandToken.Parse(commandLine).ToArray();
+ var availableCommands = new List<(ConsoleCommandMethod Command, object?[] Arguments)>();
+
+ foreach (var entries in _verbs.Values)
+ {
+ foreach (var command in entries)
+ {
+ if (command.IsThisCommand(commandArgs, out var consumedArgs))
+ {
+ var arguments = new List