private static void Sign() { foreach (string target in Targets) { if (target.StartsWith("win", StringComparison.OrdinalIgnoreCase)) { string publishedFolder = Path.Combine(BinDistDirectory, target); Console.WriteLine("Move created .msi"); var msiPath = Path.Combine(WixProjectDirectory, "bin", "Release", "Wasabi.msi"); if (!File.Exists(msiPath)) { throw new Exception(".msi does not exist. Expected path: Wasabi.msi."); } var msiFileName = Path.GetFileNameWithoutExtension(msiPath); var newMsiPath = Path.Combine(BinDistDirectory, $"{msiFileName}-{VersionPrefix}.msi"); File.Copy(msiPath, newMsiPath); Console.Write("Enter Code Signing Certificate Password: "******"cmd", BinDistDirectory, $"signtool sign /d \"Wasabi Wallet\" /f \"{PfxPath}\" /p {pfxPassword} /t http://timestamp.digicert.com /a \"{newMsiPath}\" && exit"); IoHelpers.TryDeleteDirectoryAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } else if (target.StartsWith("osx", StringComparison.OrdinalIgnoreCase)) { string dmgFilePath = Path.Combine(BinDistDirectory, $"Wasabi-{VersionPrefix}.dmg"); if (!File.Exists(dmgFilePath)) { throw new Exception(".dmg does not exist."); } string zipFilePath = Path.Combine(BinDistDirectory, $"Wasabi-osx-{VersionPrefix}.zip"); if (File.Exists(zipFilePath)) { File.Delete(zipFilePath); } } } Console.WriteLine("Signing final files..."); var finalFiles = Directory.GetFiles(BinDistDirectory); foreach (var finalFile in finalFiles) { StartProcessAndWaitForExit("cmd", BinDistDirectory, $"gpg --armor --detach-sign {finalFile} && exit"); StartProcessAndWaitForExit("cmd", WixProjectDirectory, $"git checkout -- ComponentsGenerated.wxs && exit"); } IoHelpers.OpenFolderInFileExplorer(BinDistDirectory); }
internal async Task RunAsync(string walletName, string destinationWalletName, bool keepMixAlive) { try { Logger.LogSoftwareStarted("Wasabi Daemon"); KeyManager keyManager = Global.WalletManager.GetWalletByName(walletName).KeyManager; string password = null; var count = 3; string compatibilityPassword = null; do { if (password is { }) { if (count > 0) { Logger.LogError($"Wrong password. {count} attempts left. Try again."); } else { Logger.LogCritical($"Wrong password. {count} attempts left. Exiting..."); return; } count--; } Console.Write("Password: "); password = PasswordConsole.ReadPassword(); if (PasswordHelper.IsTooLong(password, out password)) { Console.WriteLine(PasswordHelper.PasswordTooLongMessage); } if (PasswordHelper.IsTrimable(password, out password)) { Console.WriteLine(PasswordHelper.TrimWarnMessage); } }while (!PasswordHelper.TryPassword(keyManager, password, out compatibilityPassword)); if (compatibilityPassword is { })
private static void Sign() { foreach (string target in Targets) { if (target.StartsWith("win", StringComparison.OrdinalIgnoreCase)) { string publishedFolder = Path.Combine(BinDistDirectory, target); Console.WriteLine("Move created .msi"); var msiPath = Path.Combine(WixProjectDirectory, @"bin\Release\Wasabi.msi"); if (!File.Exists(msiPath)) { throw new Exception(".msi does not exist. Expected path: Wasabi.msi."); } var msiFileName = Path.GetFileNameWithoutExtension(msiPath); var newMsiPath = Path.Combine(BinDistDirectory, $"{msiFileName}-{VersionPrefix}.msi"); File.Move(msiPath, newMsiPath); Console.Write("Enter Code Signing Certificate Password: "******"cmd", RedirectStandardInput = true, WorkingDirectory = BinDistDirectory })) { process.StandardInput.WriteLine($"signtool sign /d \"Wasabi Wallet\" /f \"{PfxPath}\" /p {pfxPassword} /t http://timestamp.digicert.com /a \"{newMsiPath}\" && exit"); process.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } } Console.WriteLine("Signing final files..."); var finalFiles = Directory.GetFiles(BinDistDirectory); foreach (var finalFile in finalFiles) { using (var process = Process.Start(new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = BinDistDirectory })) { process.StandardInput.WriteLine($"gpg --armor --detach-sign {finalFile} && exit"); process.WaitForExit(); } using (var process = Process.Start(new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = WixProjectDirectory })) { process.StandardInput.WriteLine($"git checkout -- ComponentsGenerated.wxs && exit"); process.WaitForExit(); } } IoHelpers.OpenFolderInFileExplorer(BinDistDirectory); }
internal async Task RunAsync(string walletName, bool mixAll, bool keepMixAlive) { try { Logger.LogSoftwareStarted("Wasabi Daemon"); KeyManager keyManager = TryGetKeyManagerFromWalletName(walletName); if (keyManager is null) { return; } string password = null; var count = 3; string compatibilityPassword = null; do { if (password != null) { if (count > 0) { Logger.LogError($"Wrong password. {count} attempts left. Try again."); } else { Logger.LogCritical($"Wrong password. {count} attempts left. Exiting..."); return; } count--; } Console.Write("Password: "******"Correct password."); await Global.InitializeNoWalletAsync(); if (Global.KillRequested) { return; } await Global.InitializeWalletServiceAsync(keyManager); if (Global.KillRequested) { return; } await TryQueueCoinsToMixAsync(mixAll, password); bool mixing; do { if (Global.KillRequested) { break; } await Task.Delay(3000); if (Global.KillRequested) { break; } bool anyCoinsQueued = Global.ChaumianClient.State.AnyCoinsQueued(); if (!anyCoinsQueued && keepMixAlive) // If no coins queued and mixing is asked to be kept alive then try to queue coins. { await TryQueueCoinsToMixAsync(mixAll, password); } if (Global.KillRequested) { break; } mixing = anyCoinsQueued || keepMixAlive; } while (mixing); if (!Global.KillRequested) // This only has to run if it finishes by itself. Otherwise the Ctrl+c runs it. { await Global.ChaumianClient?.DequeueAllCoinsFromMixAsync("Stopping Wasabi."); } } catch { if (!Global.KillRequested) { throw; } } finally { Logger.LogInfo($"{nameof(Daemon)} stopped."); } }
internal async Task RunAsync(string walletName, string destinationWalletName, bool keepMixAlive) { try { Logger.LogSoftwareStarted("Wasabi Daemon"); KeyManager keyManager = Global.WalletManager.GetWalletByName(walletName).KeyManager; string password = null; var count = 3; string compatibilityPassword = null; do { if (password != null) { if (count > 0) { Logger.LogError($"Wrong password. {count} attempts left. Try again."); } else { Logger.LogCritical($"Wrong password. {count} attempts left. Exiting..."); return; } count--; } Console.Write("Password: "******"Correct password."); await Global.InitializeNoWalletAsync(); if (Global.KillRequested) { return; } Wallet = await Global.WalletManager.StartWalletAsync(keyManager); if (Global.KillRequested) { return; } KeyManager destinationKeyManager = Global.WalletManager.GetWalletByName(destinationWalletName).KeyManager; bool isDifferentDestinationSpecified = keyManager.ExtPubKey != destinationKeyManager.ExtPubKey; if (isDifferentDestinationSpecified) { await Global.WalletManager.StartWalletAsync(destinationKeyManager); } do { if (Global.KillRequested) { break; } // If no coins enqueued then try to enqueue the large anonset coins and mix to another wallet. if (isDifferentDestinationSpecified && !AnyCoinsQueued()) { Wallet.ChaumianClient.DestinationKeyManager = destinationKeyManager; await TryQueueCoinsToMixAsync(password, minAnonset : Wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue()); } if (Global.KillRequested) { break; } // If no coins were enqueued then try to enqueue coins those have less anonset and mix into the same wallet. if (!AnyCoinsQueued()) { Wallet.ChaumianClient.DestinationKeyManager = Wallet.ChaumianClient.KeyManager; await TryQueueCoinsToMixAsync(password, maxAnonset : Wallet.ServiceConfiguration.GetMixUntilAnonymitySetValue() - 1); } if (Global.KillRequested) { break; } await Task.Delay(3000); } // Keep this loop alive as long as a coin is enqueued or keepalive was specified. while (keepMixAlive || AnyCoinsQueued()); await Global.DisposeAsync(); } catch { if (!Global.KillRequested) { throw; } } finally { Logger.LogInfo($"{nameof(Daemon)} stopped."); } }
internal static async Task RunAsync(string walletName, bool mixAll, bool keepMixAlive) { try { Logger.LogStarting("Wasabi Daemon"); KeyManager keyManager = TryGetKeymanagerFromWalletName(walletName); if (keyManager is null) { return; } string password = null; var count = 3; do { if (password != null) { if (count > 0) { Logger.LogError($"Wrong password. {count} attempts left. Try again."); } else { Logger.LogCritical($"Wrong password. {count} attempts left. Exiting..."); return; } count--; } Console.Write("Password: "******"Correct password."); await Global.InitializeNoWalletAsync(); await Global.InitializeWalletServiceAsync(keyManager); await TryQueueCoinsToMixAsync(mixAll, password); bool mixing; do { if (Global.KillRequested) { break; } await Task.Delay(3000); if (Global.KillRequested) { break; } bool anyCoinsQueued = Global.ChaumianClient.State.AnyCoinsQueued(); if (!anyCoinsQueued && keepMixAlive) // If no coins queued and mixing is asked to be kept alive then try to queue coins. { await TryQueueCoinsToMixAsync(mixAll, password); } if (Global.KillRequested) { break; } mixing = anyCoinsQueued || keepMixAlive; } while (mixing); await Global.ChaumianClient.DequeueAllCoinsFromMixAsync(); } finally { Logger.LogInfo($"Wasabi Daemon stopped gracefully.", Logger.InstanceGuid.ToString()); } }
public static void Main(string[] args) { //args = new string[] { "help" }; //args = new string[] { "generate-wallet" }; //args = new string[] { "generate-wallet", "wallet-file=test2.json" }; ////math super cool donate beach mobile sunny web board kingdom bacon crisp ////no password //args = new string[] { "recover-wallet", "wallet-file=test5.json" }; //args = new string[] { "show-balances", "wallet-file=test5.json" }; //args = new string[] { "receive", "wallet-file=test4.json" }; //args = new string[] { "show-history", "wallet-file=test.json" }; //args = new string[] { "send", "btc=0.001", "address=mq6fK8fkFyCy9p53m4Gf4fiX2XCHvcwgi1", "wallet-file=test.json" }; //args = new string[] { "send", "btc=all", "address=mzz63n3n89KVeHQXRqJEVsQX8MZj5zeqCw", "wallet-file=test4.json" }; // Load config file // It also creates it with default settings if doesn't exist Config.Load(); if (args.Length == 0) { DisplayHelp(); Exit(color: ConsoleColor.Green); } var command = args[0]; if (!Commands.Contains(command)) { WriteLine("Wrong command is specified."); DisplayHelp(); } foreach (var arg in args.Skip(1)) { if (!arg.Contains('=')) { Exit($"Wrong argument format specified: {arg}"); } } #region HelpCommand if (command == "help") { AssertArgumentsLenght(args.Length, 1, 1); DisplayHelp(); } #endregion #region GenerateWalletCommand if (command == "generate-wallet") { AssertArgumentsLenght(args.Length, 1, 2); var walletFilePath = GetWalletFilePath(args); AssertWalletNotExists(walletFilePath); string pw; string pwConf; do { // 1. Get password from user WriteLine("Choose a password:"******"Confirm password:"******"Passwords do not match. Try again!"); } } while (pw != pwConf); // 3. Create wallet Mnemonic mnemonic; Safe safe = Safe.Create(out mnemonic, pw, walletFilePath, Config.Network); // If no exception thrown the wallet is successfully created. WriteLine(); WriteLine("Wallet is successfully created."); WriteLine($"Wallet file: {walletFilePath}"); // 4. Display mnemonic WriteLine(); WriteLine("Write down the following mnemonic words."); WriteLine("With the mnemonic words AND your password you can recover this wallet by using the recover-wallet command."); WriteLine(); WriteLine("-------"); WriteLine(mnemonic); WriteLine("-------"); } #endregion #region RecoverWalletCommand if (command == "recover-wallet") { AssertArgumentsLenght(args.Length, 1, 2); var walletFilePath = GetWalletFilePath(args); AssertWalletNotExists(walletFilePath); WriteLine($"Your software is configured using the Bitcoin {Config.Network} network."); WriteLine("Provide your mnemonic words, separated by spaces:"); var mnemonicString = ReadLine(); AssertCorrectMnemonicFormat(mnemonicString); var mnemonic = new Mnemonic(mnemonicString); WriteLine("Provide your password. Please note the wallet cannot check if your password is correct or not. If you provide a wrong password a wallet will be recovered with your provided mnemonic AND password pair:"); var password = PasswordConsole.ReadPassword(); Safe safe = Safe.Recover(mnemonic, password, walletFilePath, Config.Network); // If no exception thrown the wallet is successfully recovered. WriteLine(); WriteLine("Wallet is successfully recovered."); WriteLine($"Wallet file: {walletFilePath}"); } #endregion #region ShowBalancesCommand if (command == "show-balances") { AssertArgumentsLenght(args.Length, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { // 0. Query all operations, grouped by addresses Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7); // 1. Get all address history record with a wrapper class var addressHistoryRecords = new List <AddressHistoryRecord>(); foreach (var elem in operationsPerAddresses) { foreach (var op in elem.Value) { addressHistoryRecords.Add(new AddressHistoryRecord(elem.Key, op)); } } // 2. Calculate wallet balances Money confirmedWalletBalance; Money unconfirmedWalletBalance; GetBalances(addressHistoryRecords, out confirmedWalletBalance, out unconfirmedWalletBalance); // 3. Group all address history records by addresses var addressHistoryRecordsPerAddresses = new Dictionary <BitcoinAddress, HashSet <AddressHistoryRecord> >(); foreach (var address in operationsPerAddresses.Keys) { var recs = new HashSet <AddressHistoryRecord>(); foreach (var record in addressHistoryRecords) { if (record.Address == address) { recs.Add(record); } } addressHistoryRecordsPerAddresses.Add(address, recs); } // 4. Calculate address balances WriteLine(); WriteLine("---------------------------------------------------------------------------"); WriteLine("Address\t\t\t\t\tConfirmed\tUnconfirmed"); WriteLine("---------------------------------------------------------------------------"); foreach (var elem in addressHistoryRecordsPerAddresses) { Money confirmedBalance; Money unconfirmedBalance; GetBalances(elem.Value, out confirmedBalance, out unconfirmedBalance); if (confirmedBalance != Money.Zero || unconfirmedBalance != Money.Zero) { WriteLine($"{elem.Key.ToWif()}\t{confirmedBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}\t\t{unconfirmedBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}"); } } WriteLine("---------------------------------------------------------------------------"); WriteLine($"Confirmed Wallet Balance: {confirmedWalletBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); WriteLine($"Unconfirmed Wallet Balance: {unconfirmedWalletBalance.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); WriteLine("---------------------------------------------------------------------------"); } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion #region ShowHistoryCommand if (command == "show-history") { AssertArgumentsLenght(args.Length, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { // 0. Query all operations, grouped our used safe addresses Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = QueryOperationsPerSafeAddresses(safe); WriteLine(); WriteLine("---------------------------------------------------------------------------"); WriteLine("Date\t\t\tAmount\t\tConfirmed\tTransaction Id"); WriteLine("---------------------------------------------------------------------------"); Dictionary <uint256, List <BalanceOperation> > operationsPerTransactions = GetOperationsPerTransactions(operationsPerAddresses); // 3. Create history records from the transactions // History records is arbitrary data we want to show to the user var txHistoryRecords = new List <Tuple <DateTimeOffset, Money, int, uint256> >(); foreach (var elem in operationsPerTransactions) { var amount = Money.Zero; foreach (var op in elem.Value) { amount += op.Amount; } var firstOp = elem.Value.First(); txHistoryRecords .Add(new Tuple <DateTimeOffset, Money, int, uint256>( firstOp.FirstSeen, amount, firstOp.Confirmations, elem.Key)); } // 4. Order the records by confirmations and time (Simply time does not work, because of a QBitNinja bug) var orderedTxHistoryRecords = txHistoryRecords .OrderByDescending(x => x.Item3) // Confirmations .ThenBy(x => x.Item1); // FirstSeen foreach (var record in orderedTxHistoryRecords) { // Item2 is the Amount if (record.Item2 > 0) { ForegroundColor = ConsoleColor.Green; } else if (record.Item2 < 0) { ForegroundColor = ConsoleColor.Red; } WriteLine($"{record.Item1.DateTime}\t{record.Item2}\t{record.Item3 > 0}\t\t{record.Item4}"); ResetColor(); } } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion #region ReceiveCommand if (command == "receive") { AssertArgumentsLenght(args.Length, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerReceiveAddresses = QueryOperationsPerSafeAddresses(safe, 7, HdPathType.Receive); WriteLine("---------------------------------------------------------------------------"); WriteLine("Unused Receive Addresses"); WriteLine("---------------------------------------------------------------------------"); foreach (var elem in operationsPerReceiveAddresses) { if (elem.Value.Count == 0) { WriteLine($"{elem.Key.ToWif()}"); } } } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion #region SendCommand if (command == "send") { AssertArgumentsLenght(args.Length, 3, 4); var walletFilePath = GetWalletFilePath(args); BitcoinAddress addressToSend; try { addressToSend = BitcoinAddress.Create(GetArgumentValue(args, argName: "address", required: true), Config.Network); } catch (Exception ex) { Exit(ex.ToString()); throw ex; } Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = QueryOperationsPerSafeAddresses(safe, 7); // 1. Gather all the not empty private keys WriteLine("Finding not empty private keys..."); var operationsPerNotEmptyPrivateKeys = new Dictionary <BitcoinExtKey, List <BalanceOperation> >(); foreach (var elem in operationsPerAddresses) { var balance = Money.Zero; foreach (var op in elem.Value) { balance += op.Amount; } if (balance > Money.Zero) { var secret = safe.FindPrivateKey(elem.Key); operationsPerNotEmptyPrivateKeys.Add(secret, elem.Value); } } // 2. Get the script pubkey of the change. WriteLine("Select change address..."); Script changeScriptPubKey = null; Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerChangeAddresses = QueryOperationsPerSafeAddresses(safe, minUnusedKeys: 1, hdPathType: HdPathType.Change); foreach (var elem in operationsPerChangeAddresses) { if (elem.Value.Count == 0) { changeScriptPubKey = safe.FindPrivateKey(elem.Key).ScriptPubKey; } } if (changeScriptPubKey == null) { throw new ArgumentNullException(); } // 3. Gather coins can be spend WriteLine("Gathering unspent coins..."); Dictionary <Coin, bool> unspentCoins = GetUnspentCoins(operationsPerNotEmptyPrivateKeys.Keys); // 4. Get the fee WriteLine("Calculating transaction fee..."); Money fee; try { var txSizeInBytes = 250; using (var client = new HttpClient()) { const string request = @"https://bitcoinfees.21.co/api/v1/fees/recommended"; var result = client.GetAsync(request, HttpCompletionOption.ResponseContentRead).Result; var json = JObject.Parse(result.Content.ReadAsStringAsync().Result); var fastestSatoshiPerByteFee = json.Value <decimal>("fastestFee"); fee = new Money(fastestSatoshiPerByteFee * txSizeInBytes, MoneyUnit.Satoshi); } } catch { Exit("Couldn't calculate transaction fee, try it again later."); throw new Exception("Can't get tx fee"); } WriteLine($"Fee: {fee.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); // 5. How much money we can spend? Money availableAmount = Money.Zero; Money unconfirmedAvailableAmount = Money.Zero; foreach (var elem in unspentCoins) { // If can spend unconfirmed add all if (Config.CanSpendUnconfirmed) { availableAmount += elem.Key.Amount; if (!elem.Value) { unconfirmedAvailableAmount += elem.Key.Amount; } } // else only add confirmed ones else { if (elem.Value) { availableAmount += elem.Key.Amount; } } } // 6. How much to spend? Money amountToSend = null; string amountString = GetArgumentValue(args, argName: "btc", required: true); if (string.Equals(amountString, "all", StringComparison.OrdinalIgnoreCase)) { amountToSend = availableAmount; amountToSend -= fee; } else { amountToSend = ParseBtcString(amountString); } // 7. Do some checks if (amountToSend < Money.Zero || availableAmount < amountToSend + fee) { Exit("Not enough coins."); } decimal feePc = Math.Round((100 * fee.ToDecimal(MoneyUnit.BTC)) / amountToSend.ToDecimal(MoneyUnit.BTC)); if (feePc > 1) { WriteLine(); WriteLine($"The transaction fee is {feePc.ToString("0.#")}% of your transaction amount."); WriteLine($"Sending:\t {amountToSend.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); WriteLine($"Fee:\t\t {fee.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")}btc"); ConsoleKey response = GetYesNoAnswerFromUser(); if (response == ConsoleKey.N) { Exit("User interruption."); } } var confirmedAvailableAmount = availableAmount - unconfirmedAvailableAmount; var totalOutAmount = amountToSend + fee; if (confirmedAvailableAmount < totalOutAmount) { var unconfirmedToSend = totalOutAmount - confirmedAvailableAmount; WriteLine(); WriteLine($"In order to complete this transaction you have to spend {unconfirmedToSend.ToDecimal(MoneyUnit.BTC).ToString("0.#############################")} unconfirmed btc."); ConsoleKey response = GetYesNoAnswerFromUser(); if (response == ConsoleKey.N) { Exit("User interruption."); } } // 8. Select coins WriteLine("Selecting coins..."); var coinsToSpend = new HashSet <Coin>(); var unspentConfirmedCoins = new List <Coin>(); var unspentUnconfirmedCoins = new List <Coin>(); foreach (var elem in unspentCoins) { if (elem.Value) { unspentConfirmedCoins.Add(elem.Key); } else { unspentUnconfirmedCoins.Add(elem.Key); } } bool haveEnough = SelectCoins(ref coinsToSpend, totalOutAmount, unspentConfirmedCoins); if (!haveEnough) { haveEnough = SelectCoins(ref coinsToSpend, totalOutAmount, unspentUnconfirmedCoins); } if (!haveEnough) { throw new Exception("Not enough funds."); } // 9. Get signing keys var signingKeys = new HashSet <ISecret>(); foreach (var coin in coinsToSpend) { foreach (var elem in operationsPerNotEmptyPrivateKeys) { if (elem.Key.ScriptPubKey == coin.ScriptPubKey) { signingKeys.Add(elem.Key); } } } // 10. Build the transaction WriteLine("Signing transaction..."); var builder = new TransactionBuilder(); var tx = builder .AddCoins(coinsToSpend) .AddKeys(signingKeys.ToArray()) .Send(addressToSend, amountToSend) .SetChange(changeScriptPubKey) .SendFees(fee) .BuildTransaction(true); if (!builder.Verify(tx)) { Exit("Couldn't build the transaction."); } WriteLine($"Transaction Id: {tx.GetHash()}"); var qBitClient = new QBitNinjaClient(Config.Network); // QBit's success response is buggy so let's check manually, too BroadcastResponse broadcastResponse; var success = false; var tried = 0; var maxTry = 7; do { tried++; WriteLine($"Try broadcasting transaction... ({tried})"); broadcastResponse = qBitClient.Broadcast(tx).Result; var getTxResp = qBitClient.GetTransaction(tx.GetHash()).Result; if (getTxResp == null) { Thread.Sleep(3000); continue; } else { success = true; break; } } while (tried <= maxTry); if (!success) { if (broadcastResponse.Error != null) { WriteLine($"Error code: {broadcastResponse.Error.ErrorCode} Reason: {broadcastResponse.Error.Reason}"); } Exit($"The transaction might not have been successfully broadcasted. Please check the Transaction ID in a block explorer.", ConsoleColor.Blue); } Exit("Transaction is successfully propagated on the network.", ConsoleColor.Green); } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion Exit(color: ConsoleColor.Green); }
public static async Task <bool> RunAsyncReturnTrueIfContinueWithGuiAsync(string[] args) { var continueWithGui = true; var silent = false; var showHelp = false; var showVersion = false; LogLevel?logLevel = null; string walletName = null; var doMix = false; var mixAll = false; var keepMixAlive = false; try { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Native.AttachParentConsole(); Console.WriteLine(); } var options = new OptionSet() { { "v|version", "Displays Wasabi version and exit.", x => showVersion = x != null }, { "h|help", "Displays help page and exit.", x => showHelp = x != null }, { "s|silent", "Do not log to the standard outputs.", x => silent = x != null }, { "l|loglevel=", "Sets the level of verbosity for the log TRACE|INFO|WARNING|DEBUG|ERROR.", x => { var normalized = x?.ToLower()?.Trim(); if (normalized == "info") { logLevel = LogLevel.Info; } else if (normalized == "warning") { logLevel = LogLevel.Warning; } else if (normalized == "error") { logLevel = LogLevel.Error; } else if (normalized == "trace") { logLevel = LogLevel.Trace; } else if (normalized == "debug") { logLevel = LogLevel.Debug; } else { Console.WriteLine("ERROR: Log level not recognized."); showHelp = true; } } }, { "m|mix", "Start mixing without the GUI with the specified wallet.", x => doMix = x != null }, { "w|wallet=", "The specified wallet file.", x => { walletName = x?.Trim(); } }, { "mixall", "Mix once even if the coin reached the target anonymity set specified in the config file.", x => mixAll = x != null }, { "keepalive", "Don't exit the software after mixing has been finished, rather keep mixing when new money arrives.", x => keepMixAlive = x != null }, }; try { var extras = options.Parse(args); if (extras.Count > 0) { showHelp = true; } } catch (OptionException) { continueWithGui = false; Console.WriteLine("Option not recognized."); Console.WriteLine(); ShowHelp(options); return(continueWithGui); } if (showHelp) { continueWithGui = false; ShowHelp(options); return(continueWithGui); } else if (showVersion) { continueWithGui = false; ShowVersion(); return(continueWithGui); } } finally { if (silent) { Native.DettachParentConsole(); } } Logger.InitializeDefaults(Path.Combine(Global.DataDir, "Logs.txt")); if (logLevel.HasValue) { Logger.SetMinimumLevel(logLevel.Value); } if (silent) { Logger.Modes.Remove(LogMode.Console); Logger.Modes.Remove(LogMode.Debug); } else { Logger.Modes.Add(LogMode.Console); Logger.Modes.Add(LogMode.Debug); } Logger.LogStarting("Wasabi"); KeyManager keyManager = null; if (walletName != null) { continueWithGui = false; var walletFullPath = Global.GetWalletFullPath(walletName); var walletBackupFullPath = Global.GetWalletBackupFullPath(walletName); if (!File.Exists(walletFullPath) && !File.Exists(walletBackupFullPath)) { // The selected wallet is not available any more (someone deleted it?). Logger.LogCritical("The selected wallet doesn't exsist, did you delete it?", nameof(Daemon)); return(continueWithGui); } try { keyManager = Global.LoadKeyManager(walletFullPath, walletBackupFullPath); } catch (Exception ex) { Logger.LogCritical(ex, nameof(Daemon)); return(continueWithGui); } } if (doMix) { continueWithGui = false; if (keyManager is null) { Logger.LogCritical("Wallet was not supplied. Add --wallet {WalletName}", nameof(Daemon)); return(continueWithGui); } string password = null; var count = 3; do { if (password != null) { if (count > 0) { Logger.LogError($"Wrong password. {count} attempts left. Try again."); } else { Logger.LogCritical($"Wrong password. {count} attempts left. Exiting..."); return(continueWithGui); } count--; } Console.Write("Password: "******"Correct password."); await Global.InitializeNoWalletAsync(); await Global.InitializeWalletServiceAsync(keyManager); await TryQueueCoinsToMixAsync(mixAll, password); var mixing = true; do { if (Global.KillRequested) { break; } await Task.Delay(3000); if (Global.KillRequested) { break; } bool anyCoinsQueued = Global.ChaumianClient.State.AnyCoinsQueued(); if (!anyCoinsQueued && keepMixAlive) // If no coins queued and mixing is asked to be kept alive then try to queue coins. { await TryQueueCoinsToMixAsync(mixAll, password); } if (Global.KillRequested) { break; } mixing = anyCoinsQueued || keepMixAlive; } while (mixing); await Global.ChaumianClient.DequeueAllCoinsFromMixAsync(); } return(continueWithGui); }
private static async Task MainAsync(IReadOnlyList <string> args) { //args = new string[] { "help" }; //args = new string[] { "generate-wallet" }; //args = new string[] { "generate-wallet", "wallet-file=test2.json" }; ////math super cool donate beach mobile sunny web board kingdom bacon crisp ////no password //args = new string[] { "recover-wallet", "wallet-file=test5.json" }; //args = new string[] { "show-balances"}; //args = new string[] { "receive" }; //args = new string[] { "send","btc=1", "address=mqjVoPiXtLdBdxdqQzWvFSMSBv93swPUUH", "wallet-file=MoliWallet.json" }; //args = new string[] { "send", "btc=0.1", "address=mkpC5HFC8QHbJbuwajYLDkwPoqcftMU1ga" }; //args = new string[] { "send", "btc=all", "address=mzz63n3n89KVeHQXRqJEVsQX8MZj5zeqCw", "wallet-file=test4.json" }; // Load config file // It also creates it with default settings if doesn't exist Config.Load(); // Configure QBitNinjaClient _qBitClient = new QBitNinjaClient(Config.Network); _httpClient = new HttpClient(); if (Config.UseTor) { var torHandler = new SocksPortHandler(Config.TorHost, Config.TorSocksPort, ignoreSslCertification: true); // ignoreSslCertification needed for linux, until QBit or DotNetTor fixes its issues _qBitClient.SetHttpMessageHandler(torHandler); _httpClient = new HttpClient(torHandler); } if (args.Count == 0) { DisplayHelp(); Exit(color: ConsoleColor.Green); } var command = args[0]; if (!Commands.Contains(command)) { WriteLine("Wrong command is specified."); DisplayHelp(); } foreach (var arg in args.Skip(1).Where(arg => !arg.Contains('='))) { Exit($"Wrong argument format specified: {arg}"); } #region HelpCommand if (command == "help") { AssertArgumentsLenght(args.Count, 1, 1); DisplayHelp(); } #endregion HelpCommand #region GenerateWalletCommand if (command == "generate-wallet") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); AssertWalletNotExists(walletFilePath); string pw; string pwConf; do { // 1. Get password from user WriteLine("Choose a password:"******"Confirm password:"******"Passwords do not match. Try again!"); } } while (pw != pwConf); // 3. Create wallet string mnemonic; Safe.Create(out mnemonic, pw, walletFilePath, Config.Network); // If no exception thrown the wallet is successfully created. WriteLine(); WriteLine("Wallet is successfully created."); WriteLine($"Wallet file: {walletFilePath}"); // 4. Display mnemonic WriteLine(); WriteLine("Write down the following mnemonic words."); WriteLine("With the mnemonic words AND your password you can recover this wallet by using the recover-wallet command."); WriteLine(); WriteLine("-------"); WriteLine(mnemonic); WriteLine("-------"); } #endregion GenerateWalletCommand #region RecoverWalletCommand if (command == "recover-wallet") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); AssertWalletNotExists(walletFilePath); WriteLine($"Your software is configured using the Bitcoin {Config.Network} network."); WriteLine("Provide your mnemonic words, separated by spaces:"); var mnemonic = ReadLine(); AssertCorrectMnemonicFormat(mnemonic); WriteLine("Provide your password. Please note the wallet cannot check if your password is correct or not. If you provide a wrong password a wallet will be recovered with your provided mnemonic AND password pair:"); var password = PasswordConsole.ReadPassword(); Safe.Recover(mnemonic, password, walletFilePath, Config.Network); // If no exception thrown the wallet is successfully recovered. WriteLine(); WriteLine("Wallet is successfully recovered."); WriteLine($"Wallet file: {walletFilePath}"); } #endregion RecoverWalletCommand #region ShowBalancesCommand if (command == "show-balances") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { await AssertCorrectQBitBlockHeightAsync().ConfigureAwait(false); // 0. Query all operations, grouped by addresses Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = await QueryOperationsPerSafeAddressesAsync(_qBitClient, safe, MinUnusedKeyNum).ConfigureAwait(false); // 1. Get all address history record with a wrapper class var addressHistoryRecords = new List <AddressHistoryRecord>(); foreach (var elem in operationsPerAddresses) { foreach (BalanceOperation op in elem.Value) { addressHistoryRecords.Add(new AddressHistoryRecord(elem.Key, op)); } } // 2. Calculate wallet balances Money confirmedWalletBalance; Money unconfirmedWalletBalance; GetBalances(addressHistoryRecords, out confirmedWalletBalance, out unconfirmedWalletBalance); // 3. Group all address history records by addresses var addressHistoryRecordsPerAddresses = new Dictionary <BitcoinAddress, HashSet <AddressHistoryRecord> >(); foreach (BitcoinAddress address in operationsPerAddresses.Keys) { var recs = new HashSet <AddressHistoryRecord>(); foreach (AddressHistoryRecord record in addressHistoryRecords) { if (record.Address == address) { recs.Add(record); } } addressHistoryRecordsPerAddresses.Add(address, recs); } // 4. Calculate address balances WriteLine(); WriteLine("---------------------------------------------------------------------------"); WriteLine(@"Address Confirmed Unconfirmed"); WriteLine("---------------------------------------------------------------------------"); foreach (var elem in addressHistoryRecordsPerAddresses) { Money confirmedBalance; Money unconfirmedBalance; GetBalances(elem.Value, out confirmedBalance, out unconfirmedBalance); if (confirmedBalance != Money.Zero || unconfirmedBalance != Money.Zero) { WriteLine($@"{elem.Key.ToWif()} {confirmedBalance.ToDecimal(MoneyUnit.BTC):0.#############################} {unconfirmedBalance.ToDecimal(MoneyUnit.BTC):0.#############################}"); } } WriteLine("---------------------------------------------------------------------------"); WriteLine($"Confirmed Wallet Balance: {confirmedWalletBalance.ToDecimal(MoneyUnit.BTC):0.#############################}btc"); WriteLine($"Unconfirmed Wallet Balance: {unconfirmedWalletBalance.ToDecimal(MoneyUnit.BTC):0.#############################}btc"); WriteLine("---------------------------------------------------------------------------"); } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion ShowBalancesCommand #region ShowHistoryCommand if (command == "show-history") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { await AssertCorrectQBitBlockHeightAsync().ConfigureAwait(false); // 0. Query all operations, grouped our used safe addresses Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = await QueryOperationsPerSafeAddressesAsync(_qBitClient, safe, MinUnusedKeyNum).ConfigureAwait(false); WriteLine(); WriteLine("---------------------------------------------------------------------------"); WriteLine(@"Date Amount Confirmed Transaction Id"); WriteLine("---------------------------------------------------------------------------"); Dictionary <uint256, List <BalanceOperation> > operationsPerTransactions = GetOperationsPerTransactions(operationsPerAddresses); // 3. Create history records from the transactions // History records is arbitrary data we want to show to the user var txHistoryRecords = new List <Tuple <DateTimeOffset, Money, int, uint256> >(); foreach (var elem in operationsPerTransactions) { var amount = Money.Zero; foreach (var op in elem.Value) { amount += op.Amount; } var firstOp = elem.Value.First(); txHistoryRecords .Add(new Tuple <DateTimeOffset, Money, int, uint256>( firstOp.FirstSeen, amount, firstOp.Confirmations, elem.Key)); } // 4. Order the records by confirmations and time (Simply time does not work, because of a QBitNinja issue) var orderedTxHistoryRecords = txHistoryRecords .OrderByDescending(x => x.Item3) // Confirmations .ThenBy(x => x.Item1); // FirstSeen foreach (var record in orderedTxHistoryRecords) { // Item2 is the Amount if (record.Item2 > 0) { ForegroundColor = ConsoleColor.Green; } else if (record.Item2 < 0) { ForegroundColor = ConsoleColor.DarkGreen; } WriteLine($@"{record.Item1.DateTime} {record.Item2} {record.Item3 > 0} {record.Item4}"); ResetColor(); } } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion ShowHistoryCommand #region ShowExtKeys if (command == "show-extkey") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); WriteLine($"ExtKey: {safe.BitcoinExtKey}"); WriteLine($"Network: {safe.Network}"); } if (command == "show-extpubkey") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); WriteLine($"ExtPubKey: {safe.BitcoinExtPubKey}"); WriteLine($"Network: {safe.Network}"); } #endregion ShowExtKeys #region ReceiveCommand if (command == "receive") { AssertArgumentsLenght(args.Count, 1, 2); var walletFilePath = GetWalletFilePath(args); Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { await AssertCorrectQBitBlockHeightAsync().ConfigureAwait(false); Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerReceiveAddresses = await QueryOperationsPerSafeAddressesAsync(_qBitClient, safe, 7, Safe.HdPathType.Receive).ConfigureAwait(false); WriteLine("---------------------------------------------------------------------------"); WriteLine("Unused Receive Addresses"); WriteLine("---------------------------------------------------------------------------"); foreach (var elem in operationsPerReceiveAddresses) { if (elem.Value.Count == 0) { WriteLine($"{elem.Key.ToWif()}"); } } } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion ReceiveCommand #region SendCommand if (command == "send") { await AssertCorrectQBitBlockHeightAsync().ConfigureAwait(false); AssertArgumentsLenght(args.Count, 3, 4); var walletFilePath = GetWalletFilePath(args); BitcoinAddress addressToSend; try { addressToSend = BitcoinAddress.Create(GetArgumentValue(args, argName: "address", required: true), Config.Network); } catch (Exception ex) { Exit(ex.ToString()); throw; } Safe safe = DecryptWalletByAskingForPassword(walletFilePath); if (Config.ConnectionType == ConnectionType.Http) { Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerAddresses = await QueryOperationsPerSafeAddressesAsync(_qBitClient, safe, MinUnusedKeyNum).ConfigureAwait(false); // 1. Gather all the not empty private keys WriteLine("Finding not empty private keys..."); var operationsPerNotEmptyPrivateKeys = new Dictionary <BitcoinExtKey, List <BalanceOperation> >(); foreach (var elem in operationsPerAddresses) { var balance = Money.Zero; foreach (var op in elem.Value) { balance += op.Amount; } if (balance > Money.Zero) { var secret = safe.FindPrivateKey(elem.Key); operationsPerNotEmptyPrivateKeys.Add(secret, elem.Value); } } // 2. Get the script pubkey of the change. WriteLine("Select change address..."); Script changeScriptPubKey = null; Dictionary <BitcoinAddress, List <BalanceOperation> > operationsPerChangeAddresses = await QueryOperationsPerSafeAddressesAsync(_qBitClient, safe, minUnusedKeys : 1, hdPathType : Safe.HdPathType.Change).ConfigureAwait(false); foreach (var elem in operationsPerChangeAddresses) { if (elem.Value.Count == 0) { changeScriptPubKey = safe.FindPrivateKey(elem.Key).ScriptPubKey; } } if (changeScriptPubKey == null) { throw new ArgumentNullException(); } // 3. Gather coins can be spend WriteLine("Gathering unspent coins..."); Dictionary <Coin, bool> unspentCoins = await GetUnspentCoinsAsync(operationsPerNotEmptyPrivateKeys.Keys, _qBitClient).ConfigureAwait(false); // 4. How much money we can spend? var availableAmount = Money.Zero; var unconfirmedAvailableAmount = Money.Zero; foreach (var elem in unspentCoins) { // If can spend unconfirmed add all if (Config.CanSpendUnconfirmed) { availableAmount += elem.Key.Amount; if (!elem.Value) { unconfirmedAvailableAmount += elem.Key.Amount; } } // else only add confirmed ones else { if (elem.Value) { availableAmount += elem.Key.Amount; } } } // 5. Get and calculate fee WriteLine("Calculating dynamic transaction fee..."); Money feePerBytes = null; try { feePerBytes = await QueryFeePerBytesAsync().ConfigureAwait(false); } catch (Exception ex) { WriteLine(ex.Message); Exit("Couldn't calculate transaction fee, try it again later."); } int inNum; string amountString = GetArgumentValue(args, argName: "btc", required: true); if (string.Equals(amountString, "all", StringComparison.OrdinalIgnoreCase)) { inNum = unspentCoins.Count; } else { const int expectedMinTxSize = 1 * 148 + 2 * 34 + 10 - 1; inNum = SelectCoinsToSpend(unspentCoins, ParseBtcString(amountString) + feePerBytes * expectedMinTxSize).Count; } const int outNum = 2; // 1 address to send + 1 for change var estimatedTxSize = inNum * 148 + outNum * 34 + 10 + inNum; // http://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending WriteLine($"Estimated tx size: {estimatedTxSize} bytes"); Money fee = feePerBytes * estimatedTxSize; WriteLine($"Fee: {fee.ToDecimal(MoneyUnit.BTC):0.#############################}btc"); // 6. How much to spend? Money amountToSend = null; if (string.Equals(amountString, "all", StringComparison.OrdinalIgnoreCase)) { amountToSend = availableAmount; amountToSend -= fee; } else { amountToSend = ParseBtcString(amountString); } // 7. Do some checks if (amountToSend < Money.Zero || availableAmount < amountToSend + fee) { Exit("Not enough coins."); } decimal feePc = Math.Round((100 * fee.ToDecimal(MoneyUnit.BTC)) / amountToSend.ToDecimal(MoneyUnit.BTC)); if (feePc > 1) { WriteLine(); WriteLine($"The transaction fee is {feePc:0.#}% of your transaction amount."); WriteLine($"Sending:\t {amountToSend.ToDecimal(MoneyUnit.BTC):0.#############################}btc"); WriteLine($"Fee:\t\t {fee.ToDecimal(MoneyUnit.BTC):0.#############################}btc"); ConsoleKey response = GetYesNoAnswerFromUser(); if (response == ConsoleKey.N) { Exit("User interruption."); } } var confirmedAvailableAmount = availableAmount - unconfirmedAvailableAmount; var totalOutAmount = amountToSend + fee; if (confirmedAvailableAmount < totalOutAmount) { var unconfirmedToSend = totalOutAmount - confirmedAvailableAmount; WriteLine(); WriteLine($"In order to complete this transaction you have to spend {unconfirmedToSend.ToDecimal(MoneyUnit.BTC):0.#############################} unconfirmed btc."); ConsoleKey response = GetYesNoAnswerFromUser(); if (response == ConsoleKey.N) { Exit("User interruption."); } } // 8. Select coins WriteLine("Selecting coins..."); HashSet <Coin> coinsToSpend = SelectCoinsToSpend(unspentCoins, totalOutAmount); // 9. Get signing keys var signingKeys = new HashSet <ISecret>(); foreach (var coin in coinsToSpend) { foreach (var elem in operationsPerNotEmptyPrivateKeys) { if (elem.Key.ScriptPubKey == coin.ScriptPubKey) { signingKeys.Add(elem.Key); } } } // 10. Build the transaction WriteLine("Signing transaction..."); var builder = new TransactionBuilder(); var tx = builder .AddCoins(coinsToSpend) .AddKeys(signingKeys.ToArray()) .Send(addressToSend, amountToSend) .SetChange(changeScriptPubKey) .SendFees(fee) .BuildTransaction(true); if (!builder.Verify(tx)) { Exit("Couldn't build the transaction."); } WriteLine($"Transaction Id: {tx.GetHash()}"); // QBit's success response is buggy so let's check manually, too BroadcastResponse broadcastResponse; var success = false; var tried = 0; const int maxTry = 7; do { tried++; WriteLine($"Try broadcasting transaction... ({tried})"); broadcastResponse = await _qBitClient.Broadcast(tx).ConfigureAwait(false); var getTxResp = await _qBitClient.GetTransaction(tx.GetHash()).ConfigureAwait(false); if (getTxResp != null) { success = true; break; } else { await Task.Delay(3000).ConfigureAwait(false); } } while (tried < maxTry); if (!success) { if (broadcastResponse.Error != null) { // Try broadcasting with smartbit if QBit fails (QBit issue) if (broadcastResponse.Error.ErrorCode == NBitcoin.Protocol.RejectCode.INVALID && broadcastResponse.Error.Reason == "Unknown") { WriteLine("Try broadcasting transaction with smartbit..."); var post = "https://testnet-api.smartbit.com.au/v1/blockchain/pushtx"; if (Config.Network == Network.Main) { post = "https://api.smartbit.com.au/v1/blockchain/pushtx"; } var content = new StringContent(new JObject(new JProperty("hex", tx.ToHex())).ToString(), Encoding.UTF8, "application/json"); var resp = await _httpClient.PostAsync(post, content).ConfigureAwait(false); var json = JObject.Parse(await resp.Content.ReadAsStringAsync().ConfigureAwait(false)); if (json.Value <bool>("success")) { Exit("Transaction is successfully propagated on the network.", ConsoleColor.Green); } else { WriteLine($"Error code: {json["error"].Value<string>("code")} Reason: {json["error"].Value<string>("message")}"); } } else { WriteLine($"Error code: {broadcastResponse.Error.ErrorCode} Reason: {broadcastResponse.Error.Reason}"); } } Exit("The transaction might not have been successfully broadcasted. Please check the Transaction ID in a block explorer.", ConsoleColor.Blue); } Exit("Transaction is successfully propagated on the network.", ConsoleColor.Green); } else if (Config.ConnectionType == ConnectionType.FullNode) { throw new NotImplementedException(); } else { Exit("Invalid connection type."); } } #endregion SendCommand Exit(color: ConsoleColor.Green); }