diff --git a/src/AutomatedFlowTest/AutomatedFlowTest.csproj b/src/AutomatedFlowTest/AutomatedFlowTest.csproj
new file mode 100644
index 0000000..6f505e5
--- /dev/null
+++ b/src/AutomatedFlowTest/AutomatedFlowTest.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net6.0-windows
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/AutomatedFlowTest/AutomatedStateHandler.cs b/src/AutomatedFlowTest/AutomatedStateHandler.cs
new file mode 100644
index 0000000..cbdc6f1
--- /dev/null
+++ b/src/AutomatedFlowTest/AutomatedStateHandler.cs
@@ -0,0 +1,296 @@
+using MasternodeSetupTool;
+
+using NodeType = MasternodeSetupTool.NodeType;
+
+public class AutomatedStateHandler : IStateHandler
+{
+ private Configuration configuration;
+ private ILogger logger;
+
+ public AutomatedStateHandler(Configuration configuration, ILogger logger)
+ {
+ this.configuration = configuration;
+ this.logger = logger;
+ }
+
+ public void Error(string message)
+ {
+ this.logger.Log($"Error: {message}");
+ }
+
+ public void Error(Exception exception)
+ {
+ this.logger.Log($"Error: {exception}");
+ }
+
+ public void Error(string message, Exception exception)
+ {
+ this.logger.Log($"Error: {message}");
+ this.logger.Log($"Error: {exception}");
+ }
+
+ public void Info(string message, string? updateTag = null)
+ {
+ this.logger.Log($"Info: {message}");
+ }
+
+ public async Task OnAlreadyMember()
+ {
+ this.logger.Log("This node is already a member");
+ }
+
+ public async Task OnAskCreatePassword(NodeType nodeType)
+ {
+ this.logger.Log($"Asked to create a password for {nodeType} wallet");
+
+ if (nodeType == NodeType.MainChain)
+ {
+ return this.configuration.collateralWalletPassword;
+ }
+ else
+ {
+ return this.configuration.miningWalletPassword;
+ }
+ }
+
+ public async Task OnAskForEULA()
+ {
+ this.logger.Log($"Asked for EULA");
+ return this.configuration.confirmEULA;
+ }
+
+ public async Task OnAskForMnemonicConfirmation(NodeType nodeType, string mnemonic)
+ {
+ this.logger.Log($"Asked for mnemonic confirmation");
+ this.logger.Log($"Mnemonic: {mnemonic}");
+ return this.configuration.confirmMnemonic;
+ }
+
+ public async Task OnAskForNewFederationKey()
+ {
+ this.logger.Log($"Asked for new federation key");
+ return this.configuration.confirmNewFederationKey;
+ }
+
+ public async Task OnAskForPassphrase(NodeType nodeType)
+ {
+ this.logger.Log($"Asked to create a passphrase for {nodeType} wallet");
+
+ if (nodeType == NodeType.MainChain)
+ {
+ return this.configuration.collateralWalletPassphrase;
+ }
+ else
+ {
+ return this.configuration.miningWalletPassphrase;
+ }
+ }
+
+ public async Task OnAskForUserMnemonic(NodeType nodeType)
+ {
+ this.logger.Log($"Asked a mnemonic for {nodeType} wallet");
+
+ if (nodeType == NodeType.MainChain)
+ {
+ return this.configuration.collateralWalletMnemonic;
+ }
+ else
+ {
+ return this.configuration.miningWalletMnemonic;
+ }
+ }
+
+ public async Task OnAskForWalletName(NodeType nodeType, bool newWallet)
+ {
+ this.logger.Log($"Asked a wallet name for {nodeType} wallet");
+
+ if (nodeType == NodeType.MainChain)
+ {
+ return this.configuration.collateralWalletName;
+ }
+ else
+ {
+ return this.configuration.miningWalletName;
+ }
+ }
+
+ public async Task OnAskForWalletPassword(NodeType nodeType)
+ {
+ this.logger.Log($"Asked a password for {nodeType} wallet");
+
+ if (nodeType == NodeType.MainChain)
+ {
+ return this.configuration.collateralWalletPassword;
+ }
+ else
+ {
+ return this.configuration.miningWalletPassword;
+ }
+ }
+
+ public async Task OnAskForWalletSource(NodeType nodeType)
+ {
+ this.logger.Log($"Asked a wallet source for {nodeType} wallet");
+
+ if (nodeType == NodeType.MainChain)
+ {
+ return this.configuration.collateralWalletSource;
+ }
+ else
+ {
+ return this.configuration.miningWalletSource;
+ }
+ }
+
+ public async Task OnAskReenterPassword(NodeType nodeType)
+ {
+ this.logger.Log($"Asked to reenter password");
+
+ return this.configuration.confirmReenterPassword;
+ }
+
+ public async Task OnAskToRunIfAlreadyMember()
+ {
+ this.logger.Log($"Already a member, stopping");
+ return this.configuration.confirmRunIfAlreadyMember;
+ }
+
+ public async Task OnChooseAddress(List addresses, NodeType nodeType)
+ {
+ string? address = (nodeType == NodeType.MainChain
+ ? this.configuration.collateralWalletAddress
+ : this.configuration.miningWalletAddress)
+ ?? addresses.FirstOrDefault().Address;
+
+ this.logger.Log($"Choosing address {address}");
+ return address;
+ }
+
+ public async Task OnChooseWallet(List wallets, NodeType nodeType)
+ {
+ string? walletName = (nodeType == NodeType.MainChain
+ ? this.configuration.collateralWalletName
+ : this.configuration.miningWalletName)
+ ?? wallets.FirstOrDefault().Name;
+
+ this.logger.Log($"Choosing {nodeType} wallet {walletName}");
+ return walletName;
+ }
+
+ public async Task OnCreateWalletFailed(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet creation failed");
+ }
+
+ public async Task OnFederationKeyMissing()
+ {
+ this.logger.Log($"Missing federation key");
+ }
+
+ public async Task OnMissingRegistrationFee(string address)
+ {
+ this.logger.Log($"Missing registration fee on address: {address}");
+ }
+
+ public async Task OnMnemonicExists(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet mnemonic already exists");
+ }
+
+ public async Task OnMnemonicIsInvalid(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet mnemonic is invalid");
+ }
+
+ public async Task OnNodeFailedToStart(NodeType nodeType, string? reason = null)
+ {
+ this.logger.Log($"{nodeType} node failed to start");
+ this.logger.Log($"Reason: {reason}");
+ }
+
+ public async Task OnProgramVersionAvailable(string? version)
+ {
+ this.logger.Log($"App version: {version ?? "null"}");
+ }
+
+ public async Task OnRegistrationCanceled()
+ {
+ this.logger.Log($"Registration canceled");
+ }
+
+ public async Task OnRegistrationComplete()
+ {
+ this.logger.Log($"Registration complete");
+ }
+
+ public async Task OnRegistrationFailed()
+ {
+ this.logger.Log($"Registration failed");
+ }
+
+ public async Task OnRestoreWalletFailed(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet restore failed");
+ }
+
+ public async Task OnResyncFailed(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet resync failed");
+ }
+
+ public async Task OnShowNewFederationKey(string pubKey, string savePath)
+ {
+ this.logger.Log($"New pubKey is: {pubKey}");
+ this.logger.Log($"New savePath is: {savePath}");
+ }
+
+ public async Task OnShowWalletAddress(NodeType nodeType, string address)
+ {
+ this.logger.Log($"{nodeType} wallet address is {address}");
+ }
+
+ public async Task OnShowWalletName(NodeType nodeType, string walletName)
+ {
+ this.logger.Log($"{nodeType} wallet name is {walletName}");
+ }
+
+ public async Task OnStart()
+ {
+ this.logger.Log($"Started");
+ }
+
+ public async Task OnWaitingForCollateral()
+ {
+ this.logger.Log($"Waiting for collateral");
+ }
+
+ public async Task OnWaitingForRegistrationFee()
+ {
+ this.logger.Log($"Waiting for registration fee");
+ }
+
+ public async Task OnWalletExistsOrInvalid(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet exists or invalid");
+ }
+
+ public async Task OnWalletNameExists(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet name already exists");
+ }
+
+ public async Task OnWalletSynced(NodeType nodeType)
+ {
+ this.logger.Log($"{nodeType} wallet synced");
+ }
+
+ public async Task OnWalletSyncing(NodeType nodeType, int progress)
+ {
+ this.logger.Log($"{nodeType} wallet is syncing, {progress}%");
+ }
+
+ public interface ILogger
+ {
+ void Log(string message);
+ }
+}
diff --git a/src/AutomatedFlowTest/CompositeLogger.cs b/src/AutomatedFlowTest/CompositeLogger.cs
new file mode 100644
index 0000000..4c9cb56
--- /dev/null
+++ b/src/AutomatedFlowTest/CompositeLogger.cs
@@ -0,0 +1,20 @@
+namespace AutomatedFlowTest
+{
+ public class CompositeLogger: AutomatedStateHandler.ILogger
+ {
+ private IEnumerable Loggers;
+
+ public CompositeLogger(IEnumerable loggers)
+ {
+ this.Loggers = loggers;
+ }
+
+ public void Log(string message)
+ {
+ foreach (AutomatedStateHandler.ILogger logger in this.Loggers)
+ {
+ logger.Log(message);
+ }
+ }
+ }
+}
diff --git a/src/AutomatedFlowTest/Configuration.cs b/src/AutomatedFlowTest/Configuration.cs
new file mode 100644
index 0000000..05b345e
--- /dev/null
+++ b/src/AutomatedFlowTest/Configuration.cs
@@ -0,0 +1,38 @@
+using MasternodeSetupTool;
+using NBitcoin;
+
+public class Configuration
+{
+ public FlowType flowType = FlowType.SetupNode;
+
+ public bool writeConsoleLog = true;
+ public string? logFilePath = null;
+
+ public NetworkType networkType = NetworkType.Testnet;
+
+ public WalletSource collateralWalletSource = WalletSource.UseExistingWallet;
+ public WalletSource miningWalletSource = WalletSource.UseExistingWallet;
+
+ public bool confirmEULA = true;
+ public bool confirmNewFederationKey = true;
+ public bool confirmRunIfAlreadyMember = true;
+ public bool confirmMnemonic = true;
+ public bool confirmReenterPassword = true;
+
+ public string? collateralWalletName = "TestWallet";
+ public string? miningWalletName = "TestWallet";
+ public string collateralWalletPassword = "12345";
+ public string miningWalletPassword = "12345";
+ public string collateralWalletPassphrase = "";
+ public string miningWalletPassphrase = "";
+ public string collateralWalletMnemonic = "";
+ public string miningWalletMnemonic = "";
+ public string? collateralWalletAddress = null;
+ public string? miningWalletAddress = null;
+}
+
+
+public enum FlowType
+{
+ RunNode, SetupNode
+}
diff --git a/src/AutomatedFlowTest/ConsoleLogger.cs b/src/AutomatedFlowTest/ConsoleLogger.cs
new file mode 100644
index 0000000..354b2e0
--- /dev/null
+++ b/src/AutomatedFlowTest/ConsoleLogger.cs
@@ -0,0 +1,10 @@
+namespace AutomatedFlowTest
+{
+ public class ConsoleLogger : AutomatedStateHandler.ILogger
+ {
+ public void Log(string message)
+ {
+ Console.WriteLine(message);
+ }
+ }
+}
diff --git a/src/AutomatedFlowTest/FileLogger.cs b/src/AutomatedFlowTest/FileLogger.cs
new file mode 100644
index 0000000..45902ef
--- /dev/null
+++ b/src/AutomatedFlowTest/FileLogger.cs
@@ -0,0 +1,34 @@
+namespace AutomatedFlowTest
+{
+ public class FileLogger : AutomatedStateHandler.ILogger
+ {
+ private readonly string FilePath;
+
+ public FileLogger(string filePath)
+ {
+ this.FilePath = filePath;
+
+ // Test if we can access the file or directory
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(this.FilePath)); // Ensure directory exists
+ File.AppendAllText(this.FilePath, ""); // Try accessing the file
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to initialize the logger with file path: {this.FilePath}.", ex);
+ }
+ }
+
+ public void Log(string message)
+ {
+ try
+ {
+ File.AppendAllText(this.FilePath, message + "\n");
+ }
+ catch
+ {
+ }
+ }
+ }
+}
diff --git a/src/AutomatedFlowTest/Program.cs b/src/AutomatedFlowTest/Program.cs
new file mode 100644
index 0000000..840cf8a
--- /dev/null
+++ b/src/AutomatedFlowTest/Program.cs
@@ -0,0 +1,94 @@
+using AutomatedFlowTest;
+using MasternodeSetupTool;
+using Newtonsoft.Json;
+
+class Program
+{
+ static void Main(string[] args)
+ {
+ Configuration configuration = TryGetConfigurationFromParams(args) ?? new Configuration();
+
+ IStateHandler stateHandler = new AutomatedStateHandler(configuration, BuildLogger(configuration));
+ IStateHolder stateHolder = new DefaultStateHolder(repeatOnEndState: false);
+
+ var stateMachine = new StateMachine(
+ networkType: configuration.networkType,
+ stateHandler: stateHandler,
+ stateHolder: stateHolder);
+
+ Task.Run(async () =>
+ {
+ switch (configuration.flowType)
+ {
+ case FlowType.RunNode:
+ stateMachine.OnRunNode();
+ break;
+ case FlowType.SetupNode:
+ stateMachine.OnSetupNode();
+ break;
+ }
+
+ while (true)
+ {
+ if (stateHolder.CurrentState == StateMachine.State.End)
+ {
+ return;
+ }
+
+ await stateMachine.TickAsync();
+ await Task.Delay(TimeSpan.FromMilliseconds(200));
+ }
+ }).Wait();
+ }
+
+ private static Configuration? TryGetConfigurationFromParams(string[] args)
+ {
+ if (args.Length != 0)
+ {
+ string? configArg = args.FirstOrDefault(a => a.Contains("-config"));
+ if (configArg != null)
+ {
+ string[] parts = configArg.Split('=');
+ if (parts.Length == 2)
+ {
+ string? filePath = parts[1];
+ if (!string.IsNullOrEmpty(filePath))
+ {
+ try
+ {
+ string jsonConfig = File.ReadAllText(filePath);
+ Console.WriteLine($"Using configuration from {filePath}");
+ Console.WriteLine(jsonConfig);
+ return JsonConvert.DeserializeObject(jsonConfig);
+ } catch
+ {
+
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static AutomatedStateHandler.ILogger BuildLogger(Configuration configuration)
+ {
+ List loggers = new List();
+
+ if (configuration != null)
+ {
+ if (configuration.writeConsoleLog)
+ {
+ loggers.Add(new ConsoleLogger());
+ }
+
+ if (!string.IsNullOrEmpty(configuration.logFilePath))
+ {
+ loggers.Add(new FileLogger(configuration.logFilePath));
+ }
+ }
+
+ return new CompositeLogger(loggers);
+ }
+}
\ No newline at end of file
diff --git a/src/AutomatedFlowTest/example.json b/src/AutomatedFlowTest/example.json
new file mode 100644
index 0000000..4ec415d
--- /dev/null
+++ b/src/AutomatedFlowTest/example.json
@@ -0,0 +1,23 @@
+{
+ "flowType": "SetupNode",
+ "writeConsoleLog": true,
+ "logFilePath": null,
+ "networkType": "Testnet",
+ "collateralWalletSource": "UseExistingWallet",
+ "miningWalletSource": "UseExistingWallet",
+ "confirmEULA": true,
+ "confirmNewFederationKey": false,
+ "confirmRunIfAlreadyMember": true,
+ "confirmMnemonic": true,
+ "confirmReenterPassword": true,
+ "collateralWalletName": "TestWallet",
+ "miningWalletName": "TestWallet",
+ "collateralWalletPassword": "12345",
+ "miningWalletPassword": "12345",
+ "collateralWalletPassphrase": "",
+ "miningWalletPassphrase": "",
+ "collateralWalletMnemonic": "",
+ "miningWalletMnemonic": "",
+ "collateralWalletAddress": null,
+ "miningWalletAddress": null
+}
diff --git a/src/MasternodeSetupTool/AddressItem.cs b/src/Core/AddressItem.cs
similarity index 100%
rename from src/MasternodeSetupTool/AddressItem.cs
rename to src/Core/AddressItem.cs
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
new file mode 100644
index 0000000..cd782d7
--- /dev/null
+++ b/src/Core/Core.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net6.0-windows
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Core/CoreLib.cs b/src/Core/CoreLib.cs
new file mode 100644
index 0000000..380ff30
--- /dev/null
+++ b/src/Core/CoreLib.cs
@@ -0,0 +1,27 @@
+using Flurl.Http;
+using Flurl.Http.Configuration;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Stratis.Bitcoin.Utilities.JsonConverters;
+
+namespace MasternodeSetupTool
+{
+ public static class CoreLib
+ {
+ public static void Initialize()
+ {
+ FlurlHttp.Configure(settings => {
+ var jsonSettings = new JsonSerializerSettings
+ {
+ Converters = new List()
+ {
+ new DateTimeToUnixTimeConverter(),
+ new IsoDateTimeConverter(),
+ }
+ };
+
+ settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/DefaultStateHolder.cs b/src/Core/DefaultStateHolder.cs
new file mode 100644
index 0000000..243c271
--- /dev/null
+++ b/src/Core/DefaultStateHolder.cs
@@ -0,0 +1,55 @@
+namespace MasternodeSetupTool
+{
+ public class DefaultStateHolder : IStateHolder
+ {
+ public StateMachine.State currentState_ = StateMachine.State.Begin;
+ public StateMachine.State? nextState_;
+
+ private bool repeatOnEndState;
+
+ public DefaultStateHolder(bool repeatOnEndState = true)
+ {
+ this.repeatOnEndState = repeatOnEndState;
+ }
+
+ public StateMachine.State CurrentState
+ {
+ get
+ {
+ return this.currentState_;
+ }
+
+ private set
+ {
+ this.currentState_ = value;
+ }
+ }
+
+ public StateMachine.State? NextState
+ {
+ get
+ {
+ return this.nextState_;
+ }
+
+ set
+ {
+ this.nextState_ = value;
+ }
+ }
+
+ public void SwitchToNextState()
+ {
+ StateMachine.State? nextState = this.NextState;
+ if (nextState != null)
+ {
+ if (nextState == StateMachine.State.End && this.repeatOnEndState)
+ {
+ nextState = StateMachine.State.Begin;
+ }
+ this.CurrentState = (StateMachine.State)nextState;
+ this.NextState = null;
+ }
+ }
+ }
+}
diff --git a/src/MasternodeSetupTool/ILogger.cs b/src/Core/ILogger.cs
similarity index 100%
rename from src/MasternodeSetupTool/ILogger.cs
rename to src/Core/ILogger.cs
diff --git a/src/Core/IRegistrationService.cs b/src/Core/IRegistrationService.cs
new file mode 100644
index 0000000..68b0dd8
--- /dev/null
+++ b/src/Core/IRegistrationService.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using NBitcoin;
+using Stratis.Bitcoin.Features.Wallet.Models;
+
+namespace MasternodeSetupTool
+{
+ public interface IRegistrationService
+ {
+ Network MainchainNetwork { get; }
+ NetworkType NetworkType { get; }
+ string PubKey { get; }
+ string RootDataDir { get; }
+ Network SidechainNetwork { get; }
+
+ Task BuildTransactionAsync(int apiPort, string walletName, string walletPassword, string accountName, List recipients, string? opReturnData = null, string? opReturnAmount = null, string feeType = "low", string? changeAddress = null, bool allowUnconfirmed = false);
+ Task CallJoinFederationRequestAsync(WalletCredentials collateralCredentials, WalletCredentials miningCredentials);
+ bool CheckFederationKeyExists();
+ Task CheckIsFederationMemberAsync();
+ Task CheckWalletBalanceAsync(int apiPort, string walletName, int amountToCheck);
+ Task CheckWalletBalanceWithConfirmationsAsync(int apiPort, string walletName, int amountToCheck, int requiredConfirmations);
+ Task CheckWalletPasswordAsync(int apiPort, string walletName, string walletPassword);
+ string CreateFederationKey();
+ void DeleteFederationKey();
+ Task EnsureBlockstoreIsSyncedAsync(NodeType nodeType, int apiPort);
+ Task EnsureMainChainNodeAddressIndexerIsSyncedAsync();
+ Task EnsureNodeIsInitializedAsync(NodeType nodeType, int apiPort);
+ Task EnsureNodeIsSyncedAsync(NodeType nodeType, int apiPort);
+ Task FindWalletByNameAsync(int apiPort, string walletName);
+ Task GetFirstWalletAddressAsync(int apiPort, string walletName);
+ Task?> GetWalletAddressesAsync(string walletName, int apiPort);
+ Task GetWalletBalanceAsync(string walletName, int apiPort);
+ Task?> GetWalletsWithBalanceAsync(int apiPort);
+ Task IsWalletSyncedAsync(int apiPort, string walletName);
+ void LaunchBrowser(string url);
+ Task MonitorJoinFederationRequestAsync();
+ Task PerformCrossChainTransferAsync(int apiPort, string walletName, string walletPassword, string amount, string cirrusAddress, string changeAddress);
+ Task RestoreWalletAsync(int apiPort, NodeType nodeType, string walletName, string mnemonic, string passphrase, string password, bool createNewWallet);
+ Task ResyncWalletAsync(int apiPort, string walletName);
+ string SendTransaction(int apiPort, string hex);
+ Task ShutdownNodeAsync(NodeType nodeType, int apiPort);
+ Task StartMasterNodeDashboardAsync();
+ Task StartNodeAsync(NodeType nodeType, int apiPort, bool reindex = false);
+ Task WalletSyncProgressAsync(int apiPort, string walletName);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/IStateHandler.cs b/src/Core/IStateHandler.cs
new file mode 100644
index 0000000..cd0b7ef
--- /dev/null
+++ b/src/Core/IStateHandler.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using MasternodeSetupTool;
+
+namespace MasternodeSetupTool;
+
+public interface IStateHandler: ILogger
+{
+ public Task OnStart();
+
+ public Task OnProgramVersionAvailable(string? version);
+
+ public Task OnFederationKeyMissing();
+
+ public Task OnNodeFailedToStart(NodeType nodeType, string? reason = null);
+
+ public Task OnAskForEULA();
+
+ public Task OnAskForNewFederationKey();
+
+ public Task OnShowNewFederationKey(string pubKey, string savePath);
+
+ public Task OnAskToRunIfAlreadyMember();
+
+ public Task OnAlreadyMember();
+
+ public Task OnAskForWalletSource(NodeType nodeType);
+
+ public Task OnChooseWallet(List wallets, NodeType nodeType);
+
+ public Task OnChooseAddress(List wallets, NodeType nodeType);
+
+ public Task OnWaitingForCollateral();
+
+ public Task OnWaitingForRegistrationFee();
+
+ public Task OnMissingRegistrationFee(string address);
+
+ public Task OnRegistrationCanceled();
+
+ public Task OnRegistrationComplete();
+ public Task OnRegistrationFailed();
+
+ public Task OnAskForMnemonicConfirmation(NodeType nodeType, string mnemonic);
+
+ public Task OnAskForUserMnemonic(NodeType nodeType);
+
+ public Task OnAskForWalletName(NodeType nodeType, bool newWallet);
+
+ public Task OnAskForPassphrase(NodeType nodeType);
+
+ public Task OnAskForWalletPassword(NodeType nodeType);
+
+ public Task OnAskCreatePassword(NodeType nodeType);
+
+ public Task OnAskReenterPassword(NodeType nodeType);
+
+ public Task OnWalletNameExists(NodeType nodeType);
+
+ public Task OnMnemonicIsInvalid(NodeType nodeType);
+
+ public Task OnMnemonicExists(NodeType nodeType);
+
+ public Task OnWalletExistsOrInvalid(NodeType nodeType);
+
+ public Task OnWalletSyncing(NodeType nodeType, int progress);
+
+ public Task OnWalletSynced(NodeType nodeType);
+
+ public Task OnShowWalletName(NodeType nodeType, string walletName);
+
+ public Task OnShowWalletAddress(NodeType nodeType, string address);
+
+ public Task OnRestoreWalletFailed(NodeType nodeType);
+ public Task OnCreateWalletFailed(NodeType nodeType);
+ public Task OnResyncFailed(NodeType nodeType);
+}
+
+public enum WalletSource
+{
+ NewWallet, RestoreWallet, UseExistingWallet
+}
\ No newline at end of file
diff --git a/src/Core/IStateHolder.cs b/src/Core/IStateHolder.cs
new file mode 100644
index 0000000..d1105ce
--- /dev/null
+++ b/src/Core/IStateHolder.cs
@@ -0,0 +1,11 @@
+using static MasternodeSetupTool.StateMachine;
+
+namespace MasternodeSetupTool;
+
+public interface IStateHolder
+{
+ State CurrentState { get; }
+ State? NextState { get; set; }
+
+ void SwitchToNextState();
+}
\ No newline at end of file
diff --git a/src/MasternodeSetupTool/RegistrationService.cs b/src/Core/RegistrationService.cs
similarity index 97%
rename from src/MasternodeSetupTool/RegistrationService.cs
rename to src/Core/RegistrationService.cs
index e214179..7b6fed9 100644
--- a/src/MasternodeSetupTool/RegistrationService.cs
+++ b/src/Core/RegistrationService.cs
@@ -1,17 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
+using System.Diagnostics;
using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
using Flurl;
using Flurl.Http;
-using ICSharpCode.Decompiler.CSharp.Syntax;
-using LiteDB;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
@@ -27,7 +17,7 @@
namespace MasternodeSetupTool
{
- public sealed class RegistrationService
+ public sealed class RegistrationService : IRegistrationService
{
public const int CollateralRequirement = 100_000;
public const int FeeRequirement = 500;
@@ -138,7 +128,7 @@ public async Task StartNodeAsync(NodeType nodeType, int apiPort, bool rein
}
if (this.networkType == NetworkType.Testnet)
- {
+ {
argumentBuilder.Append("-testnet ");
}
@@ -158,10 +148,9 @@ public async Task StartNodeAsync(NodeType nodeType, int apiPort, bool rein
{
FileName = Path.Combine(fullPath, "Stratis.CirrusMinerD.exe"),
Arguments = argumentBuilder.ToString(),
- UseShellExecute = false,
+ UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Minimized,
WorkingDirectory = fullPath,
- RedirectStandardError = true,
};
var process = Process.Start(startInfo);
@@ -170,7 +159,7 @@ public async Task StartNodeAsync(NodeType nodeType, int apiPort, bool rein
if (process == null || process.HasExited)
{
- Error($"{nodeType} node process failed to start, exiting...");
+ Error($"{nodeType} node process failed to start, exiting...");
return false;
}
@@ -205,7 +194,7 @@ public async Task ShutdownNodeAsync(NodeType nodeType, int apiPort)
.WithHeader("Content-Type", "application/json-patch+json")
.PostAsync();
}
- catch (Exception e)
+ catch (Exception e)
{
Error($"Can not shutdown {nodeType}.");
Error(e);
@@ -585,7 +574,7 @@ public async Task RestoreWalletAsync(int apiPort, NodeType nodeType, strin
return walletBalanceModel.AccountsBalances.FirstOrDefault()
?.Addresses?.Select(item => new AddressItem(item.Address, item.AmountConfirmed))
?.ToList();
- }
+ }
catch
{
return null;
@@ -605,7 +594,7 @@ public async Task GetWalletBalanceAsync(string walletName, int apiPo
return new WalletItem(name: walletName, balance: walletBalanceModel.AccountsBalances[0].SpendableAmount);
}
- public class WalletCollisionException: Exception { }
+ public class WalletCollisionException : Exception { }
public async Task ResyncWalletAsync(int apiPort, string walletName)
{
@@ -774,16 +763,16 @@ public string SendTransaction(int apiPort, string hex)
return sendActionResult.TransactionId.ToString();
}
- public async Task CallJoinFederationRequestAsync(string collateralAddress, string collateralWallet, string collateralPassword, string cirrusWalletName, string cirrusWalletPassword)
+ public async Task CallJoinFederationRequestAsync(WalletCredentials collateralCredentials, WalletCredentials miningCredentials)
{
var request = new JoinFederationRequestModel()
{
- CollateralAddress = collateralAddress,
- CollateralWalletName = collateralWallet,
- CollateralWalletPassword = collateralPassword,
+ CollateralAddress = collateralCredentials.ChoosenAddress,
+ CollateralWalletName = collateralCredentials.Name,
+ CollateralWalletPassword = collateralCredentials.Password,
WalletAccount = "account 0",
- WalletName = cirrusWalletName,
- WalletPassword = cirrusWalletPassword
+ WalletName = miningCredentials.Name,
+ WalletPassword = miningCredentials.Password
};
try
@@ -794,7 +783,7 @@ public async Task CallJoinFederationRequestAsync(string collateralAddress,
.ReceiveJson()
.ConfigureAwait(false);
- if (response != null && !string.IsNullOrEmpty(response.MinerPublicKey))
+ if (response != null && !string.IsNullOrEmpty(response.MinerPublicKey))
{
Status($"SUCCESS: The masternode request has now been submitted to the network");
return true;
@@ -935,7 +924,7 @@ public async Task StartMasterNodeDashboardAsync()
}
Status($"Starting the masternode dashboard on {this.networkType}. Start up arguments: {argumentBuilder}");
-
+
string osSpecificCommand = "CMD.EXE";
string osSpecificArguments = $"/K \"{argumentBuilder}\"";
diff --git a/src/Core/StateMachine.cs b/src/Core/StateMachine.cs
new file mode 100644
index 0000000..29f6e7f
--- /dev/null
+++ b/src/Core/StateMachine.cs
@@ -0,0 +1,912 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+using NBitcoin;
+using static MasternodeSetupTool.RegistrationService;
+
+namespace MasternodeSetupTool;
+
+public class StateMachine: ILogger
+{
+ public enum State
+ {
+ Begin,
+ End,
+ RunMasterNode_KeyPresent,
+ Run_StartMainChain,
+ Run_MainChainSynced,
+ Run_StartSideChain,
+ Run_SideChainSynced,
+ Run_LaunchBrowser,
+ SetupMasterNode_Eula,
+ Setup_KeyPresent,
+ Setup_StartMainChain,
+ Setup_CreateKey,
+ Setup_MainChainSynced,
+ Setup_StartSideChain,
+ Setup_SideChainSynced,
+ Setup_Create_AskForCollateral,
+ Setup_CheckIsFederationMember,
+ Setup_Select,
+ Setup_Create,
+ Setup_Create_Mining,
+ Setup_CheckForCollateral,
+ Setup_Restore,
+ Setup_Restore_Mining,
+ Setup_CheckForRegistrationFee,
+ Setup_PerformRegistration,
+ Setup_WaitForBalance,
+ Setup_WaitForRegistration,
+ Setup_UseExisting,
+ Setup_UseExisting_CollateralPassword,
+ Setup_UseExisting_Mining,
+ Setup_UseExisting_MiningPassword,
+ Setup_UseExisting_CheckMainWalletSynced,
+ Setup_UseExisting_CheckSideWalletSynced
+ }
+
+ private readonly IRegistrationService registrationService;
+ private readonly IStateHandler stateHandler;
+
+ private IStateHolder stateHolder;
+
+ public WalletCredentials? collateralWalletCredentials;
+ public WalletCredentials? miningWalletCredentials;
+
+ public WalletCreationState? collateralWalletCreationState;
+ public WalletCreationState? miningWalletCreationState;
+
+
+ private bool IsEnabled = true;
+
+ public StateMachine(
+ NetworkType networkType,
+ IStateHandler stateHandler,
+ IRegistrationService? registrationService = null,
+ IStateHolder? stateHolder = null)
+ {
+ this.stateHandler = stateHandler;
+ this.registrationService = registrationService ?? new RegistrationService(networkType, this);
+ this.stateHolder = stateHolder ?? new DefaultStateHolder();
+ }
+
+ public void OnRunNode()
+ {
+ this.stateHolder.NextState = State.RunMasterNode_KeyPresent;
+ }
+
+ public void OnSetupNode()
+ {
+ this.stateHolder.NextState = State.SetupMasterNode_Eula;
+ }
+
+ public async Task TickAsync()
+ {
+ if (!this.IsEnabled) return;
+
+ this.IsEnabled = false;
+
+ if (this.stateHolder.CurrentState == State.Begin)
+ {
+ await this.stateHandler.OnStart();
+ await this.stateHandler.OnProgramVersionAvailable(GetInformationalVersion());
+ }
+
+ if (this.stateHolder.NextState == null)
+ {
+ this.IsEnabled = true;
+
+ return;
+ }
+
+ this.stateHolder.SwitchToNextState();
+
+ if (await RunBranchAsync())
+ {
+ this.IsEnabled = true;
+
+ return;
+ }
+
+ if (await SetupBranchAsync())
+ {
+ this.IsEnabled = true;
+
+ return;
+ }
+
+ this.IsEnabled = true;
+ }
+
+ private async Task RunBranchAsync()
+ {
+ // The 'Run' branch
+
+ if (this.stateHolder.CurrentState == State.RunMasterNode_KeyPresent)
+ {
+ if (!this.registrationService.CheckFederationKeyExists())
+ {
+ await this.stateHandler.OnFederationKeyMissing();
+
+ GoToEndState();
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Run_StartMainChain;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Run_StartMainChain)
+ {
+ if (!await this.registrationService.StartNodeAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true))
+ {
+ await this.stateHandler.OnNodeFailedToStart(NodeType.MainChain);
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Run_MainChainSynced;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Run_MainChainSynced)
+ {
+ await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ await this.registrationService.EnsureMainChainNodeAddressIndexerIsSyncedAsync().ConfigureAwait(true);
+
+ await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ this.stateHolder.NextState = State.Run_StartSideChain;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Run_StartSideChain)
+ {
+ if (!await this.registrationService.StartNodeAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true))
+ {
+ await this.stateHandler.OnNodeFailedToStart(NodeType.SideChain);
+ GoToEndState();
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Run_SideChainSynced;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Run_SideChainSynced)
+ {
+ await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ await this.registrationService.EnsureNodeIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ this.stateHolder.NextState = State.Run_LaunchBrowser;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Run_LaunchBrowser)
+ {
+ await this.registrationService.StartMasterNodeDashboardAsync().ConfigureAwait(true);
+ this.registrationService.LaunchBrowser($"http://localhost:{RegistrationService.DashboardPort}");
+
+ GoToEndState();
+ return true;
+ }
+
+ return false;
+ }
+
+ private async Task SetupBranchAsync()
+ {
+ if (this.stateHolder.CurrentState == State.SetupMasterNode_Eula)
+ {
+ if (!await this.stateHandler.OnAskForEULA())
+ {
+ GoToEndState();
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_KeyPresent;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_KeyPresent)
+ {
+ if (this.registrationService.CheckFederationKeyExists())
+ {
+ if (!await this.stateHandler.OnAskForNewFederationKey())
+ {
+ this.stateHolder.NextState = State.Setup_StartMainChain;
+ return true;
+ }
+
+ this.registrationService.DeleteFederationKey();
+ }
+
+ this.stateHolder.NextState = State.Setup_CreateKey;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_CreateKey)
+ {
+ string savePath = this.registrationService.CreateFederationKey();
+
+ await this.stateHandler.OnShowNewFederationKey(this.registrationService.PubKey, savePath);
+
+ this.stateHolder.NextState = State.Setup_StartMainChain;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_StartMainChain)
+ {
+ // All 3 sub-branches of this state require the mainchain and sidechain nodes to be initialized, so do that first.
+ if (!await this.registrationService.StartNodeAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true))
+ {
+ await this.stateHandler.OnNodeFailedToStart(NodeType.MainChain);
+
+ GoToEndState();
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_MainChainSynced;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_MainChainSynced)
+ {
+ await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ await this.registrationService.EnsureMainChainNodeAddressIndexerIsSyncedAsync().ConfigureAwait(true);
+
+ await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ this.stateHolder.NextState = State.Setup_StartSideChain;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_StartSideChain)
+ {
+ if (!await this.registrationService.StartNodeAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true))
+ {
+ await this.stateHandler.OnNodeFailedToStart(NodeType.SideChain);
+
+ GoToEndState();
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_SideChainSynced;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_SideChainSynced)
+ {
+ await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ await this.registrationService.EnsureNodeIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+
+ this.stateHolder.NextState = State.Setup_CheckIsFederationMember;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_CheckIsFederationMember)
+ {
+ if (await this.registrationService.CheckIsFederationMemberAsync().ConfigureAwait(true))
+ {
+ if (await this.stateHandler.OnAskToRunIfAlreadyMember())
+ {
+ this.stateHolder.NextState = State.Run_LaunchBrowser;
+ return true;
+ }
+ else
+ {
+ await this.stateHandler.OnAlreadyMember();
+ GoToEndState();
+ return true;
+ }
+ }
+
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_Select)
+ {
+ //TODO: Probably we need to show picker for collateral and mining wallets independently
+ switch (await this.stateHandler.OnAskForWalletSource(NodeType.MainChain))
+ {
+ case WalletSource.NewWallet:
+ this.stateHolder.NextState = State.Setup_Create;
+ break;
+ case WalletSource.RestoreWallet:
+ this.stateHolder.NextState = State.Setup_Restore;
+ break;
+ case WalletSource.UseExistingWallet:
+ this.stateHolder.NextState = State.Setup_UseExisting;
+ break;
+ default:
+ await this.stateHandler.OnRegistrationCanceled();
+ GoToEndState();
+ break;
+ }
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_Create)
+ {
+
+ if (!await HandleCreateWalletsAsync(NodeType.MainChain, createNewMnemonic: true))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_Create_Mining;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_Create_Mining)
+ {
+
+ if (!await HandleCreateWalletsAsync(NodeType.SideChain, createNewMnemonic: true))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_Create_AskForCollateral;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_Create_AskForCollateral)
+ {
+ this.collateralWalletCredentials.ChoosenAddress = await HandleAddressSelectionAsync(NodeType.MainChain, this.collateralWalletCredentials.Name);
+
+ if (this.collateralWalletCredentials.ChoosenAddress == null)
+ {
+ this.collateralWalletCredentials.ChoosenAddress = await this.registrationService.GetFirstWalletAddressAsync(this.registrationService.MainchainNetwork.DefaultAPIPort, this.collateralWalletCredentials.Name).ConfigureAwait(true);
+ }
+
+ // The 3 sub-branches recombine after this and can share common states.
+ this.stateHolder.NextState = State.Setup_CheckForCollateral;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_CheckForCollateral)
+ {
+ if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.MainchainNetwork.DefaultAPIPort, this.collateralWalletCredentials.Name, RegistrationService.CollateralRequirement).ConfigureAwait(true))
+ {
+ this.stateHolder.NextState = State.Setup_CheckForRegistrationFee;
+ }
+ else
+ {
+ await this.stateHandler.OnWaitingForCollateral();
+ this.stateHolder.NextState = State.Setup_CheckForCollateral;
+ }
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_CheckForRegistrationFee)
+ {
+ if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletCredentials.Name, RegistrationService.FeeRequirement).ConfigureAwait(true))
+ {
+ this.stateHolder.NextState = State.Setup_PerformRegistration;
+ }
+ else
+ {
+ string? miningAddress = await this.registrationService.GetFirstWalletAddressAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletCredentials.Name).ConfigureAwait(true);
+ this.miningWalletCredentials.ChoosenAddress = miningAddress;
+ await this.stateHandler.OnMissingRegistrationFee(miningAddress);
+ this.stateHolder.NextState = State.Setup_WaitForBalance;
+ }
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_WaitForBalance)
+ {
+
+ if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletCredentials.Name, RegistrationService.FeeRequirement).ConfigureAwait(true))
+ {
+ this.stateHolder.NextState = State.Setup_PerformRegistration;
+ }
+ else
+ {
+ await this.stateHandler.OnWaitingForRegistrationFee();
+ this.stateHolder.NextState = State.Setup_WaitForBalance;
+ }
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_PerformRegistration)
+ {
+ bool registeredSuccessfully = await this.registrationService.CallJoinFederationRequestAsync(this.collateralWalletCredentials, this.miningWalletCredentials).ConfigureAwait(true);
+ if (!registeredSuccessfully)
+ {
+ await this.stateHandler.OnRegistrationFailed();
+ GoToEndState();
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_WaitForRegistration;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_WaitForRegistration)
+ {
+ if (await this.registrationService.MonitorJoinFederationRequestAsync().ConfigureAwait(true))
+ {
+ await this.stateHandler.OnRegistrationComplete();
+ this.stateHolder.NextState = State.Run_LaunchBrowser;
+ }
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_Restore)
+ {
+ if (!await HandleCreateWalletsAsync(NodeType.MainChain, createNewMnemonic: false))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_Restore_Mining;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_Restore_Mining)
+ {
+ if (!await HandleCreateWalletsAsync(NodeType.SideChain, createNewMnemonic: false))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_Create_AskForCollateral;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_UseExisting)
+ {
+ this.collateralWalletCredentials = new WalletCredentials();
+ if (!await HandleExistingWalletNameAsync(NodeType.MainChain, this.collateralWalletCredentials))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_UseExisting_CollateralPassword;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_UseExisting_CollateralPassword)
+ {
+ if (!await HandleExistingPasswordAsync(NodeType.MainChain, collateralWalletCredentials))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_UseExisting_Mining;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_UseExisting_Mining)
+ {
+ this.miningWalletCredentials = new WalletCredentials();
+ if (!await HandleExistingWalletNameAsync(NodeType.SideChain,this.miningWalletCredentials))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_UseExisting_MiningPassword;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_UseExisting_MiningPassword)
+ {
+ if (!await HandleExistingPasswordAsync(NodeType.SideChain, miningWalletCredentials))
+ {
+ this.stateHolder.NextState = State.Setup_Select;
+ return true;
+ }
+
+ this.stateHolder.NextState = State.Setup_UseExisting_CheckMainWalletSynced;
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_UseExisting_CheckMainWalletSynced)
+ {
+ if (await HandleWalletSyncAsync(NodeType.MainChain))
+ {
+ this.stateHolder.NextState = State.Setup_UseExisting_CheckSideWalletSynced;
+ }
+ return true;
+ }
+
+ if (this.stateHolder.CurrentState == State.Setup_UseExisting_CheckSideWalletSynced)
+ {
+ if (await HandleWalletSyncAsync(NodeType.SideChain))
+ {
+ // Now we can jump back into the same sequence as the other 2 sub-branches.
+ this.stateHolder.NextState = State.Setup_Create_AskForCollateral;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private async Task HandleAddressSelectionAsync(NodeType nodeType, string walletName)
+ {
+ Network network = nodeType == NodeType.MainChain
+ ? this.registrationService.MainchainNetwork
+ : this.registrationService.SidechainNetwork;
+
+ List? addressesWithBalance = await this.registrationService.GetWalletAddressesAsync(walletName, network.DefaultAPIPort);
+
+ if (addressesWithBalance != null)
+ {
+ return await this.stateHandler.OnChooseAddress(addressesWithBalance, nodeType);
+ }
+
+ return null;
+ }
+
+ private async Task HandleExistingWalletNameAsync(NodeType nodeType, WalletCredentials walletCredentials)
+ {
+ Network network = nodeType == NodeType.MainChain
+ ? this.registrationService.MainchainNetwork
+ : this.registrationService.SidechainNetwork;
+
+ List? walletsWithBalance = await this.registrationService.GetWalletsWithBalanceAsync(network.DefaultAPIPort);
+
+ if (walletsWithBalance != null)
+ {
+ string? walletName = await this.stateHandler.OnChooseWallet(walletsWithBalance, nodeType);
+
+ if (walletName == null)
+ return false;
+
+ walletCredentials.Name = walletName;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private async Task HandleNewWalletNameAsync(NodeType nodeType, WalletCreationState creationState)
+ {
+ do
+ {
+ string? walletName = await this.stateHandler.OnAskForWalletName(nodeType, true);
+
+ if (walletName == null)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(walletName))
+ {
+ try
+ {
+ Network network = nodeType == NodeType.MainChain
+ ? this.registrationService.MainchainNetwork
+ : this.registrationService.SidechainNetwork;
+
+ if (!await this.registrationService.FindWalletByNameAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true))
+ {
+ creationState.Name = walletName;
+ break;
+ }
+ else
+ {
+ await this.stateHandler.OnWalletNameExists(nodeType);
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ await this.stateHandler.OnWalletExistsOrInvalid(nodeType);
+ } while (true);
+
+ return true;
+ }
+
+ private async Task HandleNewMnemonicAsync(NodeType nodeType, WalletCreationState creationState, bool canChangeMnemonic = false)
+ {
+ var mnemonic = string.Join(' ', new Mnemonic("English", WordCount.Twelve).Words);
+
+ if (await this.stateHandler.OnAskForMnemonicConfirmation(nodeType, mnemonic))
+ {
+ return false;
+ }
+
+ creationState.Mnemonic = mnemonic;
+
+ return true;
+ }
+
+ private async Task HandleUserMnemonic(NodeType nodeType, WalletCreationState creationState)
+ {
+ string? mnemonic;
+
+ do
+ {
+ mnemonic = await this.stateHandler.OnAskForUserMnemonic(nodeType);
+
+ if (mnemonic == null)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(mnemonic))
+ {
+ try
+ {
+ // Test the mnemonic to ensure validity.
+ var temp = new Mnemonic(mnemonic, Wordlist.English);
+
+ // If there was no exception, break out of the loop and continue.
+ break;
+ }
+ catch
+ {
+ }
+ }
+
+ await this.stateHandler.OnMnemonicIsInvalid(nodeType);
+ } while (true);
+
+ creationState.Mnemonic = mnemonic;
+
+ return true;
+ }
+
+ private async Task HandlePassphraseAsync(NodeType nodeType, WalletCreationState creationState)
+ {
+ string result = await this.stateHandler.OnAskForPassphrase(nodeType);
+
+ creationState.Passphrase = result;
+
+ return true;
+ }
+
+ private async Task HandleExistingPasswordAsync(NodeType nodeType, WalletCredentials credentials)
+ {
+ while (true)
+ {
+ string password = await this.stateHandler.OnAskForWalletPassword(nodeType);
+
+ if (await this.registrationService.CheckWalletPasswordAsync(NodeApiPort(nodeType), credentials.Name, password) == false)
+ {
+ if (!await this.stateHandler.OnAskReenterPassword(nodeType))
+ {
+ return false;
+ }
+
+ continue;
+ }
+
+ credentials.Password = password;
+
+ return true;
+ }
+ }
+
+ private async Task HandleNewPasswordAsync(NodeType nodeType, WalletCreationState creationState)
+ {
+ string password = await this.stateHandler.OnAskForWalletPassword(nodeType);
+
+ if (password == null)
+ {
+ return false;
+ }
+
+ creationState.Password = password;
+ return true;
+ }
+
+ private async Task HandleWalletCreationAsync(NodeType nodeType, WalletCreationState walletCreationState, bool createNewWallet)
+ {
+ Network network = nodeType == NodeType.MainChain
+ ? this.registrationService.MainchainNetwork
+ : this.registrationService.SidechainNetwork;
+
+ if (walletCreationState == null ||!walletCreationState.IsValid())
+ {
+ return false;
+ }
+
+ while (true)
+ {
+ try
+ {
+ if (!await this.registrationService.RestoreWalletAsync(network.DefaultAPIPort, nodeType, walletCreationState.Name, walletCreationState.Mnemonic, walletCreationState.Passphrase, walletCreationState.Password, createNewWallet).ConfigureAwait(true))
+ {
+ if (createNewWallet)
+ await this.stateHandler.OnCreateWalletFailed(nodeType);
+ else
+ await this.stateHandler.OnRestoreWalletFailed(nodeType);
+
+ return false;
+ }
+ break;
+ }
+ catch (WalletCollisionException)
+ {
+ await this.stateHandler.OnMnemonicExists(nodeType);
+
+ if (!await HandleNewMnemonicAsync(nodeType, walletCreationState, canChangeMnemonic: true))
+ {
+ await this.stateHandler.OnRegistrationCanceled();
+ return false;
+ }
+ }
+ }
+
+ if (!await this.registrationService.ResyncWalletAsync(network.DefaultAPIPort, walletCreationState.Name).ConfigureAwait(true))
+ {
+ await this.stateHandler.OnResyncFailed(nodeType);
+ return false;
+ }
+
+ WalletCredentials newCredentials = new WalletCredentials
+ {
+ Name = walletCreationState.Name,
+ Password = walletCreationState.Password,
+ };
+
+ if (nodeType == NodeType.MainChain)
+ {
+ this.collateralWalletCredentials = newCredentials;
+ }
+ else if (nodeType == NodeType.SideChain)
+ {
+ this.miningWalletCredentials = newCredentials;
+ }
+
+ return true;
+ }
+
+ private async Task HandleWalletSyncAsync(NodeType nodeType)
+ {
+ Network network = nodeType == NodeType.MainChain
+ ? this.registrationService.MainchainNetwork
+ : this.registrationService.SidechainNetwork;
+
+ string? walletName = nodeType == NodeType.MainChain
+ ? this.collateralWalletCredentials.Name
+ : this.miningWalletCredentials.Name;
+
+ if (walletName == null)
+ {
+ throw new ArgumentException("Wallet name can not be null.");
+ }
+
+ int percentSynced = await this.registrationService.WalletSyncProgressAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true);
+ await this.stateHandler.OnWalletSyncing(nodeType, percentSynced);
+ if (await this.registrationService.IsWalletSyncedAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true))
+ {
+ await this.stateHandler.OnWalletSynced(nodeType);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private async Task HandleCreateWalletsAsync(NodeType nodeType, bool createNewMnemonic)
+ {
+ WalletCreationState walletCreationState = new WalletCreationState();
+
+ if (createNewMnemonic)
+ {
+ if (!await HandleNewMnemonicAsync(nodeType, walletCreationState))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!await HandleUserMnemonic(nodeType, walletCreationState))
+ {
+ return false;
+ }
+ }
+
+ if (!await HandleNewWalletNameAsync(nodeType, walletCreationState))
+ {
+ return false;
+ }
+
+ if (!await HandlePassphraseAsync(nodeType, walletCreationState))
+ {
+ return false;
+ }
+
+ if (!await HandleNewPasswordAsync(nodeType, walletCreationState))
+ {
+ return false;
+ }
+
+ if (!await HandleWalletCreationAsync(nodeType, walletCreationState, createNewMnemonic))
+ {
+ return false;
+ }
+
+ try
+ {
+ while (!await HandleWalletSyncAsync(nodeType))
+ {
+ await Task.Delay(TimeSpan.FromSeconds(1));
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void GoToEndState()
+ {
+ this.stateHolder.NextState = State.End;
+ }
+
+ public static string? GetInformationalVersion() =>
+ Assembly
+ .GetExecutingAssembly()
+ ?.GetCustomAttribute()
+ ?.InformationalVersion;
+
+ private string WalletTypeName(NodeType nodeType)
+ {
+ return nodeType == NodeType.MainChain ? "collateral" : "mining";
+ }
+
+ private WalletCredentials? GetWalletCredentials(NodeType nodeType)
+ {
+ if (nodeType == NodeType.MainChain)
+ {
+ return collateralWalletCredentials;
+ }
+ else
+ {
+ return miningWalletCredentials;
+ }
+ }
+
+ private int NodeApiPort(NodeType nodeType)
+ {
+ Network network = nodeType == NodeType.MainChain ? this.registrationService.MainchainNetwork : this.registrationService.SidechainNetwork;
+ return network.DefaultAPIPort;
+ }
+
+ public void Info(string message, string? updateTag = null)
+ {
+ this.stateHandler.Info(message, updateTag);
+ }
+
+ public void Error(string message)
+ {
+ this.stateHandler.Error(message);
+ }
+
+ public void Error(Exception exception)
+ {
+ this.stateHandler.Error(exception);
+ }
+
+ public void Error(string message, Exception exception)
+ {
+ this.stateHandler.Error(message, exception);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/WalletCreationState.cs b/src/Core/WalletCreationState.cs
new file mode 100644
index 0000000..3e8853d
--- /dev/null
+++ b/src/Core/WalletCreationState.cs
@@ -0,0 +1,14 @@
+namespace MasternodeSetupTool;
+
+public class WalletCreationState
+{
+ public string Name;
+ public string Mnemonic;
+ public string Passphrase;
+ public string Password;
+
+ public bool IsValid()
+ {
+ return !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Mnemonic) && !string.IsNullOrEmpty(Password);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/WalletCredentials.cs b/src/Core/WalletCredentials.cs
new file mode 100644
index 0000000..5ff70a4
--- /dev/null
+++ b/src/Core/WalletCredentials.cs
@@ -0,0 +1,9 @@
+namespace MasternodeSetupTool;
+
+public class WalletCredentials
+{
+ public string Name;
+ public string Password;
+
+ public string ChoosenAddress;
+}
\ No newline at end of file
diff --git a/src/MasternodeSetupTool/WalletItem.cs b/src/Core/WalletItem.cs
similarity index 100%
rename from src/MasternodeSetupTool/WalletItem.cs
rename to src/Core/WalletItem.cs
diff --git a/src/MasternodeSetupTool.Tests/MasternodeSetupTool.Tests.csproj b/src/MasternodeSetupTool.Tests/MasternodeSetupTool.Tests.csproj
new file mode 100644
index 0000000..ef24dc3
--- /dev/null
+++ b/src/MasternodeSetupTool.Tests/MasternodeSetupTool.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net6.0-windows
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MasternodeSetupTool.Tests/StateMachineUnitTests.cs b/src/MasternodeSetupTool.Tests/StateMachineUnitTests.cs
new file mode 100644
index 0000000..7b5a727
--- /dev/null
+++ b/src/MasternodeSetupTool.Tests/StateMachineUnitTests.cs
@@ -0,0 +1,43 @@
+using Moq;
+using NBitcoin;
+
+namespace MasternodeSetupTool.Tests
+{
+ public class Tests
+ {
+ private NetworkType networkType;
+
+ private Mock registrationService;
+ private Mock stateHandler;
+
+ private IStateHolder stateHolder;
+ private StateMachine stateMachine;
+
+ [SetUp]
+ public void Setup()
+ {
+ this.networkType = NetworkType.Testnet;
+
+ this.registrationService = new Mock();
+ this.stateHandler = new Mock();
+
+ this.stateHolder = new DefaultStateHolder();
+ this.stateMachine = new StateMachine(networkType, stateHandler.Object, registrationService.Object, stateHolder);
+ }
+
+ [Test]
+ public async Task ShouldAskForEULA()
+ {
+ this.stateHolder.NextState = StateMachine.State.SetupMasterNode_Eula;
+
+ this.stateHandler.Setup(h => h.OnAskForEULA().Result).Returns(true);
+
+ await this.stateMachine.TickAsync();
+
+ Assert.That(this.stateHolder.NextState, Is.Not.Null);
+
+ Assert.That(this.stateHolder.NextState, Is.Not.EqualTo(StateMachine.State.SetupMasterNode_Eula));
+ Assert.That(this.stateHolder.NextState, Is.EqualTo(StateMachine.State.Setup_KeyPresent));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MasternodeSetupTool.Tests/Usings.cs b/src/MasternodeSetupTool.Tests/Usings.cs
new file mode 100644
index 0000000..cefced4
--- /dev/null
+++ b/src/MasternodeSetupTool.Tests/Usings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
\ No newline at end of file
diff --git a/src/MasternodeSetupTool.sln b/src/MasternodeSetupTool.sln
index cbed098..ab8100b 100644
--- a/src/MasternodeSetupTool.sln
+++ b/src/MasternodeSetupTool.sln
@@ -33,6 +33,12 @@ Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "MasternodeSetupToolInstalle
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.FederatedSidechains.AdminDashboard", "..\StratisMasternodeDashboard\src\StratisMasternodeDashboard\Stratis.FederatedSidechains.AdminDashboard.csproj", "{31ECC5D0-F4E8-4C5B-A1C9-3A503BAAD0A8}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MasternodeSetupTool.Tests", "MasternodeSetupTool.Tests\MasternodeSetupTool.Tests.csproj", "{9BC703A3-4C17-481A-B9E7-BF7DA0275D16}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomatedFlowTest", "AutomatedFlowTest\AutomatedFlowTest.csproj", "{7EFD772D-AC69-477C-A731-7EABF10B8DE6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{486CE220-C929-4125-AB22-9ADBCC4CB293}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -55,6 +61,18 @@ Global
{31ECC5D0-F4E8-4C5B-A1C9-3A503BAAD0A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31ECC5D0-F4E8-4C5B-A1C9-3A503BAAD0A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31ECC5D0-F4E8-4C5B-A1C9-3A503BAAD0A8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9BC703A3-4C17-481A-B9E7-BF7DA0275D16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9BC703A3-4C17-481A-B9E7-BF7DA0275D16}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9BC703A3-4C17-481A-B9E7-BF7DA0275D16}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9BC703A3-4C17-481A-B9E7-BF7DA0275D16}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7EFD772D-AC69-477C-A731-7EABF10B8DE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7EFD772D-AC69-477C-A731-7EABF10B8DE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7EFD772D-AC69-477C-A731-7EABF10B8DE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7EFD772D-AC69-477C-A731-7EABF10B8DE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {486CE220-C929-4125-AB22-9ADBCC4CB293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {486CE220-C929-4125-AB22-9ADBCC4CB293}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {486CE220-C929-4125-AB22-9ADBCC4CB293}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {486CE220-C929-4125-AB22-9ADBCC4CB293}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/MasternodeSetupTool/App.xaml.cs b/src/MasternodeSetupTool/App.xaml.cs
index c42b434..98834b5 100644
--- a/src/MasternodeSetupTool/App.xaml.cs
+++ b/src/MasternodeSetupTool/App.xaml.cs
@@ -1,10 +1,4 @@
-using System.Collections.Generic;
-using System.Windows;
-using Flurl.Http;
-using Flurl.Http.Configuration;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-using Stratis.Bitcoin.Utilities.JsonConverters;
+using System.Windows;
namespace MasternodeSetupTool
{
@@ -15,19 +9,7 @@ public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
- FlurlHttp.Configure(settings => {
- var jsonSettings = new JsonSerializerSettings
- {
- Converters = new List()
- {
- new DateTimeToUnixTimeConverter(),
- new IsoDateTimeConverter(),
- }
- };
-
- settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
- });
-
+ CoreLib.Initialize();
var wnd = new MainWindow(e.Args);
wnd.Show();
diff --git a/src/MasternodeSetupTool/DefaultStateHolder.cs b/src/MasternodeSetupTool/DefaultStateHolder.cs
new file mode 100644
index 0000000..243c271
--- /dev/null
+++ b/src/MasternodeSetupTool/DefaultStateHolder.cs
@@ -0,0 +1,55 @@
+namespace MasternodeSetupTool
+{
+ public class DefaultStateHolder : IStateHolder
+ {
+ public StateMachine.State currentState_ = StateMachine.State.Begin;
+ public StateMachine.State? nextState_;
+
+ private bool repeatOnEndState;
+
+ public DefaultStateHolder(bool repeatOnEndState = true)
+ {
+ this.repeatOnEndState = repeatOnEndState;
+ }
+
+ public StateMachine.State CurrentState
+ {
+ get
+ {
+ return this.currentState_;
+ }
+
+ private set
+ {
+ this.currentState_ = value;
+ }
+ }
+
+ public StateMachine.State? NextState
+ {
+ get
+ {
+ return this.nextState_;
+ }
+
+ set
+ {
+ this.nextState_ = value;
+ }
+ }
+
+ public void SwitchToNextState()
+ {
+ StateMachine.State? nextState = this.NextState;
+ if (nextState != null)
+ {
+ if (nextState == StateMachine.State.End && this.repeatOnEndState)
+ {
+ nextState = StateMachine.State.Begin;
+ }
+ this.CurrentState = (StateMachine.State)nextState;
+ this.NextState = null;
+ }
+ }
+ }
+}
diff --git a/src/MasternodeSetupTool/MainWindow.xaml.cs b/src/MasternodeSetupTool/MainWindow.xaml.cs
index 6950f91..5179fee 100644
--- a/src/MasternodeSetupTool/MainWindow.xaml.cs
+++ b/src/MasternodeSetupTool/MainWindow.xaml.cs
@@ -1,6 +1,5 @@
-using System;
+using System;
using System.Collections.Generic;
-using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -9,10 +8,7 @@
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Threading;
-using CSharpFunctionalExtensions;
using NBitcoin;
-using Stratis.Bitcoin.Features.Wallet.Models;
-using static MasternodeSetupTool.RegistrationService;
using Color = System.Windows.Media.Color;
namespace MasternodeSetupTool
@@ -20,7 +16,7 @@ namespace MasternodeSetupTool
///
/// Interaction logic for MainWindow.xaml
///
- public partial class MainWindow : Window, ILogger
+ public partial class MainWindow : Window, IStateHandler
{
private const string MainStackPanelTag = "Main";
private const string StatusBarTextBlockTag = "txtStatusBar";
@@ -29,49 +25,17 @@ public partial class MainWindow : Window, ILogger
private readonly StackPanel stackPanel;
private readonly TextBlock statusBar;
- private readonly RegistrationService registrationService;
+ private string collateralWalletName;
+ private string miningWalletName;
- private readonly DispatcherTimer timer;
-
- private string currentState = "Begin";
- private string? nextState = null;
-
- private bool createdButtons;
-
- private string? collateralWalletMnemonic;
- private string? miningWalletMnemonic;
+ private string collateralAddress;
+ private string miningAddress;
- private string? collateralWalletPassphrase;
- private string? miningWalletPassphrase;
+ private bool createdButtons = false;
- private string? collateralWalletPassword;
- private string? miningWalletPassword;
-
- private string? collateralWalletName;
- private string? miningWalletName;
-
- private string? _collateralAddress;
- private string? _miningAddress;
-
- private string? CollateralAddress
- {
- get => this._collateralAddress;
- set
- {
- this._collateralAddress = value;
- this.CollateralAddressText.Text = $"{collateralWalletName}: {value}";
- }
- }
+ private StateMachine stateMachine;
- private string? MiningAddress
- {
- get => this._miningAddress;
- set
- {
- this._miningAddress = value;
- this.MiningAddressText.Text = $"{miningWalletName}: {value}";
- }
- }
+ private readonly DispatcherTimer timer;
private bool PrintStacktraces
{
@@ -97,14 +61,7 @@ private Style FlatStyle
public MainWindow(string[] args)
{
InitializeComponent();
-
- string? appVersion = GetInformationalVersion();
-
- if (appVersion != null)
- {
- this.VersionText.Text = $"Version: {appVersion}";
- }
-
+
this.stackPanel = (StackPanel)this.FindName(MainStackPanelTag);
this.statusBar = (TextBlock)this.FindName(StatusBarTextBlockTag);
@@ -116,11 +73,11 @@ public MainWindow(string[] args)
if (args.Any(a => a.Contains("-regtest")))
networkType = NetworkType.Regtest;
- this.registrationService = new RegistrationService(networkType, this);
+ this.stateMachine = new StateMachine(networkType, this);
this.timer = new DispatcherTimer
{
- Interval = TimeSpan.FromSeconds(1)
+ Interval = TimeSpan.FromMilliseconds(200)
};
this.timer.Tick += StateMachine_TickAsync;
@@ -129,16 +86,133 @@ public MainWindow(string[] args)
private async void StateMachine_TickAsync(object? sender, EventArgs e)
{
- this.timer.IsEnabled = false;
+ await this.stateMachine.TickAsync();
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Button button = (Button)sender;
+
+ if (button == null)
+ {
+ return;
+ }
+
+ switch (button.Tag.ToString())
+ {
+ case "RunMasterNode":
+ {
+ this.stateMachine.OnRunNode();
+ break;
+ }
+ case "SetupMasterNode":
+ {
+ this.stateMachine.OnSetupNode();
+ break;
+ }
+ }
+ }
- if (this.currentState == "Begin")
+ private void LogWithBrush(string message, Brush? brush = null, string? updateTag = null)
+ {
+ this.statusBar.Dispatcher.Invoke(() =>
{
- if (!this.createdButtons)
+ var inline = new Run(message + "\n");
+ inline.Tag = updateTag;
+
+ if (brush != null)
+ {
+ inline.Foreground = brush;
+ }
+
+ InlineCollection inlines = this.statusBar.Inlines;
+ Inline lastInline = inlines.LastInline;
+
+ if (updateTag != null && lastInline != null && string.Equals(lastInline.Tag, updateTag))
{
- this.createdButtons = true;
+ inlines.Remove(lastInline);
+ }
+
+ inlines.Add(inline);
+ this.logScrollArea.ScrollToBottom();
+ });
+ }
+
+ private void Log(string message, string? updateTag = null)
+ {
+ this.statusBar.Dispatcher.Invoke(() =>
+ {
+ LogWithBrush(message, brush: null, updateTag);
+ });
+ }
+
+ private void Log(string message, Color color, string? updateTag = null)
+ {
+ this.statusBar.Dispatcher.Invoke(() =>
+ {
+ LogWithBrush(message, new SolidColorBrush(color), updateTag);
+ });
+ }
+
+ private void LogError(string message)
+ {
+ Log(message, Color.FromRgb(255, 51, 51));
+ }
+
+ public void Info(string message, string? updateTag = null)
+ {
+ Log(message, updateTag: updateTag);
+ }
+
+ public void Error(string message)
+ {
+ LogError(message);
+ }
+
+ public void Error(Exception exception)
+ {
+ if (this.PrintStacktraces)
+ {
+ LogError($"{exception}");
+ }
+ }
+
+ public void Error(string message, Exception exception)
+ {
+ Error(message);
+ Error(exception);
+ }
+
+ private string WalletTypeName(NodeType nodeType)
+ {
+ return nodeType == NodeType.MainChain ? "collateral" : "mining";
+ }
+
+ public static string? GetInformationalVersion() =>
+ Assembly
+ .GetExecutingAssembly()
+ ?.GetCustomAttribute()
+ ?.InformationalVersion;
+
+ public async Task OnStart()
+ {
+ this.collateralWalletName = null;
+ this.miningWalletName = null;
+
+ this.collateralAddress = null;
+ this.miningAddress = null;
+
+ this.UpdateWalletInfoLabel(NodeType.MainChain);
+ this.UpdateWalletInfoLabel(NodeType.SideChain);
+
+ if (!this.createdButtons)
+ {
+ this.createdButtons = true;
- Style flatStyle = this.FlatStyle;
+ Style flatStyle = this.FlatStyle;
+ try
+ {
var button = new Button
{
Content = "Run Masternode",
@@ -147,7 +221,6 @@ private async void StateMachine_TickAsync(object? sender, EventArgs e)
Padding = new Thickness(4.0),
Background = new SolidColorBrush(Color.FromRgb(255, 255, 255)),
};
-
button.Click += new RoutedEventHandler(Button_Click);
this.stackPanel.Children.Add(button);
@@ -164,1022 +237,330 @@ private async void StateMachine_TickAsync(object? sender, EventArgs e)
this.stackPanel.Children.Add(button);
}
- }
-
- if (this.nextState == null)
- {
- this.timer.IsEnabled = true;
+ catch (Exception ex)
+ {
- return;
+ }
}
+ }
- this.currentState = this.nextState;
- this.nextState = null;
-
- if (await RunBranchAsync())
+ public async Task OnProgramVersionAvailable(string? version)
+ {
+ if (version != null)
{
- this.timer.IsEnabled = true;
-
- return;
+ this.VersionText.Text = $"Version: {version}";
}
+ }
- if (await SetupBranchAsync())
- {
- this.timer.IsEnabled = true;
+ public async Task OnFederationKeyMissing()
+ {
+ MessageBox.Show("Federation key does not exist", "Key file missing", MessageBoxButton.OK, MessageBoxImage.Warning, MessageBoxResult.Yes);
+ }
- return;
+ public async Task OnNodeFailedToStart(NodeType nodeType, string? reason = null)
+ {
+ Error($"Cannot start the {nodeType} node, aborting...");
+ if (reason != null)
+ {
+ Error($"Reason: {reason}");
}
+ }
- this.timer.IsEnabled = true;
+ public async Task OnAskForEULA()
+ {
+ return MessageBox.Show("100K collateral is required to operate a Masternode; in addition, a balance of 500.1 CRS is required to fund the registration transaction. Are you happy to proceed?",
+ "End-User License Agreement",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning,
+ MessageBoxResult.No) == MessageBoxResult.Yes;
+ }
+
+ public async Task OnAskForNewFederationKey()
+ {
+ return MessageBox.Show(
+ "Federation key exists. Shall we create a new one?",
+ "Key file already present",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning,
+ MessageBoxResult.No) == MessageBoxResult.Yes;
}
- private async Task RunBranchAsync()
+ public async Task OnShowNewFederationKey(string pubKey, string savePath)
{
- // The 'Run' branch
+ MessageBox.Show($"Your Masternode public key is: {pubKey}");
+ MessageBox.Show($"Your private key has been saved in the root Cirrus data folder:\r\n{savePath}. Please ensure that you keep a backup of this file.");
+ }
- if (this.currentState == "RunMasterNode_KeyPresent")
- {
- if (!this.registrationService.CheckFederationKeyExists())
- {
- MessageBox.Show("Federation key does not exist", "Key file missing", MessageBoxButton.OK, MessageBoxImage.Warning, MessageBoxResult.Yes);
+ public async Task OnAskToRunIfAlreadyMember()
+ {
+ return MessageBox.Show("Your node is already a member of a federation. Do you want to run the Masternode Dashboard instead?",
+ "Member of a federation",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning,
+ MessageBoxResult.No) == MessageBoxResult.Yes;
+ }
- ResetState();
+ public async Task OnAlreadyMember()
+ {
+ Info("Your node is already a member of a federation. Consider using 'Run Masternode' instead.");
+ }
- return true;
- }
+ public async Task OnAskForWalletSource(NodeType nodeType)
+ {
+ var dialog = new CreateRestoreUseExisting();
+ dialog.ShowDialog();
- this.nextState = "Run_StartMainChain";
+ if (dialog.Choice == CreateRestoreUseExisting.ButtonChoice.CreateWallet)
+ {
+ return WalletSource.NewWallet;
}
- if (this.currentState == "Run_StartMainChain")
+ if (dialog.Choice == CreateRestoreUseExisting.ButtonChoice.RestoreWallet)
{
- if (!await this.registrationService.StartNodeAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true))
- {
- Error("Cannot start the Mainchain node, aborting...");
- return false;
- }
-
- this.nextState = "Run_MainChainSynced";
+ return WalletSource.RestoreWallet;
}
- if (this.currentState == "Run_MainChainSynced")
+ if (dialog.Choice == CreateRestoreUseExisting.ButtonChoice.UseExistingWallet)
{
- await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ return WalletSource.UseExistingWallet;
+ }
- await this.registrationService.EnsureMainChainNodeAddressIndexerIsSyncedAsync().ConfigureAwait(true);
+ return null;
+ }
- await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ public async Task OnChooseWallet(List wallets, NodeType nodeType)
+ {
+ var selectionDialog = new WalletSelectionDialog(wallets);
+ selectionDialog.ShowDialog();
- this.nextState = "Run_StartSideChain";
- }
+ return selectionDialog.SelectedWalletName;
+ }
- if (this.currentState == "Run_StartSideChain")
- {
- if (!await this.registrationService.StartNodeAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true))
- {
- Error("Cannot start the Mainchain node, aborting...");
- return false;
- }
+ public async Task OnChooseAddress(List addresses, NodeType nodeType)
+ {
+ var selectionDialog = new AddressSelectionDialog(addresses);
+ selectionDialog.ShowDialog();
- this.nextState = "Run_SideChainSynced";
- }
+ return selectionDialog.SelectedAddress;
+ }
- if (this.currentState == "Run_SideChainSynced")
- {
- await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ public async Task OnWaitingForCollateral()
+ {
+ Log($"Waiting for collateral wallet to have a balance of at least {RegistrationService.CollateralRequirement} STRAX", updateTag: "OnWaitingForCollateral");
+ }
- await this.registrationService.EnsureNodeIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ public async Task OnWaitingForRegistrationFee()
+ {
+ Log("Waiting for registration fee to be sent to the mining wallet...", updateTag: "OnWaitingForRegistrationFee");
+ }
- await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ public async Task OnMissingRegistrationFee(string address)
+ {
+ Error($"Insufficient balance to pay registration fee. Please send 500.1 CRS to the mining wallet on address: {address}");
+ }
- this.nextState = "Run_LaunchBrowser";
- }
+ public async Task OnRegistrationCanceled()
+ {
+ LogError("Registration cancelled.");
+ }
- if (this.currentState == "Run_LaunchBrowser")
- {
- await this.registrationService.StartMasterNodeDashboardAsync().ConfigureAwait(true);
- this.registrationService.LaunchBrowser($"http://localhost:{RegistrationService.DashboardPort}");
+ public async Task OnRegistrationComplete()
+ {
+ Log("Registration complete");
+ }
- ResetState();
+ public async Task OnRegistrationFailed()
+ {
+ Error("Failed to register your masternode, aborting...");
+ }
- return true;
- }
+ public async Task OnAskForMnemonicConfirmation(NodeType nodeType, string mnemonic)
+ {
+ var dialog = new ConfirmationDialog($"Enter mnemonic for the {WalletTypeName(nodeType)} wallet", "Mnemonic", mnemonic, false);
+ dialog.ShowDialog();
+ return dialog.DialogResult == true;
+ }
- return false;
+ public async Task OnAskForUserMnemonic(NodeType nodeType)
+ {
+ var inputBox = new InputBox($"Please enter your mnemonic for the {WalletTypeName(nodeType)} ({nodeType}) wallet", "Mnemonic");
+ return inputBox.ShowDialog();
}
- private async Task SetupBranchAsync()
+ public async Task OnAskForWalletName(NodeType nodeType, bool newWallet)
{
- if (this.currentState == "SetupMasterNode_Eula")
- {
- if (MessageBox.Show("100K collateral is required to operate a Masternode; in addition, a balance of 500.1 CRS is required to fund the registration transaction. Are you happy to proceed?", "End-User License Agreement", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No) != MessageBoxResult.Yes)
- {
- ResetState();
+ var inputBox = new InputBox($"Please enter {nodeType} wallet name:");
+ return inputBox.ShowDialog();
+ }
- return true;
- }
+ public async Task OnAskForPassphrase(NodeType nodeType)
+ {
+ var dialog = new ConfirmationDialog($"Enter passphrase for the {nodeType} wallet",
+ "Passphrase",
+ "",
+ true,
+ allowEmpty: true);
- this.nextState = "Setup_KeyPresent";
- }
+ dialog.ShowDialog();
- if (this.currentState == "Setup_KeyPresent")
+ if (dialog.DialogResult != true)
{
- if (this.registrationService.CheckFederationKeyExists())
- {
- if (MessageBox.Show("Federation key exists. Shall we create a new one?", "Key file already present", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No) == MessageBoxResult.No)
- {
- this.nextState = "Setup_CreateRestoreUseExisting_StartMainChain";
- return true;
- }
-
- this.registrationService.DeleteFederationKey();
- }
-
- this.nextState = "Setup_CreateKey";
+ return null;
}
- if (this.currentState == "Setup_CreateKey")
- {
- string savePath = this.registrationService.CreateFederationKey();
+ return dialog.Text2.Text ?? "";
+ }
- MessageBox.Show($"Your Masternode public key is: {this.registrationService.PubKey}");
- MessageBox.Show($"Your private key has been saved in the root Cirrus data folder:\r\n{savePath}. Please ensure that you keep a backup of this file.");
+ public async Task OnAskForWalletPassword(NodeType nodeType)
+ {
+ var dialog = new ConfirmationDialog(
+ titleText: $"Enter {WalletTypeName(nodeType)} wallet password ({nodeType})",
+ labelText: "Password",
+ firstTextContent: "",
+ firstTextEditable: true);
- this.nextState = "Setup_CreateRestoreUseExisting_StartMainChain";
- }
+ dialog.ShowDialog();
- if (this.currentState == "Setup_CreateRestoreUseExisting_StartMainChain")
+ if (dialog.DialogResult != true)
{
- // All 3 sub-branches of this state require the mainchain and sidechain nodes to be initialized, so do that first.
- if (!await this.registrationService.StartNodeAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true))
- {
- Error("Cannot start the Mainchain node, aborting...");
- ResetState();
-
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_MainChainSynced";
+ return null;
}
- if (this.currentState == "Setup_CreateRestoreUseExisting_MainChainSynced")
- {
- await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ return dialog.Text2.Text ?? string.Empty;
+ }
- await this.registrationService.EnsureMainChainNodeAddressIndexerIsSyncedAsync().ConfigureAwait(true);
+ public async Task OnAskCreatePassword(NodeType nodeType)
+ {
+ var dialog = new ConfirmationDialog(
+ titleText: $"Enter a new {WalletTypeName(nodeType)} wallet password ({nodeType})",
+ labelText: "Password",
+ firstTextContent: "",
+ firstTextEditable: true);
- await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.MainChain, this.registrationService.MainchainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ dialog.ShowDialog();
- this.nextState = "Setup_CreateRestoreUseExisting_StartSideChain";
+ if (dialog.DialogResult != true)
+ {
+ return null;
}
- if (this.currentState == "Setup_CreateRestoreUseExisting_StartSideChain")
- {
- if (!await this.registrationService.StartNodeAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true))
- {
- Error("Cannot start the Sidechain node, aborting...");
- ResetState();
+ return dialog.Text2.Text ?? string.Empty;
+ }
- return true;
- }
+ public async Task OnAskReenterPassword(NodeType nodeType)
+ {
+ return MessageBox.Show("The password you entered is incorrect. Do you want to enter it again?",
+ "Incorrect password",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning,
+ MessageBoxResult.No) == MessageBoxResult.No;
+ }
- this.nextState = "Setup_CreateRestoreUseExisting_SideChainSynced";
- }
+ public async Task OnWalletNameExists(NodeType nodeType)
+ {
+ MessageBox.Show("A wallet with this name already exists", "Error");
+ }
- if (this.currentState == "Setup_CreateRestoreUseExisting_SideChainSynced")
- {
- await this.registrationService.EnsureNodeIsInitializedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ public async Task OnMnemonicIsInvalid(NodeType nodeType)
+ {
+ MessageBox.Show("Please ensure that you enter a valid mnemonic", "Error", MessageBoxButton.OK);
+ }
- await this.registrationService.EnsureNodeIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
+ public async Task OnMnemonicExists(NodeType nodeType)
+ {
+ LogError($"The {WalletTypeName(nodeType)} wallet with this mnemonic already exists.");
+ LogError("Please provide a new mnemonic.");
+ }
- await this.registrationService.EnsureBlockstoreIsSyncedAsync(NodeType.SideChain, this.registrationService.SidechainNetwork.DefaultAPIPort).ConfigureAwait(true);
-
- this.nextState = "Setup_CreateRestoreUseExisting_CheckIsFederationMember";
- }
+ public async Task OnWalletExistsOrInvalid(NodeType nodeType)
+ {
+ MessageBox.Show("A wallet with this name already exists", "Error");
+ }
- if (this.currentState == "Setup_CreateRestoreUseExisting_CheckIsFederationMember")
- {
- if (await this.registrationService.CheckIsFederationMemberAsync().ConfigureAwait(true))
- {
- if (MessageBox.Show("Your node is already a member of a federation. Do you want to run the Masternode Dashboard instead?", "Member of a federation", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No) == MessageBoxResult.Yes)
- {
- this.nextState = "Run_LaunchBrowser";
- return true;
- }
- else
- {
- Info("Your node is already a member of a federation. Consider using 'Run Masternode' instead.");
- ResetState();
- return true;
- }
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_Select")
- {
- var dialog = new CreateRestoreUseExisting();
- dialog.ShowDialog();
-
- if (dialog.Choice == CreateRestoreUseExisting.ButtonChoice.CreateWallet)
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Create";
- }
-
- if (dialog.Choice == CreateRestoreUseExisting.ButtonChoice.RestoreWallet)
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Restore";
- }
-
- if (dialog.Choice == CreateRestoreUseExisting.ButtonChoice.UseExistingWallet)
- {
- this.nextState = "Setup_CreateRestoreUseExisting_UseExisting";
- }
-
- if (dialog.Choice == null)
- {
- LogError("Registration cancelled.");
- ResetState();
- return true;
- }
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_Create")
- {
-
- if (!await HandleCreateWalletsAsync(NodeType.MainChain, createNewMnemonic: true))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_Create_Mining";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_Create_Mining")
- {
-
- if (!await HandleCreateWalletsAsync(NodeType.SideChain, createNewMnemonic: true))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_Create_AskForCollateral";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_Create_AskForCollateral")
- {
- this.CollateralAddress = await HandleAddressSelectionAsync(NodeType.MainChain, collateralWalletName);
-
- if (this.CollateralAddress == null)
- {
- this.CollateralAddress = await this.registrationService.GetFirstWalletAddressAsync(this.registrationService.MainchainNetwork.DefaultAPIPort, this.collateralWalletName).ConfigureAwait(true);
-
- new ShowAddressDialog(NodeType.MainChain, this.CollateralAddress).ShowDialog();
- }
-
- // The 3 sub-branches recombine after this and can share common states.
- this.nextState = "Setup_CreateRestoreUseExisting_CheckForCollateral";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_CheckForCollateral")
- {
- if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.MainchainNetwork.DefaultAPIPort, this.collateralWalletName, RegistrationService.CollateralRequirement).ConfigureAwait(true))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_CheckForRegistrationFee";
- }
- else
- {
- Log($"Waiting for collateral wallet to have a balance of at least {RegistrationService.CollateralRequirement} STRAX", updateTag: this.currentState);
- this.nextState = "Setup_CreateRestoreUseExisting_CheckForCollateral";
- }
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_CheckForRegistrationFee")
- {
- if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletName, RegistrationService.FeeRequirement).ConfigureAwait(true))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_PerformRegistration";
- }
- else
- {
- string? miningAddress = await this.registrationService.GetFirstWalletAddressAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletName).ConfigureAwait(true);
- this.MiningAddress = miningAddress;
- Error($"Insufficient balance to pay registration fee. Please send 500.1 CRS to the mining wallet on address: {miningAddress}");
- this.nextState = "Setup_CreateRestoreUseExisting_WaitForBalance";
- }
- }
-
- // if (this.currentState == "Setup_CreateRestoreUseExisting_PerformCrossChain")
- // {
- // if (MessageBox.Show("Insufficient balance in the mining wallet. Perform a cross-chain transfer of 500.1 STRAX?", "Registration Fee Missing", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No) == MessageBoxResult.No)
- // {
- // this.nextState = "Setup_CreateRestoreUseExisting_WaitForBalance";
- // return true;
- // }
-
- // this.cirrusAddress = await this.registrationService.GetFirstWalletAddressAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletName).ConfigureAwait(true);
-
- // if (await this.registrationService.PerformCrossChainTransferAsync(this.registrationService.MainchainNetwork.DefaultAPIPort, this.collateralWalletName, this.collateralWalletPassword, "500.1", this.cirrusAddress, this.collateralAddress).ConfigureAwait(true))
- // {
- // this.nextState = "Setup_CreateRestoreUseExisting_WaitForCrossChainTransfer";
- // }
- // }
-
- // if (this.currentState == "Setup_CreateRestoreUseExisting_WaitForCrossChainTransfer")
- // {
-
- // if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletName, RegistrationService.FeeRequirement).ConfigureAwait(true))
- // {
- // this.nextState = "Setup_CreateRestoreUseExisting_PerformRegistration";
- // }
- // else
- // {
- // Log("Waiting for registration fee to be sent via cross-chain transfer...", updateTag: this.currentState);
- // await Task.Delay(TimeSpan.FromSeconds(30));
- // this.nextState = "Setup_CreateRestoreUseExisting_WaitForCrossChainTransfer";
- // }
- // }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_WaitForBalance")
- {
-
- if (await this.registrationService.CheckWalletBalanceAsync(this.registrationService.SidechainNetwork.DefaultAPIPort, this.miningWalletName, RegistrationService.FeeRequirement).ConfigureAwait(true))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_PerformRegistration";
- }
- else
- {
- Log("Waiting for registration fee to be sent to the mining wallet...", updateTag: this.currentState);
- this.nextState = "Setup_CreateRestoreUseExisting_WaitForBalance";
- }
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_PerformRegistration")
- {
- bool registeredSuccessfully = await this.registrationService.CallJoinFederationRequestAsync(this.CollateralAddress, this.collateralWalletName, this.collateralWalletPassword, this.miningWalletName, this.miningWalletPassword).ConfigureAwait(true);
- if (!registeredSuccessfully)
- {
- Error("Failed to register your masternode, aborting...");
- ResetState();
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_WaitForRegistration";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_WaitForRegistration")
- {
- if (await this.registrationService.MonitorJoinFederationRequestAsync().ConfigureAwait(true))
- {
- Log("Registration complete");
- this.nextState = "Run_LaunchBrowser";
- }
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_Restore")
- {
- if (!await HandleCreateWalletsAsync(NodeType.MainChain, createNewMnemonic: false))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_Restore_Mining";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_Restore_Mining")
- {
- if (!await HandleCreateWalletsAsync(NodeType.SideChain, createNewMnemonic: false))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_Create_AskForCollateral";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_UseExisting")
- {
- if (!await HandleExistingWalletNameAsync(NodeType.MainChain))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_UseExisting_CollateralPassword";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_UseExisting_CollateralPassword")
- {
- if (!await HandlePasswordAsync(NodeType.MainChain))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_UseExisting_Mining";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_UseExisting_Mining")
- {
- if (!await HandleExistingWalletNameAsync(NodeType.SideChain))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_UseExisting_MiningPassword";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_UseExisting_MiningPassword")
- {
- if (!await HandlePasswordAsync(NodeType.SideChain))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_Select";
- return true;
- }
-
- this.nextState = "Setup_CreateRestoreUseExisting_UseExisting_CheckMainWalletSynced";
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_UseExisting_CheckMainWalletSynced")
- {
- if (await HandleWalletSyncAsync(NodeType.MainChain))
- {
- this.nextState = "Setup_CreateRestoreUseExisting_UseExisting_CheckSideWalletSynced";
- }
- else
- {
- return true;
- }
- }
-
- if (this.currentState == "Setup_CreateRestoreUseExisting_UseExisting_CheckSideWalletSynced")
- {
- if (await HandleWalletSyncAsync(NodeType.SideChain))
- {
- // Now we can jump back into the same sequence as the other 2 sub-branches.
- this.nextState = "Setup_CreateRestoreUseExisting_Create_AskForCollateral";
- }
- else
- {
- return true;
- }
- }
-
- return false;
- }
-
- private async Task HandleAddressSelectionAsync(NodeType nodeType, string walletName)
+ public async Task OnWalletSyncing(NodeType nodeType, int progress)
{
- Network network = nodeType == NodeType.MainChain
- ? this.registrationService.MainchainNetwork
- : this.registrationService.SidechainNetwork;
-
- List? addressesWithBalance = await this.registrationService.GetWalletAddressesAsync(walletName, network.DefaultAPIPort);
-
- if (addressesWithBalance != null)
- {
- var selectionDialog = new AddressSelectionDialog(addressesWithBalance);
- selectionDialog.ShowDialog();
-
- return selectionDialog.SelectedAddress;
- }
-
- return null;
+ Log($"{nodeType} ({WalletTypeName(nodeType)}) wallet is {progress}% synced", updateTag: $"{nodeType}WalletSyncing");
}
- private async Task HandleExistingWalletNameAsync(NodeType nodeType)
+ public async Task OnWalletSynced(NodeType nodeType)
{
- string? walletName;
-
- Network network = nodeType == NodeType.MainChain
- ? this.registrationService.MainchainNetwork
- : this.registrationService.SidechainNetwork;
-
- List? wallesWithBalance = await this.registrationService.GetWalletsWithBalanceAsync(network.DefaultAPIPort);
-
- if (wallesWithBalance != null)
- {
- var selectionDialog = new WalletSelectionDialog(wallesWithBalance);
- selectionDialog.ShowDialog();
-
- if (selectionDialog.SelectedWalletName != null)
- {
- walletName = selectionDialog.SelectedWalletName;
- }
- else
- {
- return false;
- }
- }
- else
- {
- do
- {
- var inputBox = new InputBox($"Please enter your {WalletTypeName(nodeType)} ({nodeType}) wallet name:");
-
- walletName = inputBox.ShowDialog();
-
- if (walletName == null)
- {
- return false;
- }
-
- if (!string.IsNullOrEmpty(walletName))
- {
- try
- {
- if (await this.registrationService.FindWalletByNameAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true))
- {
- break;
- }
- }
- catch
- {
- }
- }
-
- MessageBox.Show($"Please ensure that you enter a valid {WalletTypeName(nodeType)} ({nodeType}) wallet name", "Error", MessageBoxButton.OK);
- } while (true);
- }
-
- if (nodeType == NodeType.MainChain)
- {
- this.collateralWalletName = walletName;
- }
- else
- {
- this.miningWalletName = walletName;
- }
-
- return true;
+ Log($"{nodeType} ({WalletTypeName(nodeType)}) wallet synced successfuly.", updateTag: $"{nodeType}WalletSyncing");
}
- private async Task HandleNewWalletNameAsync(NodeType nodeType)
+ public async Task OnShowWalletName(NodeType nodeType, string walletName)
{
- string? walletName;
- do
- {
- var inputBox = new InputBox($"Please enter new {WalletTypeName(nodeType)} ({nodeType}) wallet name:");
-
- walletName = inputBox.ShowDialog();
-
- if (walletName == null)
- {
- return false;
- }
-
- if (!string.IsNullOrEmpty(walletName))
- {
- try
- {
- Network network = nodeType == NodeType.MainChain
- ? this.registrationService.MainchainNetwork
- : this.registrationService.SidechainNetwork;
-
- if (!await this.registrationService.FindWalletByNameAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true))
- {
- break;
- }
- else
- {
- MessageBox.Show("A wallet with this name already exists", "Error");
- }
- }
- catch
- {
- }
- }
-
- MessageBox.Show($"Please ensure that you enter a valid (and non-existing) {WalletTypeName(nodeType)} ({nodeType}) wallet name", "Error", MessageBoxButton.OK);
- } while (true);
-
if (nodeType == NodeType.MainChain)
{
this.collateralWalletName = walletName;
- }
+ }
else
{
this.miningWalletName = walletName;
}
- return true;
- }
-
- private bool HandleNewMnemonic(NodeType nodeType, bool canChangeMnemonic = false)
- {
- var mnemonic = string.Join(' ', new Mnemonic("English", WordCount.Twelve).Words);
-
- var dialog = new ConfirmationDialog($"Enter mnemonic for the {WalletTypeName(nodeType)} wallet", "Mnemonic", mnemonic, canChangeMnemonic);
- dialog.ShowDialog();
-
- if (dialog.DialogResult != true)
- {
- return false;
- }
-
- if (nodeType == NodeType.MainChain)
- {
- this.collateralWalletMnemonic = mnemonic;
- }
- else
- {
- this.miningWalletMnemonic = mnemonic;
- }
-
- return true;
+ this.UpdateWalletInfoLabel(nodeType);
}
- private bool HandleUserMnemonic(NodeType nodeType)
+ public async Task OnShowWalletAddress(NodeType nodeType, string address)
{
- string? mnemonic;
-
- do
- {
- var inputBox = new InputBox($"Please enter your mnemonic for the {WalletTypeName(nodeType)} ({nodeType}) wallet", "Mnemonic");
-
- mnemonic = inputBox.ShowDialog();
-
- if (mnemonic == null)
- {
- return false;
- }
-
- if (!string.IsNullOrEmpty(mnemonic))
- {
- try
- {
- // Test the mnemonic to ensure validity.
- var temp = new Mnemonic(mnemonic, Wordlist.English);
-
- // If there was no exception, break out of the loop and continue.
- break;
- }
- catch
- {
- }
- }
-
- MessageBox.Show("Please ensure that you enter a valid mnemonic", "Error", MessageBoxButton.OK);
- } while (true);
-
if (nodeType == NodeType.MainChain)
{
- this.collateralWalletMnemonic = mnemonic;
+ this.collateralAddress = address;
}
else
{
- this.miningWalletMnemonic = mnemonic;
+ this.miningAddress = address;
}
- return true;
+ this.UpdateWalletInfoLabel(nodeType);
}
- private bool HandlePassphrase(NodeType nodeType)
+ public async Task OnRestoreWalletFailed(NodeType nodeType)
{
- var dialog = new ConfirmationDialog($"Enter passphrase for the {WalletTypeName(nodeType)} wallet",
- "Passphrase",
- "",
- true,
- allowEmpty: true);
-
- dialog.ShowDialog();
-
- if (dialog.DialogResult != true)
- {
- return false;
- }
-
- string result = dialog.Text2.Text ?? "";
-
- if (nodeType == NodeType.MainChain)
- {
- this.collateralWalletPassphrase = result;
- }
- else
- {
- this.miningWalletPassphrase = result;
- }
-
- return true;
+ LogError($"Can not restore a {WalletTypeName(nodeType)} wallet, aborting...");
}
- private async Task HandlePasswordAsync(NodeType nodeType)
+ public async Task OnCreateWalletFailed(NodeType nodeType)
{
- while (true)
- {
- var dialog = new ConfirmationDialog(
- $"Enter {WalletTypeName(nodeType)} wallet password ({nodeType})",
- "Password",
- "",
- true);
-
- dialog.ShowDialog();
-
- if (dialog.DialogResult != true)
- {
- return false;
- }
-
- string password = dialog.Text2.Text ?? string.Empty;
-
- string walletName;
- if (nodeType == NodeType.MainChain)
- {
- walletName = this.collateralWalletName ?? "";
- }
- else
- {
- walletName = this.miningWalletName ?? "";
- }
-
- if (await this.registrationService.CheckWalletPasswordAsync(NodeApiPort(nodeType), walletName, password) == false)
- {
- if (MessageBox.Show("The password you entered is incorrect. Do you want to enter it again?", "Incorrect password", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No) == MessageBoxResult.No)
- {
- return false;
- }
-
- continue;
- }
-
- if (nodeType == NodeType.MainChain)
- {
- this.collateralWalletPassword = dialog.Text2.Text;
- }
- else
- {
- this.miningWalletPassword = dialog.Text2.Text;
- }
-
- return true;
- }
+ LogError($"Can not create a {WalletTypeName(nodeType)} wallet, aborting...");
}
- private async Task HandleWalletCreationAsync(NodeType nodeType, bool createNewWallet)
+ public async Task OnResyncFailed(NodeType nodeType)
{
- Network network = nodeType == NodeType.MainChain
- ? this.registrationService.MainchainNetwork
- : this.registrationService.SidechainNetwork;
-
- string? walletName = nodeType == NodeType.MainChain
- ? this.collateralWalletName
- : this.miningWalletName;
-
- string? walletMnemonic = nodeType == NodeType.MainChain
- ? this.collateralWalletMnemonic
- : this.miningWalletMnemonic;
-
- string? walletPassphrase = nodeType == NodeType.MainChain
- ? this.collateralWalletPassphrase
- : this.miningWalletPassphrase;
-
- string? walletPassword = nodeType == NodeType.MainChain
- ? this.collateralWalletPassword
- : this.miningWalletPassword;
-
- while (true)
- {
- try
- {
- if (walletName == null
- || walletMnemonic == null
- || walletPassphrase == null
- || walletPassword == null
- || !await this.registrationService.RestoreWalletAsync(network.DefaultAPIPort, nodeType, walletName, walletMnemonic, walletPassphrase, walletPassword, createNewWallet).ConfigureAwait(true))
- {
- string action = createNewWallet ? "create" : "restore";
- LogError($"Cannot {action} {WalletTypeName(nodeType)} wallet, aborting...");
- return false;
- }
- break;
- }
- catch (WalletCollisionException)
- {
- LogError($"The {WalletTypeName(nodeType)} wallet with this mnemonic already exists.");
- LogError("Please provide a new mnemonic.");
-
- if (!HandleNewMnemonic(nodeType, canChangeMnemonic: true))
- {
- LogError("New mnemonic was not provided. Aborting...");
- return false;
- }
- }
- }
-
- if (!await this.registrationService.ResyncWalletAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true))
- {
- LogError($"Cannot resync {WalletTypeName(nodeType)} wallet, aborting...");
- return false;
- }
-
- return true;
+ LogError($"Cannot resync {WalletTypeName(nodeType)} wallet, aborting...");
}
- private async Task HandleWalletSyncAsync(NodeType nodeType)
+ private void UpdateWalletInfoLabel(NodeType nodeType)
{
- string logTag = "HandleWalletSyncAsync" + nodeType;
+ string? name;
+ string? address;
- Network network = nodeType == NodeType.MainChain
- ? this.registrationService.MainchainNetwork
- : this.registrationService.SidechainNetwork;
+ TextBlock target;
- string? walletName = nodeType == NodeType.MainChain
- ? this.collateralWalletName
- : this.miningWalletName;
-
- if (walletName == null)
- {
- throw new ArgumentException("Wallet name can not be null.");
- }
-
- int percentSynced = await this.registrationService.WalletSyncProgressAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true);
- Log($"{nodeType} ({WalletTypeName(nodeType)}) wallet is {percentSynced}% synced", updateTag: logTag);
-
- if (await this.registrationService.IsWalletSyncedAsync(network.DefaultAPIPort, walletName).ConfigureAwait(true))
- {
- Log($"{nodeType} ({WalletTypeName(nodeType)}) wallet synced successfuly.", updateTag: logTag);
- return true;
- }
- else
- {
- return false;
- }
- }
-
- private async Task HandleCreateWalletsAsync(NodeType nodeType, bool createNewMnemonic)
- {
- if (createNewMnemonic)
+ if (nodeType == NodeType.MainChain)
{
- if (!HandleNewMnemonic(nodeType))
- {
- return false;
- }
+ name = this.collateralWalletName;
+ address = this.collateralAddress;
+ target = this.CollateralAddressText;
}
else
{
- if (!HandleUserMnemonic(nodeType))
- {
- return false;
- }
- }
-
- if (!await HandleNewWalletNameAsync(nodeType))
- {
- return false;
- }
-
- if (!HandlePassphrase(nodeType))
- {
- return false;
- }
-
- if (!await HandlePasswordAsync(nodeType))
- {
- return false;
- }
-
- if (!await HandleWalletCreationAsync(nodeType, createNewMnemonic))
- {
- return false;
- }
-
- try
- {
- while (!await HandleWalletSyncAsync(nodeType))
- {
- await Task.Delay(TimeSpan.FromSeconds(1));
- }
- }
- catch
- {
- return false;
- }
-
- return true;
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- Button button = (Button)sender;
-
- if (button == null)
- {
- return;
+ name = this.miningWalletName;
+ address = this.miningAddress;
+ target = this.MiningAddressText;
}
- switch (button.Tag.ToString())
- {
- case "RunMasterNode":
- {
- this.nextState = "RunMasterNode_KeyPresent";
-
- break;
- }
- case "SetupMasterNode":
- {
- this.nextState = "SetupMasterNode_Eula";
-
- break;
- }
- }
- }
-
- private void ResetState()
- {
- this.nextState = null;
- this.currentState = "Begin";
- }
-
- private void LogWithBrush(string message, Brush? brush = null, string? updateTag = null)
- {
- this.statusBar.Dispatcher.Invoke(() =>
- {
- var inline = new Run(message + "\n");
- inline.Tag = updateTag;
-
- if (brush != null)
- {
- inline.Foreground = brush;
- }
-
- InlineCollection inlines = this.statusBar.Inlines;
- Inline lastInline = inlines.LastInline;
-
- if (updateTag != null && lastInline != null && string.Equals(lastInline.Tag, updateTag))
- {
- inlines.Remove(lastInline);
- }
-
- inlines.Add(inline);
- this.logScrollArea.ScrollToBottom();
- });
- }
-
- private void Log(string message, string? updateTag = null)
- {
- this.statusBar.Dispatcher.Invoke(() =>
- {
- LogWithBrush(message, brush: null, updateTag);
- });
- }
-
- private void Log(string message, Color color, string? updateTag = null)
- {
- this.statusBar.Dispatcher.Invoke(() =>
+ string label = name;
+ if (label == null)
{
- LogWithBrush(message, new SolidColorBrush(color), updateTag);
- });
- }
-
- private void LogError(string message)
- {
- Log(message, Color.FromRgb(255, 51, 51));
- }
-
- public void Info(string message, string? updateTag = null)
- {
- Log(message, updateTag: updateTag);
- }
-
- public void Error(string message)
- {
- LogError(message);
- }
-
- public void Error(Exception exception)
- {
- if (this.PrintStacktraces)
+ label = "";
+ } else
{
- LogError($"{exception}");
+ if (address != null)
+ label += $": {address}";
}
- }
- public void Error(string message, Exception exception)
- {
- Error(message);
- Error(exception);
+ target.Text = label;
}
-
- private string WalletTypeName(NodeType nodeType)
- {
- return nodeType == NodeType.MainChain ? "collateral" : "mining";
- }
-
- private int NodeApiPort(NodeType nodeType)
- {
- Network network = nodeType == NodeType.MainChain ? this.registrationService.MainchainNetwork : this.registrationService.SidechainNetwork;
- return network.DefaultAPIPort;
- }
-
- public static string? GetInformationalVersion() =>
- Assembly
- .GetExecutingAssembly()
- ?.GetCustomAttribute()
- ?.InformationalVersion;
}
}
diff --git a/src/MasternodeSetupTool/MasternodeSetupTool.csproj b/src/MasternodeSetupTool/MasternodeSetupTool.csproj
index 3fd0152..8c0bb46 100644
--- a/src/MasternodeSetupTool/MasternodeSetupTool.csproj
+++ b/src/MasternodeSetupTool/MasternodeSetupTool.csproj
@@ -14,21 +14,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+