/// <summary> /// Constructs an unsigned transaction by referencing previous unspent outputs. /// A change output is added when necessary to return extra value back to the wallet. /// </summary> /// <param name="outputs">Transaction output array without change.</param> /// <param name="changeScript">Output script to pay change to.</param> /// <param name="fetchInputsAsync">Input selection source.</param> /// <returns>Unsigned transaction and total input amount.</returns> /// <exception cref="InsufficientFundsException">Input source was unable to provide enough input value.</exception> public static async Task <Tuple <Transaction, Amount> > BuildUnsignedTransaction(Transaction.Output[] outputs, OutputScript changeScript, Amount feePerKb, InputSource fetchInputsAsync) { if (outputs == null) { throw new ArgumentNullException(nameof(outputs)); } if (changeScript == null) { throw new ArgumentNullException(nameof(changeScript)); } if (fetchInputsAsync == null) { throw new ArgumentNullException(nameof(fetchInputsAsync)); } var targetAmount = outputs.Sum(o => o.Amount); var estimatedSize = Transaction.EstimateSerializeSize(1, outputs, true); var targetFee = TransactionFees.FeeForSerializeSize(feePerKb, estimatedSize); while (true) { var funding = await fetchInputsAsync(targetAmount + targetFee); var inputAmount = funding.Item1; var inputs = funding.Item2; if (inputAmount < targetAmount + targetFee) { throw new InsufficientFundsException(); } var unsignedTransaction = new Transaction(Transaction.SupportedVersion, inputs, outputs, 0, 0); if (inputAmount > targetAmount + targetFee) { unsignedTransaction = TransactionFees.AddChange(unsignedTransaction, inputAmount, changeScript, feePerKb); } if (TransactionFees.EstimatedFeePerKb(unsignedTransaction, inputAmount) < feePerKb) { estimatedSize = Transaction.EstimateSerializeSize(inputs.Length, outputs, true); targetFee = TransactionFees.FeeForSerializeSize(feePerKb, estimatedSize); } else { return(Tuple.Create(unsignedTransaction, inputAmount)); } } }
/// <summary> /// Constructs an unsigned transaction by referencing previous unspent outputs. /// A change output is added when necessary to return extra value back to the wallet. /// </summary> /// <param name="outputs">Transaction output array without change.</param> /// <param name="changeScript">Output script to pay change to.</param> /// <param name="fetchInputsAsync">Input selection source.</param> /// <returns>Unsigned transaction and total input amount.</returns> /// <exception cref="InsufficientFundsException">Input source was unable to provide enough input value.</exception> public static async Task <Tuple <Transaction, Amount> > BuildUnsignedTransaction(Transaction.Output[] outputs, Amount feePerKb, InputSource fetchInputsAsync, ChangeSource fetchChangeAsync) { if (outputs == null) { throw new ArgumentNullException(nameof(outputs)); } if (fetchInputsAsync == null) { throw new ArgumentNullException(nameof(fetchInputsAsync)); } if (fetchChangeAsync == null) { throw new ArgumentNullException(nameof(fetchChangeAsync)); } var targetAmount = outputs.Sum(o => o.Amount); var estimatedSize = Transaction.EstimateSerializeSize(1, outputs, true); var targetFee = TransactionFees.FeeForSerializeSize(feePerKb, estimatedSize); while (true) { var funding = await fetchInputsAsync(targetAmount + targetFee); var inputAmount = funding.Item1; var inputs = funding.Item2; if (inputAmount < targetAmount + targetFee) { throw new InsufficientFundsException(); } var maxSignedSize = Transaction.EstimateSerializeSize(inputs.Length, outputs, true); var maxRequiredFee = TransactionFees.FeeForSerializeSize(feePerKb, maxSignedSize); var remainingAmount = inputAmount - targetAmount; if (remainingAmount < maxRequiredFee) { targetFee = maxRequiredFee; continue; } var unsignedTransaction = new Transaction(Transaction.SupportedVersion, inputs, outputs, 0, 0); var changeAmount = inputAmount - targetAmount - maxRequiredFee; if (changeAmount != 0 && !TransactionRules.IsDustAmount(changeAmount, Transaction.PayToPubKeyHashPkScriptSize, feePerKb)) { var changeScript = await fetchChangeAsync(); if (changeScript.Script.Length > Transaction.PayToPubKeyHashPkScriptSize) { throw new Exception("Fee estimation requires change scripts no larger than P2PKH output scripts"); } var changeOutput = new Transaction.Output(changeAmount, Transaction.Output.LatestPkScriptVersion, changeScript.Script); var outputList = unsignedTransaction.Outputs.ToList(); outputList.Add(changeOutput); var outputsWithChange = outputList.ToArray(); unsignedTransaction = new Transaction(unsignedTransaction.Version, unsignedTransaction.Inputs, outputsWithChange, unsignedTransaction.LockTime, unsignedTransaction.Expiry); } return(Tuple.Create(unsignedTransaction, inputAmount)); } }