예제 #1
0
        /// <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));
            }
        }
예제 #2
0
        /// <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())
            });
        }