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 @@ - - - - - - - - - - - - - - - +