/// <summary> /// Add recipients to the <see cref="TransactionBuilder"/>. /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> /// <remarks> /// Add outputs to the <see cref="TransactionBuilder"/> based on the <see cref="Recipient"/> list. /// </remarks> protected virtual void AddRecipients(TransactionBuildContext context) { if (context.Recipients.Any(a => a.Amount == Money.Zero)) { throw new WalletException("No amount specified."); } if (context.Recipients.Any(a => a.SubtractFeeFromAmount)) { throw new NotImplementedException("Substracting the fee from the recipient is not supported yet."); } foreach (Recipient recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } }
/// <summary> /// Find the next available change address. /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> protected void FindChangeAddress(TransactionBuildContext context) { if (context.ChangeAddress == null) { // If no change address is supplied, get a new address to send the change to. context.ChangeAddress = this.walletManager.GetUnusedChangeAddress(new WalletAccountReference(context.AccountReference.WalletName, context.AccountReference.AccountName)); } if (context.UseSegwitChangeAddress) { context.TransactionBuilder.SetChange(new BitcoinWitPubKeyAddress(context.ChangeAddress.Bech32Address, this.network).ScriptPubKey); } else { context.TransactionBuilder.SetChange(context.ChangeAddress.ScriptPubKey); } }
/// <inheritdoc /> public Transaction BuildTransaction(TransactionBuildContext context) { this.InitializeTransactionBuilder(context); const int maxRetries = 5; int retryCount = 0; TransactionPolicyError[] errors = null; while (retryCount <= maxRetries) { if (context.Shuffle) { context.TransactionBuilder.Shuffle(); } Transaction transaction = context.TransactionBuilder.BuildTransaction(false); if (context.Sign) { ICoin[] coinsSpent = context.TransactionBuilder.FindSpentCoins(transaction); // TODO: Improve this as we already have secrets when running a retry iteration. this.AddSecrets(context, coinsSpent); context.TransactionBuilder.SignTransactionInPlace(transaction); } if (context.TransactionBuilder.Verify(transaction, out errors)) { return(transaction); } // Retry only if error is of type 'FeeTooLowPolicyError' if (!errors.Any(e => e is FeeTooLowPolicyError)) { break; } retryCount++; } string errorsMessage = string.Join(" - ", errors.Select(s => s.ToString())); this.logger.LogError($"Build transaction failed: {errorsMessage}"); throw new WalletException($"Could not build the transaction. Details: {errorsMessage}"); }
/// <summary> /// Find all available outputs (UTXO's) that belong to <see cref="WalletAccountReference.AccountName"/>. /// Then add them to the <see cref="TransactionBuildContext.UnspentOutputs"/>. /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> protected void AddCoins(TransactionBuildContext context) { context.UnspentOutputs = this.walletManager.GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations).ToList(); if (context.UnspentOutputs.Count == 0) { throw new WalletException("No spendable transactions found."); } // Get total spendable balance in the account. long balance = context.UnspentOutputs.Sum(t => t.Transaction.Amount); long totalToSend = context.Recipients.Sum(s => s.Amount); if (balance < totalToSend) { throw new WalletException("Not enough funds."); } Money sum = 0; var coins = new List <Coin>(); if (context.SelectedInputs != null && context.SelectedInputs.Any()) { // 'SelectedInputs' are inputs that must be included in the // current transaction. At this point we check the given // input is part of the UTXO set and filter out UTXOs that are not // in the initial list if 'context.AllowOtherInputs' is false. Dictionary <OutPoint, UnspentOutputReference> availableHashList = context.UnspentOutputs.ToDictionary(item => item.ToOutPoint(), item => item); if (!context.SelectedInputs.All(input => availableHashList.ContainsKey(input))) { throw new WalletException("Not all the selected inputs were found on the wallet."); } if (!context.AllowOtherInputs) { foreach (KeyValuePair <OutPoint, UnspentOutputReference> unspentOutputsItem in availableHashList) { if (!context.SelectedInputs.Contains(unspentOutputsItem.Key)) { context.UnspentOutputs.Remove(unspentOutputsItem.Value); } } } foreach (OutPoint outPoint in context.SelectedInputs) { UnspentOutputReference item = availableHashList[outPoint]; coins.Add(new Coin(item.Transaction.Id, (uint)item.Transaction.Index, item.Transaction.Amount, item.Transaction.ScriptPubKey)); sum += item.Transaction.Amount; } } foreach (UnspentOutputReference item in context.UnspentOutputs .OrderByDescending(a => a.Confirmations > 0) .ThenByDescending(a => a.Transaction.Amount)) { if (context.SelectedInputs?.Contains(item.ToOutPoint()) ?? false) { continue; } // If the total value is above the target // then it's safe to stop adding UTXOs to the coin list. // The primary goal is to reduce the time it takes to build a trx // when the wallet is bloated with UTXOs. // Get to our total, and then check that we're a little bit over to account for tx fees. // If it gets over totalToSend but doesn't hit this break, that's fine too. // The TransactionBuilder will have a go with what we give it, and throw NotEnoughFundsException accurately if it needs to. if (sum > totalToSend + PretendMaxFee) { break; } coins.Add(new Coin(item.Transaction.Id, (uint)item.Transaction.Index, item.Transaction.Amount, item.Transaction.ScriptPubKey)); sum += item.Transaction.Amount; } // All the UTXOs are added to the builder without filtering. // The builder then has its own coin selection mechanism // to select the best UTXO set for the corresponding amount. // To add a custom implementation of a coin selection override // the builder using builder.SetCoinSelection(). context.TransactionBuilder.AddCoins(coins); }
/// <inheritdoc /> public Money EstimateFee(TransactionBuildContext context) { this.InitializeTransactionBuilder(context); return(context.TransactionFee); }