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); } WalletAccountReference accountReference = this.GetAccount(); 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."); } Transaction transaction = this.walletTransactionHandler.BuildTransaction(context); await this.broadcasterManager.BroadcastTransactionAsync(transaction); return(transaction.GetHash()); } catch (SecurityException exception) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_UNLOCK_NEEDED, exception.Message); } catch (WalletException exception) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); } catch (NotImplementedException exception) { throw new RPCServerException(RPCErrorCode.RPC_MISC_ERROR, exception.Message); } }
/// <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."); } int totalSubtractingRecipients = context.Recipients.Count(r => r.SubtractFeeFromAmount); // If none of them need the fee subtracted then it's simply a matter of adding the individual recipients to the builder. if (totalSubtractingRecipients == 0) { foreach (Recipient recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } return; } // If the transaction fee has been explicitly specified, and we have any recipients that require a fee to be subtracted // from the amount to be sent, then evenly distribute the chosen fee among all recipients. Any remaining fee should be // subtracted from the first recipient. if (context.TransactionFee != null) { Money fee = context.TransactionFee; long recipientFee = fee.Satoshi / totalSubtractingRecipients; long remainingFee = fee.Satoshi % totalSubtractingRecipients; for (int i = 0; i < context.Recipients.Count; i++) { Recipient recipient = context.Recipients[i]; if (recipient.SubtractFeeFromAmount) { // First receiver pays the remainder not divisible by output count. long feeToSubtract = i == 0 ? remainingFee + recipientFee : recipientFee; long remainingAmount = recipient.Amount.Satoshi - feeToSubtract; if (remainingAmount <= 0) { throw new WalletException($"Fee {feeToSubtract} is higher than amount {recipient.Amount.Satoshi} to send."); } recipient.Amount = new Money(remainingAmount); } context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } } else { // This is currently a limitation of the NBitcoin TransactionBuilder. // The only alternative would possibly be to recompute the output sizes after the AddFee call. if (totalSubtractingRecipients > 1) { throw new WalletException($"Cannot subtract fee from more than 1 recipient if {nameof(context.TransactionFee)} is not set."); } // If the transaction fee has not been explicitly specified yet, then the builder needs to assign it later from the wallet fee policy. // So we just need to indicate to the builder that the fees must be subtracted from the specified recipient. foreach (Recipient recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); if (recipient.SubtractFeeFromAmount) { context.TransactionBuilder.SubtractFees(); } } } }