/// <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 (var recipient in context.Recipients)
            {
                context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount);
            }
        }
        /// <inheritdoc />
        public Transaction BuildTransaction(TransactionBuildContext context)
        {
            InitializeTransactionBuilder(context);

            const int maxRetries = 5;
            var       retryCount = 0;

            TransactionPolicyError[] errors = null;
            while (retryCount <= maxRetries)
            {
                if (context.Shuffle)
                {
                    context.TransactionBuilder.Shuffle();
                }

                var transaction = context.TransactionBuilder.BuildTransaction(false);
                if (context.Sign)
                {
                    var coinsSpent = context.TransactionBuilder.FindSpentCoins(transaction);
                    // TODO: Improve this as we already have secrets when running a retry iteration.
                    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++;
            }

            var 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 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);
            }
        }
        /// <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.
            var balance     = context.UnspentOutputs.Sum(t => t.Transaction.Amount);
            var 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.

                var 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 (var unspentOutputsItem in availableHashList)
                    {
                        if (!context.SelectedInputs.Contains(unspentOutputsItem.Key))
                        {
                            context.UnspentOutputs.Remove(unspentOutputsItem.Value);
                        }
                    }
                }

                foreach (var outPoint in context.SelectedInputs)
                {
                    var 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 (var 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)
        {
            InitializeTransactionBuilder(context);

            return(context.TransactionFee);
        }
Exemple #6
0
        public async Task <uint256> SendManyAsync(string fromAccount, string addressesJson, int minConf = 1,
                                                  string comment = null, string subtractFeeFromJson = null, bool isReplaceable = false,
                                                  int?confTarget = null, string estimateMode        = "UNSET")
        {
            if (string.IsNullOrEmpty(addressesJson))
            {
                throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER,
                                             "No valid output addresses specified.");
            }

            var addresses = new Dictionary <string, decimal>();

            try
            {
                // Outputs addresses are keyvalue pairs of address, amount. Translate to Receipient list.
                addresses = JsonConvert.DeserializeObject <Dictionary <string, decimal> >(addressesJson);
            }
            catch (JsonSerializationException ex)
            {
                throw new RPCServerException(RPCErrorCode.RPC_PARSE_ERROR, ex.Message);
            }

            if (addresses.Count == 0)
            {
                throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER,
                                             "No valid output addresses specified.");
            }

            // Optional list of addresses to subtract fees from.
            IEnumerable <BitcoinAddress> subtractFeeFromAddresses = null;

            if (!string.IsNullOrEmpty(subtractFeeFromJson))
            {
                try
                {
                    subtractFeeFromAddresses = JsonConvert.DeserializeObject <List <string> >(subtractFeeFromJson)
                                               .Select(i => BitcoinAddress.Create(i, this.FullNode.Network));
                }
                catch (JsonSerializationException ex)
                {
                    throw new RPCServerException(RPCErrorCode.RPC_PARSE_ERROR, ex.Message);
                }
            }

            var recipients = new List <Recipient>();

            foreach (var address in addresses)
            {
                // Check for duplicate recipients
                var recipientAddress = BitcoinAddress.Create(address.Key, this.FullNode.Network).ScriptPubKey;
                if (recipients.Any(r => r.ScriptPubKey == recipientAddress))
                {
                    throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER,
                                                 string.Format("Invalid parameter, duplicated address: {0}.", recipientAddress));
                }

                var recipient = new Recipient
                {
                    ScriptPubKey          = recipientAddress,
                    Amount                = Money.Coins(address.Value),
                    SubtractFeeFromAmount = subtractFeeFromAddresses == null
                        ? false
                        : subtractFeeFromAddresses.Contains(BitcoinAddress.Create(address.Key, this.FullNode.Network))
                };

                recipients.Add(recipient);
            }

            var accountReference = GetWalletAccountReference();

            var context = new TransactionBuildContext(this.FullNode.Network)
            {
                AccountReference = accountReference,
                MinConfirmations = minConf,
                Shuffle          = true, // We shuffle transaction outputs by default as it's better for anonymity.
                Recipients       = recipients,
                CacheSecret      = false
            };

            // Set fee type for transaction build context.
            context.FeeType = FeeType.Medium;

            if (estimateMode.Equals("ECONOMICAL", StringComparison.InvariantCultureIgnoreCase))
            {
                context.FeeType = FeeType.Low;
            }

            else if (estimateMode.Equals("CONSERVATIVE", StringComparison.InvariantCultureIgnoreCase))
            {
                context.FeeType = FeeType.High;
            }

            try
            {
                // Log warnings for currently unsupported parameters.
                if (!string.IsNullOrEmpty(comment))
                {
                    this.logger.LogWarning("'comment' parameter is currently unsupported. Ignored.");
                }

                if (isReplaceable)
                {
                    this.logger.LogWarning("'replaceable' parameter is currently unsupported. Ignored.");
                }

                if (confTarget != null)
                {
                    this.logger.LogWarning("'conf_target' parameter is currently unsupported. Ignored.");
                }

                var transaction = this.walletTransactionHandler.BuildTransaction(context);
                await this.broadcasterManager.BroadcastTransactionAsync(transaction);

                return(transaction.GetHash());
            }
            catch (SecurityException)
            {
                throw new RPCServerException(RPCErrorCode.RPC_WALLET_UNLOCK_NEEDED, "Wallet unlock needed");
            }
            catch (WalletException exception)
            {
                throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message);
            }
            catch (NotImplementedException exception)
            {
                throw new RPCServerException(RPCErrorCode.RPC_MISC_ERROR, exception.Message);
            }
        }