Implement Input Selection
MODIFY
HiddenWallet.FullSpvWallet/WalletJob.cs
HiddenWallet.FullSpvWallet/WalletJob.cs
@@ -890,34 +890,30 @@ namespace HiddenWallet.FullSpv
#region TransactionSending
- /// <summary>
- ///
- /// </summary>
- /// <param name="scriptPubKeyToSend"></param>
/// <param name="amount">If Money.Zero then spend all available amount</param>
- /// <param name="feeType"></param>
- /// <param name="account"></param>
/// <param name="allowUnconfirmed">Allow to spend unconfirmed transactions, if necessary</param>
- /// <returns></returns>
- public async Task<BuildTransactionResult> BuildTransactionAsync(Script scriptPubKeyToSend, Money amount, FeeType feeType, SafeAccount account = null, bool allowUnconfirmed = false, bool subtractFeeFromAmount = false, Script customChangeScriptPubKey = null)
+ /// <param name="allowedInputs">Only these inputs allowed to build the transaction. The specified account must contain it.</param>
+ public async Task<BuildTransactionResult> BuildTransactionAsync(Script scriptPubKeyToSend, Money amount, FeeType feeType, SafeAccount account = null, bool allowUnconfirmed = false, bool subtractFeeFromAmount = false, Script customChangeScriptPubKey = null, IEnumerable<OutPoint> allowedInputs = null)
{
try
{
AssertAccount(account);
// 1. Get the script pubkey of the change.
- Debug.WriteLine("Select change address...");
- Script changeScriptPubKey;
- if (customChangeScriptPubKey != null)
- {
- changeScriptPubKey = customChangeScriptPubKey;
- }
- else changeScriptPubKey = (await GetUnusedScriptPubKeysAsync(AddressType.Pay2WitnessPublicKeyHash, account, HdPathType.Change)).FirstOrDefault();
+ Script changeScriptPubKey = await GetChangeScriptPubKey(account, customChangeScriptPubKey);
// 2. Find all coins I can spend from the account
// 3. How much money we can spend?
Debug.WriteLine("Calculating available amount...");
- var getBalanceResult = await GetBalanceAsync(account);
+ (AvailableAmount Available, IDictionary<Coin, bool> UnspentCoins) getBalanceResult;
+ if (allowedInputs != null)
+ {
+ getBalanceResult = await GetBalanceAsync(allowedInputs, account);
+ }
+ else
+ {
+ getBalanceResult = await GetBalanceAsync(account);
+ }
var unspentCoins = getBalanceResult.UnspentCoins;
AvailableAmount balance = getBalanceResult.Available;
Money spendableConfirmedAmount = balance.Confirmed;
@@ -1100,6 +1096,8 @@ namespace HiddenWallet.FullSpv
successfulResult.ChangeOutput = new TxOut(totalOut - amountToSend, changeScriptPubKey);
}
else successfulResult.ChangeOutput = null;
+
+ successfulResult.SpentCoins = coinsToSpend;
return successfulResult;
}
catch (Exception ex)
@@ -1112,6 +1110,18 @@ namespace HiddenWallet.FullSpv
}
}
+ private async Task<Script> GetChangeScriptPubKey(SafeAccount account, Script customChangeScriptPubKey)
+ {
+ Debug.WriteLine("Select change address...");
+ Script changeScriptPubKey;
+ if (customChangeScriptPubKey != null)
+ {
+ changeScriptPubKey = customChangeScriptPubKey;
+ }
+ else changeScriptPubKey = (await GetUnusedScriptPubKeysAsync(AddressType.Pay2WitnessPublicKeyHash, account, HdPathType.Change)).FirstOrDefault();
+ return changeScriptPubKey;
+ }
+
private static BuildTransactionResult NotEnoughFundsBuildTransactionResult =>
new BuildTransactionResult
{
@@ -1171,6 +1181,26 @@ namespace HiddenWallet.FullSpv
return haveEnough;
}
+ public async Task<(AvailableAmount Available, IDictionary<Coin, bool> UnspentCoins)> GetBalanceAsync(IEnumerable<OutPoint> inputs, SafeAccount account = null)
+ {
+ // 1. Find all coins I can spend from the account
+ Debug.WriteLine("Finding all unspent coins...");
+ var unspentCoins = await GetUnspentCoinsAsync(account);
+
+ // 2. filter out to the inputs
+ var filteredUnspentCoins = new Dictionary<Coin, bool>();
+ foreach (var elem in unspentCoins)
+ {
+ if(inputs.Any(x=>x.Hash == elem.Key.Outpoint.Hash && x.N == elem.Key.Outpoint.N))
+ {
+ filteredUnspentCoins.Add(elem.Key, elem.Value);
+ }
+ }
+
+ // 2. How much money we can spend?
+ return CalculateBalance(filteredUnspentCoins);
+ }
+
public async Task<(AvailableAmount Available, IDictionary<Coin, bool> UnspentCoins)> GetBalanceAsync(SafeAccount account = null)
{
// 1. Find all coins I can spend from the account
@@ -1178,6 +1208,11 @@ namespace HiddenWallet.FullSpv
var unspentCoins = await GetUnspentCoinsAsync(account);
// 2. How much money we can spend?
+ return CalculateBalance(unspentCoins);
+ }
+
+ private static (AvailableAmount Available, IDictionary<Coin, bool> UnspentCoins) CalculateBalance(IDictionary<Coin, bool> unspentCoins)
+ {
var confirmedAvailableAmount = Money.Zero;
var unconfirmedAvailableAmount = Money.Zero;
foreach (var elem in unspentCoins)
@@ -1200,7 +1235,6 @@ namespace HiddenWallet.FullSpv
Unconfirmed = unconfirmedAvailableAmount
},
unspentCoins);
-
}
public struct BuildTransactionResult
@@ -1213,6 +1247,7 @@ namespace HiddenWallet.FullSpv
public decimal FeePercentOfSent;
public TxOut ActiveOutput;
public TxOut ChangeOutput;
+ public IEnumerable<ICoin> SpentCoins { get; internal set; }
}
public struct AvailableAmount
{

MODIFY
HiddenWallet.Tests/SendTests.cs
HiddenWallet.Tests/SendTests.cs
@@ -331,36 +331,46 @@ namespace HiddenWallet.Tests
Debug.WriteLine($"Active Output: {res.ActiveOutput.Value.ToString(false, true)} {res.ActiveOutput.ScriptPubKey.GetDestinationAddress(network)}");
Debug.WriteLine($"TxId: {res.Transaction.GetHash()}");
- foundReceive = false;
- Assert.InRange(res.Transaction.Outputs.Count, 1, 2);
- foreach (var output in res.Transaction.Outputs)
- {
- if (output.ScriptPubKey == receive)
- {
- foundReceive = true;
- Assert.Equal(bal.Confirmed + bal.Unconfirmed - res.Fee, output.Value);
- }
- }
- Assert.True(foundReceive);
+ Assert.Single(res.Transaction.Outputs);
+ var maxBuiltTxOutput = res.Transaction.Outputs.Single();
+ Assert.Equal(receive, maxBuiltTxOutput.ScriptPubKey);
+ Assert.Equal(bal.Confirmed + bal.Unconfirmed - res.Fee, maxBuiltTxOutput.Value);
- _txProbArrived = false;
- _prevCount = (await walletJob.GetTrackerAsync()).TrackedTransactions.Count;
- _currentWalletJob = walletJob;
- (await walletJob.GetTrackerAsync()).TrackedTransactions.CollectionChanged += TrackedTransactions_CollectionChangedAsync;
+ #endregion
- sendRes = await walletJob.SendTransactionAsync(res.Transaction);
- Assert.True(sendRes.Success);
- Assert.Empty(sendRes.FailingReason);
+ #region InputSelection
- while (_txProbArrived == false)
- {
- Debug.WriteLine("Waiting for transaction...");
- await Task.Delay(1000);
- }
+ receive = (await walletJob.GetUnusedScriptPubKeysAsync(AddressType.Pay2WitnessPublicKeyHash, account, HdPathType.Receive)).FirstOrDefault();
- Debug.WriteLine("TrackedTransactions collection changed");
- Assert.Contains((await walletJob.GetTrackerAsync()).TrackedTransactions, x => x.Transaction.GetHash() == res.Transaction.GetHash());
- Debug.WriteLine("Transaction arrived");
+ bal = (await walletJob.GetBalanceAsync(account)).Available;
+
+ var inputCountBefore = res.SpentCoins.Count();
+ res = await walletJob.BuildTransactionAsync(receive, Money.Zero, FeeType.Low, account,
+ allowUnconfirmed: true,
+ allowedInputs: res.SpentCoins.Where((x, i) => i == 0 || i % 2 == 0).Select(x=>x.Outpoint));
+
+ Assert.True(inputCountBefore >= res.SpentCoins.Count());
+ Assert.Equal(res.SpentCoins.Count(), res.Transaction.Inputs.Count);
+ Assert.True(res.Success);
+ Assert.Empty(res.FailingReason);
+ Assert.Equal(receive, res.ActiveOutput.ScriptPubKey);
+ Assert.Null(res.ChangeOutput);
+ Debug.WriteLine($"Fee: {res.Fee}");
+ Debug.WriteLine($"FeePercentOfSent: {res.FeePercentOfSent} %");
+ Debug.WriteLine($"SpendsUnconfirmed: {res.SpendsUnconfirmed}");
+ Debug.WriteLine($"Active Output: {res.ActiveOutput.Value.ToString(false, true)} {res.ActiveOutput.ScriptPubKey.GetDestinationAddress(network)}");
+ Debug.WriteLine($"TxId: {res.Transaction.GetHash()}");
+
+ Assert.Single(res.Transaction.Outputs);
+
+ res = await walletJob.BuildTransactionAsync(receive, Money.Zero, FeeType.Low, account,
+ allowUnconfirmed: true,
+ allowedInputs: new[] { res.SpentCoins.Select(x => x.Outpoint).First() });
+
+ Assert.Single(res.Transaction.Inputs);
+ Assert.Single(res.Transaction.Outputs);
+ Assert.Single(res.SpentCoins);
+ Assert.Null(res.ChangeOutput);
#endregion
}

Implement Input Selection