Add labelling for transactions
MODIFY
MagicalCryptoWallet.Tests/RegTests.cs
MagicalCryptoWallet.Tests/RegTests.cs
@@ -669,7 +669,7 @@ namespace MagicalCryptoWallet.Tests
}
var scp = new Key().ScriptPubKey;
- var res2 = await wallet.BuildTransactionAsync("password", new[] { (scp, new Money(0.05m, MoneyUnit.BTC)) }, 5, false);
+ var res2 = await wallet.BuildTransactionAsync("password", new[] { (scp, new Money(0.05m, MoneyUnit.BTC), "foo") }, 5, false);
Assert.NotNull(res2.Transaction);
Assert.Single(res2.OuterWalletOutputs);
@@ -690,7 +690,7 @@ namespace MagicalCryptoWallet.Tests
Script receive = wallet.GetReceiveKey("Basic").GetP2wpkhScript();
Money amountToSend = wallet.Coins.Where(x => x.Unspent).Sum(x => x.Amount) / 2;
- var res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend) }, 1008, allowUnconfirmed: true);
+ var res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend, "foo") }, 1008, allowUnconfirmed: true);
Assert.Equal(2, res.InnerWalletOutputs.Count());
Assert.Empty(res.OuterWalletOutputs);
@@ -730,7 +730,7 @@ namespace MagicalCryptoWallet.Tests
receive = wallet.GetReceiveKey("SubtractFeeFromAmount").GetP2wpkhScript();
amountToSend = wallet.Coins.Where(x => x.Unspent).Sum(x => x.Amount) / 2;
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend) }, 1008, allowUnconfirmed: true, subtractFeeFromAmountIndex: 0);
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend, "foo") }, 1008, allowUnconfirmed: true, subtractFeeFromAmountIndex: 0);
Assert.Equal(2, res.InnerWalletOutputs.Count());
Assert.Empty(res.OuterWalletOutputs);
@@ -763,7 +763,7 @@ namespace MagicalCryptoWallet.Tests
#region LowFee
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend) }, 1008, allowUnconfirmed: true);
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend, "foo") }, 1008, allowUnconfirmed: true);
Assert.Equal(2, res.InnerWalletOutputs.Count());
Assert.Empty(res.OuterWalletOutputs);
@@ -796,7 +796,7 @@ namespace MagicalCryptoWallet.Tests
#region MediumFee
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend) }, 144, allowUnconfirmed: true);
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend, "foo") }, 144, allowUnconfirmed: true);
Assert.Equal(2, res.InnerWalletOutputs.Count());
Assert.Empty(res.OuterWalletOutputs);
@@ -829,7 +829,7 @@ namespace MagicalCryptoWallet.Tests
#region HighFee
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend) }, 2, allowUnconfirmed: true);
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, amountToSend, "foo") }, 2, allowUnconfirmed: true);
Assert.Equal(2, res.InnerWalletOutputs.Count());
Assert.Empty(res.OuterWalletOutputs);
@@ -868,7 +868,7 @@ namespace MagicalCryptoWallet.Tests
#region MaxAmount
receive = wallet.GetReceiveKey("MaxAmount").GetP2wpkhScript();
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero) }, 1008, allowUnconfirmed: true);
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero, "foo") }, 1008, allowUnconfirmed: true);
Assert.Single(res.InnerWalletOutputs);
Assert.Empty(res.OuterWalletOutputs);
@@ -896,7 +896,7 @@ namespace MagicalCryptoWallet.Tests
receive = wallet.GetReceiveKey("InputSelection").GetP2wpkhScript();
var inputCountBefore = res.SpentCoins.Count();
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero) }, 1008,
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero, "foo") }, 1008,
allowUnconfirmed: true,
allowedInputs: wallet.Coins.Where(x=>x.Unspent).Select(x=>new TxoRef(x.TransactionId, x.Index)).Take(1));
@@ -916,7 +916,7 @@ namespace MagicalCryptoWallet.Tests
Assert.Single(res.Transaction.Transaction.Outputs);
- res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero) }, 1008,
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero, "foo") }, 1008,
allowUnconfirmed: true,
allowedInputs: new[] { res.SpentCoins.Select(x => new TxoRef(x.TransactionId, x.Index)).First() });
@@ -929,6 +929,40 @@ namespace MagicalCryptoWallet.Tests
Assert.Single(res.SpentCoins);
#endregion
+
+ #region Labeling
+
+ res = await wallet.BuildTransactionAsync("password", new[] { (receive, Money.Zero, "my label") }, 1008,
+ allowUnconfirmed: true);
+
+ Assert.Single(res.InnerWalletOutputs);
+ Assert.Equal("change of (my label)", res.InnerWalletOutputs.Single().Label);
+
+ amountToSend = wallet.Coins.Where(x => x.Unspent).Sum(x => x.Amount) / 3;
+ res = await wallet.BuildTransactionAsync("password", new[] {
+ (new Key().ScriptPubKey, amountToSend, "outgoing"),
+ (new Key().ScriptPubKey, amountToSend, "outgoing2")
+ }, 1008,
+ allowUnconfirmed: true);
+
+ Assert.Single(res.InnerWalletOutputs);
+ Assert.Equal(2, res.OuterWalletOutputs.Count());
+ Assert.Equal("change of (outgoing, outgoing2)", res.InnerWalletOutputs.Single().Label);
+
+ await wallet.SendTransactionAsync(res.Transaction);
+
+ Assert.Contains("change of (outgoing, outgoing2)", wallet.Coins.Where(x=>x.Height == Height.MemPool).Select(x => x.Label));
+ Assert.Contains("change of (outgoing, outgoing2)", keyManager.GetKeys().Select(x => x.Label));
+
+ _filtersProcessedByWalletCount = 0;
+ await Global.RpcClient.GenerateAsync(1);
+ await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1);
+
+ var bestHeight = wallet.IndexDownloader.GetBestFilter().BlockHeight;
+ Assert.Contains("change of (outgoing, outgoing2)", wallet.Coins.Where(x => x.Height == bestHeight).Select(x => x.Label));
+ Assert.Contains("change of (outgoing, outgoing2)", keyManager.GetKeys().Select(x => x.Label));
+
+ #endregion
}
finally
{

MODIFY
MagicalCryptoWallet/Services/WalletService.cs
MagicalCryptoWallet/Services/WalletService.cs
@@ -454,7 +454,7 @@ namespace MagicalCryptoWallet.Services
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
- public async Task<BuildTransactionResult> BuildTransactionAsync(string password, (Script script, Money amount)[] toSend, int feeTarget, bool allowUnconfirmed = false, int? subtractFeeFromAmountIndex = null, Script customChange = null, IEnumerable<TxoRef> allowedInputs = null)
+ public async Task<BuildTransactionResult> BuildTransactionAsync(string password, (Script script, Money amount, string label)[] 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);
@@ -514,19 +514,6 @@ namespace MagicalCryptoWallet.Services
}
}
- // 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 };
- }
-
-
// 4. Get and calculate fee
Logger.LogInfo<WalletService>("Calculating dynamic transaction fee...");
Money feePerBytes = null;
@@ -586,12 +573,13 @@ namespace MagicalCryptoWallet.Services
// 5. How much to spend?
long toSendAmountSumInSatoshis = toSend.Sum(x => x.amount); // Does it work if I simply go with Money class here? Is that copied by reference of value?
- var realToSend = new (Script script, Money amount)[toSend.Length];
+ var realToSend = new (Script script, Money amount, string label)[toSend.Length];
for (int i = 0; i < toSend.Length; i++) // clone
{
realToSend[i] = (
new Script(toSend[i].script.ToString()),
- new Money(toSend[i].amount.Satoshi));
+ new Money(toSend[i].amount.Satoshi),
+ toSend[i].label);
}
for (int i = 0; i < realToSend.Length; i++)
{
@@ -618,10 +606,33 @@ namespace MagicalCryptoWallet.Services
}
}
- var toRemoveList = new List<(Script script, Money money)>(realToSend);
+ var toRemoveList = new List<(Script script, Money money, string label)>(realToSend);
toRemoveList.RemoveAll(x => x.money == Money.Zero);
realToSend = toRemoveList.ToArray();
+ // 1. Get the possible changes.
+ Script changeScriptPubKey;
+ var sb = new StringBuilder();
+ foreach (var item in realToSend)
+ {
+ sb.Append(item.label ?? "?");
+ sb.Append(", ");
+ }
+ var changeLabel = $"change of ({sb.ToString().TrimEnd(',', ' ')})";
+
+ if (customChange == null)
+ {
+ AssertCleanKeysIndexed(21, true);
+ var changeHdPubKey = KeyManager.GetKeys(KeyState.Clean, true).RandomElement();
+
+ changeHdPubKey.Label = changeLabel;
+ changeScriptPubKey = changeHdPubKey.GetP2wpkhScript();
+ }
+ else
+ {
+ changeScriptPubKey = customChange;
+ }
+
// 6. Do some checks
Money totalOutgoingAmount = realToSend.Sum(x => x.amount) + fee;
decimal feePc = (100 * fee.ToDecimal(MoneyUnit.BTC)) / totalOutgoingAmount.ToDecimal(MoneyUnit.BTC);
@@ -655,13 +666,13 @@ namespace MagicalCryptoWallet.Services
.AddCoins(coinsToSpend.Select(x => x.ToCoin()))
.AddKeys(signingKeys.ToArray());
- foreach((Script scriptPubKey, Money amount) output in realToSend)
+ foreach((Script scriptPubKey, Money amount, string label) output in realToSend)
{
builder = builder.Send(output.scriptPubKey, output.amount);
}
var tx = builder
- .SetChange(allowedChanges.RandomElement())
+ .SetChange(changeScriptPubKey)
.SendFees(fee)
.Shuffle()
.BuildTransaction(true);
@@ -684,6 +695,7 @@ namespace MagicalCryptoWallet.Services
var coin = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, spentOutputs, Height.Unknown);
if (KeyManager.GetKeys(KeyState.Clean).Select(x => x.GetP2wpkhScript()).Contains(coin.ScriptPubKey))
{
+ coin.Label = changeLabel;
innerWalletOutputs.Add(coin);
}
else

Add labelling for transactions