/// <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)); } }
/// <summary> /// Builds a transaction that sends value from one address to another. /// Change is spent to the source address, if necessary. /// </summary> /// <param name="request"></param> /// <param name="feeFactor"></param> /// <returns></returns> /// <exception cref="BusinessException"></exception> public async Task <BuildTransactionResponse> BuildSingleTransactionAsync(BuildSingleTransactionRequest request, decimal feeFactor) { if (string.IsNullOrWhiteSpace(request.FromAddress)) { throw new BusinessException(ErrorReason.BadRequest, "FromAddress missing"); } if (string.IsNullOrWhiteSpace(request.ToAddress)) { throw new BusinessException(ErrorReason.BadRequest, "ToAddress missing"); } if (!long.TryParse(request.Amount, out var amount) || amount <= 0) { throw new BusinessException(ErrorReason.BadRequest, $"Invalid amount {amount}"); } var feePerKb = await _feeService.GetFeePerKb(); if (TransactionRules.IsDustAmount(amount, Transaction.PayToPubKeyHashPkScriptSize, new Amount(feePerKb))) { throw new BusinessException(ErrorReason.AmountTooSmall, "Amount is dust"); } const int outputVersion = 0; const int lockTime = 0; const int expiry = 0; // Number of outputs newly build tx will contain, // not including the change address const int numOutputs = 1; // Lykke api doesn't have option to specify a change address. var changeAddress = Address.Decode(request.FromAddress); var toAddress = Address.Decode(request.ToAddress); var allInputs = (await GetUtxosForAddress(request.FromAddress)).ToList(); long estFee = 0; long totalSpent = 0; var consumedInputs = new List <Transaction.Input>(); bool HasEnoughInputs(out long fee) { var calculateWithChange = false; while (true) { var changeCount = calculateWithChange ? 1 : 0; fee = _feeService.CalculateFee(feePerKb, consumedInputs.Count, numOutputs + changeCount, feeFactor); var estAmount = amount + (request.IncludeFee ? 0 : fee); if (totalSpent < estAmount) { return(false); } if (totalSpent == estAmount) { return(true); } if (totalSpent > estAmount && calculateWithChange) { return(true); } // Loop one more time but make sure change is accounted for this time. if (totalSpent > estAmount) { calculateWithChange = true; } } } // Accumulate inputs until we have enough to cover the cost // of the amount + fee foreach (var input in allInputs) { // Don't consume an outpoint if it's spent. if (await IsBroadcastedUtxo(input.PreviousOutpoint)) { continue; } consumedInputs.Add(input); totalSpent += input.InputAmount; if (HasEnoughInputs(out estFee)) { break; } } // If all inputs do not have enough value to fund the transaction. if (totalSpent < amount + (request.IncludeFee ? 0 : estFee)) { throw new BusinessException(ErrorReason.NotEnoughBalance, "Address balance too low"); } // The fee either comes from the change or the sent amount var send = amount - (request.IncludeFee ? estFee : 0); var change = (totalSpent - amount) - (request.IncludeFee ? 0 : estFee); // If all inputs do not have enough value to fund the transaction, throw error. if (request.IncludeFee && estFee > amount) { throw new BusinessException(ErrorReason.AmountTooSmall, "Amount not enough to include fee"); } // If all inputs do not have enough value to fund the transaction, throw error. if (totalSpent < amount + (request.IncludeFee ? 0 : estFee)) { throw new BusinessException(ErrorReason.NotEnoughBalance, "Address balance too low"); } // Build outputs to address + change address. // If any of the outputs is zero value, exclude it. For example, if there is no change. var outputs = new[] { new Transaction.Output(send, outputVersion, toAddress.BuildScript().Script), new Transaction.Output(change, outputVersion, changeAddress.BuildScript().Script) }.Where(o => o.Amount != 0).ToArray(); var newTx = new Transaction( Transaction.SupportedVersion, consumedInputs.ToArray(), outputs, lockTime, expiry ); return(new BuildTransactionResponse { TransactionContext = HexUtil.FromByteArray(newTx.Serialize()) }); }