diff --git a/neo.sln b/neo.sln
index 827bddc1b2b..2168c241b3a 100644
--- a/neo.sln
+++ b/neo.sln
@@ -24,6 +24,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.VM", "src\Neo.VM\Neo.VM
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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.ConsoleService", "src\Neo.ConsoleService\Neo.ConsoleService.csproj", "{9E886812-7243-48D8-BEAF-47AADC11C054}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.GUI", "src\Neo.GUI\Neo.GUI.csproj", "{02ABDE42-9880-43B4-B6F7-8D618602A277}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.CLI", "src\Neo.CLI\Neo.CLI.csproj", "{BDFBE455-4C1F-4FC4-B5FC-1387B93A8687}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.ConsoleService.Tests", "tests\Neo.ConsoleService.Tests\Neo.ConsoleService.Tests.csproj", "{B40F8584-5AFB-452C-AEFA-009C80CC23A9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -62,6 +70,22 @@ Global
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -75,6 +99,10 @@ Global
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}
diff --git a/src/Neo.CLI/CLI/ConsolePercent.cs b/src/Neo.CLI/CLI/ConsolePercent.cs
new file mode 100644
index 00000000000..09077f25033
--- /dev/null
+++ b/src/Neo.CLI/CLI/ConsolePercent.cs
@@ -0,0 +1,145 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 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 00000000000..d0bc4e8d34e
--- /dev/null
+++ b/src/Neo.CLI/CLI/Helper.cs
@@ -0,0 +1,41 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 Neo.SmartContract.Manifest;
+
+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 00000000000..d3636887013
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Blockchain.cs
@@ -0,0 +1,45 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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;
+
+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);
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Contracts.cs b/src/Neo.CLI/CLI/MainService.Contracts.cs
new file mode 100644
index 00000000000..77b4a77219d
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Contracts.cs
@@ -0,0 +1,182 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 (!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 (!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() && 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 (!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 00000000000..6624f931a65
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Logger.cs
@@ -0,0 +1,171 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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));
+ }
+ messages.Add(parts.LastOrDefault());
+ }
+
+ 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 00000000000..34de5e58e5a
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.NEP17.cs
@@ -0,0 +1,140 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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.Collections.Generic;
+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 (!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 00000000000..189168b73ec
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Native.cs
@@ -0,0 +1,29 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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;
+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 00000000000..b09bd5aeda9
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Network.cs
@@ -0,0 +1,164 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 00000000000..9752dfd5eab
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Node.cs
@@ -0,0 +1,118 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 00000000000..6373c9c4a78
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Plugins.cs
@@ -0,0 +1,244 @@
+// Copyright (C) 2016-2023 The Neo Project.
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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.ConsoleService;
+using Neo.Json;
+using Neo.Plugins;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+
+namespace Neo.CLI
+{
+ partial class MainService
+ {
+ ///
+ /// Process "install" command
+ ///
+ /// Plugin name
+ [ConsoleCommand("install", Category = "Plugin Commands")]
+ private async Task OnInstallCommandAsync(string pluginName)
+ {
+ if (PluginExists(pluginName))
+ {
+ ConsoleHelper.Warning($"Plugin already exist.");
+ return;
+ }
+
+ await InstallPluginAsync(pluginName);
+ ConsoleHelper.Warning("Install successful, please restart neo-cli.");
+ }
+
+ ///
+ /// 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 async Task OnReinstallCommand(string pluginName)
+ {
+ await InstallPluginAsync(pluginName, overWrite: true);
+ ConsoleHelper.Warning("Reinstall successful, please restart neo-cli.");
+ }
+
+ ///
+ /// 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 async Task DownloadPluginAsync(string pluginName)
+ {
+ var url =
+ $"https://github.com/neo-project/neo-modules/releases/download/v{typeof(Plugin).Assembly.GetVersion()}/{pluginName}.zip";
+ using HttpClient http = new();
+ HttpResponseMessage response = await http.GetAsync(url);
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ response.Dispose();
+ Version versionCore = typeof(Plugin).Assembly.GetName().Version;
+ HttpRequestMessage request = new(HttpMethod.Get,
+ "https://api.github.com/repos/neo-project/neo-modules/releases");
+ request.Headers.UserAgent.ParseAdd(
+ $"{GetType().Assembly.GetName().Name}/{GetType().Assembly.GetVersion()}");
+ using HttpResponseMessage responseApi = await http.SendAsync(request);
+ byte[] buffer = await responseApi.Content.ReadAsByteArrayAsync();
+ var releases = JObject.Parse(buffer);
+ var asset = ((JArray)releases)
+ .Where(p => !p["tag_name"].GetString().Contains('-'))
+ .Select(p => new
+ {
+ Version = Version.Parse(p["tag_name"].GetString().TrimStart('v')),
+ Assets = (JArray)p["assets"]
+ })
+ .OrderByDescending(p => p.Version)
+ .First(p => p.Version <= versionCore).Assets
+ .FirstOrDefault(p => p["name"].GetString() == $"{pluginName}.zip");
+ if (asset is null) throw new Exception("Plugin doesn't exist.");
+ response = await http.GetAsync(asset["browser_download_url"].GetString());
+ }
+
+ using (response)
+ {
+ var totalRead = 0L;
+ byte[] buffer = new byte[1024];
+ int read;
+ await using Stream stream = await response.Content.ReadAsStreamAsync();
+ ConsoleHelper.Info("From ", $"{url}");
+ var output = new MemoryStream();
+ while ((read = await stream.ReadAsync(buffer)) > 0)
+ {
+ output.Write(buffer, 0, read);
+ totalRead += read;
+ Console.Write(
+ $"\rDownloading {pluginName}.zip {totalRead / 1024}KB/{response.Content.Headers.ContentLength / 1024}KB {(totalRead * 100) / response.Content.Headers.ContentLength}%");
+ }
+
+ Console.WriteLine();
+ return output;
+ }
+ }
+
+ ///
+ /// Install plugin from stream
+ ///
+ /// name of the plugin
+ /// 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;
+ if (!overWrite && PluginExists(pluginName)) return;
+
+ await using MemoryStream stream = await DownloadPluginAsync(pluginName);
+ using (SHA256 sha256 = SHA256.Create())
+ {
+ ConsoleHelper.Info("SHA256: ", $"{sha256.ComputeHash(stream.ToArray()).ToHexString()}");
+ }
+
+ using ZipArchive zip = new(stream, ZipArchiveMode.Read);
+ ZipArchiveEntry entry = zip.Entries.FirstOrDefault(p => p.Name == "config.json");
+ if (entry is not null)
+ {
+ await using Stream es = entry.Open();
+ await InstallDependenciesAsync(es, installed);
+ }
+ zip.ExtractToDirectory("./", true);
+ }
+
+ ///
+ /// Install the dependency of the plugin
+ ///
+ /// plugin config path in temp
+ /// Dependency set
+ private async Task InstallDependenciesAsync(Stream config, HashSet installed)
+ {
+ IConfigurationSection 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 (string plugin in dependencies.Where(p => !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.Warning("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(d => d.Get())
+ .Any(v => v.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)))
+ {
+ ConsoleHelper.Error(
+ $"Can not uninstall. Other plugins depend on this plugin, try `reinstall {pluginName}` if the plugin is broken.");
+ 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()
+ {
+ if (Plugin.Plugins.Count > 0)
+ {
+ Console.WriteLine("Loaded plugins:");
+ foreach (Plugin plugin in Plugin.Plugins)
+ {
+ var name = $"{plugin.Name}@{plugin.Version}";
+ Console.WriteLine($"\t{name,-25}{plugin.Description}");
+ }
+ }
+ else
+ {
+ ConsoleHelper.Warning("No loaded plugins");
+ }
+ }
+ }
+}
diff --git a/src/Neo.CLI/CLI/MainService.Tools.cs b/src/Neo.CLI/CLI/MainService.Tools.cs
new file mode 100644
index 00000000000..01366177296
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Tools.cs
@@ -0,0 +1,462 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 00000000000..aa23562eb90
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Vote.cs
@@ -0,0 +1,239 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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.Linq;
+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())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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;
+
+ 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;
+ 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 00000000000..f916ea1bbca
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.Wallet.cs
@@ -0,0 +1,743 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 = 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 = 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 (!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 (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 = 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 = ReadUserInput("password", true);
+ if (password.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ string password2 = 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 (!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 (!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 = 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("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 (!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)
+ {
+ 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 (!NoWallet() && 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 Transaction
+ {
+ 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 = 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 (!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 = ReadUserInput("password", true);
+ if (oldPassword.Length == 0)
+ {
+ ConsoleHelper.Info("Cancelled");
+ return;
+ }
+ if (!CurrentWallet.VerifyPassword(oldPassword))
+ {
+ ConsoleHelper.Error("Incorrect password");
+ return;
+ }
+ string newPassword = ReadUserInput("New password", true);
+ string newPasswordReEntered = 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)
+ {
+ 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 00000000000..7beea393100
--- /dev/null
+++ b/src/Neo.CLI/CLI/MainService.cs
@@ -0,0 +1,609 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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.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 Array = System.Array;
+
+namespace Neo.CLI
+{
+ public partial class MainService : ConsoleServiceBase, IWalletProvider
+ {
+ public event EventHandler WalletChanged;
+
+ public const long TestModeGas = 20_00000000;
+
+ private Wallet _currentWallet;
+ public LocalNode LocalNode;
+
+ 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;
+ }
+
+ 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 static UInt160 StringToAddress(string input, byte version)
+ {
+ switch (input.ToLowerInvariant())
+ {
+ case "neo": return NativeContract.NEO.Hash;
+ case "gas": return NativeContract.GAS.Hash;
+ }
+
+ // 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 FileStream(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 FileStream(pathAccZip, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read))
+ using (Stream zs = zip.GetEntry(pathAcc).Open())
+ 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 FileStream(path.FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read))
+ using (Stream zs = zip.GetEntry(Path.GetFileNameWithoutExtension(path.FileName)).Open())
+ foreach (var block in GetBlocks(zs, true))
+ yield return block;
+ else
+ using (FileStream fs = new FileStream(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 void OnStart(string[] args)
+ {
+ base.OnStart(args);
+ Start(args);
+ }
+
+ 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(string[] args)
+ {
+ if (NeoSystem != null) return;
+ bool verifyImport = true;
+ for (int i = 0; i < args.Length; i++)
+ switch (args[i])
+ {
+ case "/noverify":
+ case "--noverify":
+ verifyImport = false;
+ break;
+ }
+
+ ProtocolSettings protocol = ProtocolSettings.Load("config.json");
+
+ 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;
+
+ 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),
+ WebSocket = new IPEndPoint(IPAddress.Any, Settings.Default.P2P.WsPort),
+ MinDesiredConnections = Settings.Default.P2P.MinDesiredConnections,
+ MaxConnections = Settings.Default.P2P.MaxConnections,
+ MaxConnectionsPerAddress = Settings.Default.P2P.MaxConnectionsPerAddress
+ });
+
+ if (Settings.Default.UnlockWallet.IsActive)
+ {
+ try
+ {
+ 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)
+ {
+ 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 (!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 List();
+
+ if (contractParameters != null)
+ {
+ foreach (var contractParameter in contractParameters)
+ {
+ 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 ? 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;
+ }
+ }
+}
diff --git a/src/Neo.CLI/Dockerfile b/src/Neo.CLI/Dockerfile
new file mode 100644
index 00000000000..7b67a6812d2
--- /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 00000000000..d3dd1aa5ed4
--- /dev/null
+++ b/src/Neo.CLI/Extensions.cs
@@ -0,0 +1,28 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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);
+ 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 00000000000..19f92a92cc4
--- /dev/null
+++ b/src/Neo.CLI/Neo.CLI.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Neo.CLI
+ neo-cli
+ Exe
+ Neo.CLI
+ Neo
+ Neo.CLI
+ neo.ico
+
+
+
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
+
+
+
+
+
diff --git a/src/Neo.CLI/Program.cs b/src/Neo.CLI/Program.cs
new file mode 100644
index 00000000000..8a12b7dddb2
--- /dev/null
+++ b/src/Neo.CLI/Program.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 00000000000..ace4cb48638
--- /dev/null
+++ b/src/Neo.CLI/Settings.cs
@@ -0,0 +1,120 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The neo-cli is free software distributed under the MIT software
+// license, see the accompanying file LICENSE in the main directory of
+// the project 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 System.Threading;
+
+namespace Neo
+{
+ public class Settings
+ {
+ public LoggerSettings Logger { get; }
+ public StorageSettings Storage { get; }
+ public P2PSettings P2P { get; }
+ public UnlockWalletSettings UnlockWallet { get; }
+
+ static Settings _default;
+
+ static bool UpdateDefault(IConfiguration configuration)
+ {
+ var settings = new Settings(configuration.GetSection("ApplicationConfiguration"));
+ return null == Interlocked.CompareExchange(ref _default, settings, null);
+ }
+
+ public static bool Initialize(IConfiguration configuration)
+ {
+ return UpdateDefault(configuration);
+ }
+
+ public static Settings Default
+ {
+ get
+ {
+ if (_default == null)
+ {
+ IConfigurationRoot config = new ConfigurationBuilder().AddJsonFile("config.json", optional: true).Build();
+ Initialize(config);
+ }
+
+ return _default;
+ }
+ }
+
+ public Settings(IConfigurationSection section)
+ {
+ this.Logger = new LoggerSettings(section.GetSection("Logger"));
+ this.Storage = new StorageSettings(section.GetSection("Storage"));
+ this.P2P = new P2PSettings(section.GetSection("P2P"));
+ this.UnlockWallet = new UnlockWalletSettings(section.GetSection("UnlockWallet"));
+ }
+ }
+
+ public class LoggerSettings
+ {
+ public string Path { get; }
+ public bool ConsoleOutput { get; }
+ public bool Active { get; }
+
+ public LoggerSettings(IConfigurationSection section)
+ {
+ this.Path = section.GetValue("Path", "Logs");
+ this.ConsoleOutput = section.GetValue("ConsoleOutput", false);
+ this.Active = section.GetValue("Active", false);
+ }
+ }
+
+ public class StorageSettings
+ {
+ public string Engine { get; }
+ public string Path { get; }
+
+ public StorageSettings(IConfigurationSection section)
+ {
+ this.Engine = section.GetValue("Engine", "LevelDBStore");
+ this.Path = section.GetValue("Path", "Data_LevelDB_{0}");
+ }
+ }
+
+ public class P2PSettings
+ {
+ public ushort Port { get; }
+ public ushort WsPort { get; }
+ public int MinDesiredConnections { get; }
+ public int MaxConnections { get; }
+ public int MaxConnectionsPerAddress { get; }
+
+ public P2PSettings(IConfigurationSection section)
+ {
+ this.Port = ushort.Parse(section.GetValue("Port", "10333"));
+ this.WsPort = ushort.Parse(section.GetValue("WsPort", "10334"));
+ this.MinDesiredConnections = section.GetValue("MinDesiredConnections", Peer.DefaultMinDesiredConnections);
+ this.MaxConnections = section.GetValue("MaxConnections", Peer.DefaultMaxConnections);
+ this.MaxConnectionsPerAddress = section.GetValue("MaxConnectionsPerAddress", 3);
+ }
+ }
+
+ public class UnlockWalletSettings
+ {
+ public string Path { get; }
+ public string Password { get; }
+ public bool IsActive { get; }
+
+ public UnlockWalletSettings(IConfigurationSection section)
+ {
+ if (section.Exists())
+ {
+ this.Path = section.GetValue("Path", "");
+ this.Password = section.GetValue("Password", "");
+ this.IsActive = bool.Parse(section.GetValue("IsActive", "false"));
+ }
+ }
+ }
+}
diff --git a/src/Neo.CLI/config.fs.mainnet.json b/src/Neo.CLI/config.fs.mainnet.json
new file mode 100644
index 00000000000..1e1b5fc92da
--- /dev/null
+++ b/src/Neo.CLI/config.fs.mainnet.json
@@ -0,0 +1,53 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore",
+ "Path": "Data_LevelDB_{0}"
+ },
+ "P2P": {
+ "Port": 40333,
+ "WsPort": 40334,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ }
+ },
+ "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 00000000000..5e827a84801
--- /dev/null
+++ b/src/Neo.CLI/config.fs.testnet.json
@@ -0,0 +1,53 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore",
+ "Path": "Data_LevelDB_{0}"
+ },
+ "P2P": {
+ "Port": 50333,
+ "WsPort": 50334,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ }
+ },
+ "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 00000000000..6c9423ed05c
--- /dev/null
+++ b/src/Neo.CLI/config.json
@@ -0,0 +1,69 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore",
+ "Path": "Data_LevelDB_{0}"
+ },
+ "P2P": {
+ "Port": 10333,
+ "WsPort": 10334,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ }
+ },
+ "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 00000000000..6c9423ed05c
--- /dev/null
+++ b/src/Neo.CLI/config.mainnet.json
@@ -0,0 +1,69 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore",
+ "Path": "Data_LevelDB_{0}"
+ },
+ "P2P": {
+ "Port": 10333,
+ "WsPort": 10334,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ }
+ },
+ "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 00000000000..1d118016a3a
--- /dev/null
+++ b/src/Neo.CLI/config.testnet.json
@@ -0,0 +1,69 @@
+{
+ "ApplicationConfiguration": {
+ "Logger": {
+ "Path": "Logs",
+ "ConsoleOutput": false,
+ "Active": false
+ },
+ "Storage": {
+ "Engine": "LevelDBStore",
+ "Path": "Data_LevelDB_{0}"
+ },
+ "P2P": {
+ "Port": 20333,
+ "WsPort": 20334,
+ "MinDesiredConnections": 10,
+ "MaxConnections": 40,
+ "MaxConnectionsPerAddress": 3
+ },
+ "UnlockWallet": {
+ "Path": "",
+ "Password": "",
+ "IsActive": false
+ }
+ },
+ "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 00000000000..60786c424d8
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 00000000000..497a956a7fe
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandQuoteToken.cs
@@ -0,0 +1,53 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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 00000000000..9a59f144106
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandSpaceToken.cs
@@ -0,0 +1,64 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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 00000000000..c22d9899961
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandStringToken.cs
@@ -0,0 +1,90 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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 00000000000..a8b8a47fd60
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandToken.cs
@@ -0,0 +1,225 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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; }
+
+ ///
+ /// 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);
+
+ 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 00000000000..828ea34b4e9
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandTokenType.cs
@@ -0,0 +1,19 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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 00000000000..465ab39f984
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleColorSet.cs
@@ -0,0 +1,51 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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 00000000000..b880c03becb
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleCommandAttribute.cs
@@ -0,0 +1,45 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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; }
+
+ ///
+ /// Description
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// 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 00000000000..55176ecbc3c
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
@@ -0,0 +1,120 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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 00000000000..fdf6f180cda
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleHelper.cs
@@ -0,0 +1,65 @@
+using System;
+
+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);
+
+ ///
+ /// 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);
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleServiceBase.cs b/src/Neo.ConsoleService/ConsoleServiceBase.cs
new file mode 100644
index 00000000000..ae55b5fd768
--- /dev/null
+++ b/src/Neo.ConsoleService/ConsoleServiceBase.cs
@@ -0,0 +1,627 @@
+// Copyright (C) 2016-2023 The Neo Project.
+//
+// The Neo.ConsoleService is free software distributed under the MIT
+// software license, see the accompanying file LICENSE in the main directory
+// of the project 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.Security;
+using System.ServiceProcess;
+using System.Text;
+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;
+ public bool ReadingPassword { get; set; } = false;
+
+ private bool _running;
+ private readonly CancellationTokenSource _shutdownTokenSource = new();
+ private readonly CountdownEvent _shutdownAcknowledged = new(1);
+ private readonly Dictionary> _verbs = new();
+ private readonly Dictionary _instances = new();
+ private readonly Dictionary, bool, object>> _handlers = new();
+
+ private 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