Prepare BuildTransactionAsync
MODIFY
MagicalCryptoWallet.Tests/RegTests.cs
MagicalCryptoWallet.Tests/RegTests.cs
@@ -246,7 +246,7 @@ namespace MagicalCryptoWallet.Tests
// Test filter block hashes are correct.
var filters = downloader.GetFiltersIncluding(Network.RegTest.GenesisHash).ToArray();
- for (int i = 0; i < 111; i++)
+ for (int i = 0; i < 101; i++)
{
var expectedHash = await Global.RpcClient.GetBlockHashAsync(i);
var filter = filters[i];
@@ -632,6 +632,77 @@ namespace MagicalCryptoWallet.Tests
}
+ [Fact]
+ public async Task SendingTestsAsync()
+ {
+ // Make sure fitlers are created on the server side.
+ await AssertFiltersInitializedAsync();
+
+ var network = Global.RpcClient.Network;
+
+ // Create the services.
+ // 1. Create connection service.
+ var nodes = new NodesGroup(Global.Config.Network,
+ requirements: new NodeRequirement
+ {
+ RequiredServices = NodeServices.Network,
+ MinVersion = ProtocolVersion.WITNESS_VERSION
+ });
+ nodes.ConnectedNodes.Add(Fixture.BackendRegTestNode.CreateNodeClient());
+
+ // 2. Create mempool service.
+ var memPoolService = new MemPoolService();
+ Node node = Fixture.BackendRegTestNode.CreateNodeClient();
+ node.Behaviors.Add(new MemPoolBehavior(memPoolService));
+
+ // 3. Create index downloader service.
+ var indexFilePath = Path.Combine(SharedFixture.DataDir, nameof(SendingTestsAsync), $"Index{Global.RpcClient.Network}.dat");
+ var indexDownloader = new IndexDownloader(Global.RpcClient.Network, indexFilePath, new Uri(Fixture.BackendEndPoint));
+
+ // 4. Create key manager service.
+ var keyManager = KeyManager.CreateNew(out Mnemonic mnemonic, "password");
+
+ // 5. Create wallet service.
+ var blocksFolderPath = Path.Combine(SharedFixture.DataDir, nameof(SendingTestsAsync), $"Blocks");
+ var wallet = new WalletService(keyManager, indexDownloader, memPoolService, nodes, blocksFolderPath);
+ wallet.NewFilterProcessed += IncrementFilterProcessed;
+
+ // Get some money, make it confirm.
+ var key = wallet.GetReceiveKey("foo label");
+ var txid = await Global.RpcClient.SendToAddressAsync(key.GetP2wpkhAddress(network), new Money(0.1m, MoneyUnit.BTC));
+ await Global.RpcClient.GenerateAsync(1);
+
+ try
+ {
+ nodes.Connect(); // Start connection service.
+ node.VersionHandshake(); // Start mempool service.
+ indexDownloader.Synchronize(requestInterval: TimeSpan.FromSeconds(3)); // Start index downloader service.
+
+ // Wait until the filter our previous transaction is present.
+ await WaitForIndexesToSyncAsync(TimeSpan.FromSeconds(30), indexDownloader, true);
+
+ using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
+ {
+ await wallet.InitializeAsync(cts.Token); // Initialize wallet service.
+ }
+
+ await wallet.BuildTransactionAsync("", new [] { (new Script(), Money.Zero) }, 3, false);
+ }
+ finally
+ {
+ wallet.NewFilterProcessed -= IncrementFilterProcessed;
+ wallet?.Dispose();
+ // Dispose index downloader service.
+ if (indexDownloader != null)
+ {
+ await indexDownloader.StopAsync();
+ }
+
+ // Dispose connection service.
+ nodes?.Dispose();
+ }
+ }
+
#endregion
}
}

MODIFY
MagicalCryptoWallet/Backend/Models/FeeEstimationPair.cs
MagicalCryptoWallet/Backend/Models/FeeEstimationPair.cs
@@ -6,7 +6,7 @@ using System.Text;
namespace MagicalCryptoWallet.Backend.Models
{
/// <summary>
- /// satoshi per bytes
+ /// Satoshi per bytes.
/// </summary>
public class FeeEstimationPair
{

MODIFY
MagicalCryptoWallet/Models/Height.cs
MagicalCryptoWallet/Models/Height.cs
@@ -24,6 +24,8 @@ namespace MagicalCryptoWallet.Models
public static Height MemPool => new Height(HeightType.MemPool);
public static Height Unknown => new Height(HeightType.Unknown);
+ public bool IsConfirmed => this != MemPool && this != Unknown;
+
public Height(int height)
{
if (height < 0) throw new ArgumentException($"{nameof(height)} : {height} cannot be < 0");

MODIFY
MagicalCryptoWallet/Services/WalletService.cs
MagicalCryptoWallet/Services/WalletService.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net;
+using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -11,6 +13,7 @@ using MagicalCryptoWallet.Helpers;
using MagicalCryptoWallet.KeyManagement;
using MagicalCryptoWallet.Logging;
using MagicalCryptoWallet.Models;
+using MagicalCryptoWallet.TorSocks5;
using NBitcoin;
using NBitcoin.Protocol;
using Nito.AsyncEx;
@@ -442,6 +445,132 @@ namespace MagicalCryptoWallet.Services
}
}
+ /// <param name="toSend">If Money.Zero then spend all available amount. Don't generate change.</param>
+ /// <param name="allowUnconfirmed">Allow to spend unconfirmed transactions, if necessary.</param>
+ /// <param name="allowedInputs">Only these inputs allowed to be used to build the transaction. The wallet must know the corresponding private keys.</param>
+ /// <param name="subtractFeeFromAmountIndex">If null, fee is substracted from the change. Otherwise it denotes the index in the toSend array.</param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public async Task BuildTransactionAsync(string password, IEnumerable<(Script script, Money money)> toSend, int feeTarget, bool allowUnconfirmed = false, int? subtractFeeFromAmountIndex = null, Script customChange = null, IEnumerable<TxoRef> allowedInputs = null)
+ {
+ password = password ?? ""; // Correction.
+ toSend = Guard.NotNullOrEmpty(nameof(toSend), toSend);
+ int sendAllCount = toSend.Count(x => x.money == Money.Zero);
+ if (sendAllCount > 1)
+ {
+ throw new ArgumentException($"Only one {nameof(toSend)} element can contain Money.Zero. Money.Zero means add the change to the value of this output.");
+ }
+ if (sendAllCount == 1 && customChange != null)
+ {
+ throw new ArgumentException($"{nameof(customChange)} and send all to destination cannot be specified the same time.");
+ }
+ Guard.InRangeAndNotNull(nameof(feeTarget), feeTarget, 0, 1008); // Allow 0 and 1, and correct later.
+ if (feeTarget < 2) // Correct 0 and 1 to 2.
+ {
+ feeTarget = 2;
+ }
+ if (subtractFeeFromAmountIndex != null) // If not null, make sure not out of range. If null fee is substracted from the change.
+ {
+ if (subtractFeeFromAmountIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(subtractFeeFromAmountIndex)} cannot be smaller than 0.");
+ }
+ if (subtractFeeFromAmountIndex > toSend.Count() - 1)
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(subtractFeeFromAmountIndex)} can be maximum {nameof(toSend)}.Count() - 1. {nameof(subtractFeeFromAmountIndex)}: {subtractFeeFromAmountIndex}, {nameof(toSend)}.Count() - 1: {toSend.Count() - 1}.");
+ }
+ }
+
+ // Get allowed coins to spend.
+ List<SmartCoin> allowedSmartCoinInputs; // Inputs those can be used to build the transaction.
+ if (allowedInputs != null && allowedInputs.Count() != 0) // If allowedInputs are specified then select the coins from them.
+ {
+ if (allowUnconfirmed)
+ {
+ allowedSmartCoinInputs = Coins.Where(x => allowedInputs.Count(y => y.TransactionId == x.TransactionId && y.Index == x.Index) > 0).ToList();
+ }
+ else
+ {
+ allowedSmartCoinInputs = Coins.Where(x => x.Height.IsConfirmed && allowedInputs.Count(y => y.TransactionId == x.TransactionId && y.Index == x.Index) > 0).ToList();
+ }
+ }
+ else
+ {
+ if (allowUnconfirmed)
+ {
+ allowedSmartCoinInputs = Coins.ToList();
+ }
+ else
+ {
+ allowedSmartCoinInputs = Coins.Where(x => x.Height.IsConfirmed).ToList();
+ }
+ }
+
+ // 1. Get the possible changes.
+ List<Script> allowedChanges;
+ if (customChange == null)
+ {
+ AssertCleanKeysIndexed(21, true);
+ allowedChanges = KeyManager.GetKeys(KeyState.Clean, true).Select(x => x.GetP2wpkhScript()).ToList();
+ }
+ else
+ {
+ allowedChanges = new List<Script> { customChange };
+ }
+
+ // 2. Find all coins I can spend from the account
+ // 3. How much money we can spend?
+ Money spendableConfirmedAmount = Coins.Where(x => x.Height.IsConfirmed).Sum(x => x.Amount);
+ Logger.LogInfo<WalletService>($"Spendable confirmed amount: {spendableConfirmedAmount}.");
+ Money spendableUnconfirmedAmount;
+ if (allowUnconfirmed)
+ {
+ spendableUnconfirmedAmount = Coins.Sum(x => x.Amount);
+ Logger.LogInfo<WalletService>($"Spendable unconfirmed amount: {spendableUnconfirmedAmount}.");
+ }
+ else
+ {
+ spendableUnconfirmedAmount = Money.Zero;
+ }
+
+ // 4. Get and calculate fee
+ Logger.LogInfo<WalletService>("Calculating dynamic transaction fee...");
+ Money feePerBytes = null;
+ using (var torClient = new TorHttpClient(IndexDownloader.Client.DestinationUri, IndexDownloader.Client.TorSocks5EndPoint, isolateStream: true))
+ using (var response = await torClient.SendAsync(HttpMethod.Get, $"/api/v1/btc/blockchain/fees/{feeTarget}"))
+ {
+ if (response.StatusCode != HttpStatusCode.OK) // Try again.
+ {
+ using (var response2 = await torClient.SendAsync(HttpMethod.Get, $"/api/v1/btc/blockchain/fees/{feeTarget}"))
+ {
+ await Task.Delay(1000);
+ if (response2.StatusCode != HttpStatusCode.OK)
+ {
+ throw new HttpRequestException($"Couldn't query network fees. Reason: {response2.StatusCode.ToReasonString()}");
+ }
+ else
+ {
+ using (var content = response.Content)
+ {
+ var json = await content.ReadAsJsonAsync<SortedDictionary<int, FeeEstimationPair>>();
+ feePerBytes = new Money(json.Single().Value.Conservative);
+ }
+ }
+ }
+ }
+ else
+ {
+ using (var content = response.Content)
+ {
+ var json = await content.ReadAsJsonAsync<SortedDictionary<int, FeeEstimationPair>>();
+ feePerBytes = new Money(json.Single().Value.Conservative);
+ }
+ }
+ }
+
+ }
+
#region IDisposable Support
private volatile bool _disposedValue = false; // To detect redundant calls

Prepare BuildTransactionAsync