Skip to content

Commit 7cb024e

Browse files
committed
Merge #9222: Add 'subtractFeeFromAmount' option to 'fundrawtransaction'.
453bda6 Add 'subtractFeeFromOutputs' option to 'fundrawtransaction'. (Chris Moore)
2 parents f9117f2 + 453bda6 commit 7cb024e

File tree

5 files changed

+117
-14
lines changed

5 files changed

+117
-14
lines changed

qa/rpc-tests/fundrawtransaction.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,5 +660,75 @@ def run_test(self):
660660
assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
661661
assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
662662

663+
######################################
664+
# Test subtractFeeFromOutputs option #
665+
######################################
666+
667+
# Make sure there is exactly one input so coin selection can't skew the result
668+
assert_equal(len(self.nodes[3].listunspent(1)), 1)
669+
670+
inputs = []
671+
outputs = {self.nodes[2].getnewaddress(): 1}
672+
rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
673+
674+
result = [self.nodes[3].fundrawtransaction(rawtx), # uses min_relay_tx_fee (set by settxfee)
675+
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list
676+
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses min_relay_tx_fee (set by settxfee)
677+
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2*min_relay_tx_fee}),
678+
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2*min_relay_tx_fee, "subtractFeeFromOutputs": [0]})]
679+
680+
dec_tx = [self.nodes[3].decoderawtransaction(tx['hex']) for tx in result]
681+
output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)]
682+
change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)]
683+
684+
assert_equal(result[0]['fee'], result[1]['fee'], result[2]['fee'])
685+
assert_equal(result[3]['fee'], result[4]['fee'])
686+
assert_equal(change[0], change[1])
687+
assert_equal(output[0], output[1])
688+
assert_equal(output[0], output[2] + result[2]['fee'])
689+
assert_equal(change[0] + result[0]['fee'], change[2])
690+
assert_equal(output[3], output[4] + result[4]['fee'])
691+
assert_equal(change[3] + result[3]['fee'], change[4])
692+
693+
inputs = []
694+
outputs = {self.nodes[2].getnewaddress(): value for value in (1.0, 1.1, 1.2, 1.3)}
695+
keys = list(outputs.keys())
696+
rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
697+
698+
result = [self.nodes[3].fundrawtransaction(rawtx),
699+
# split the fee between outputs 0, 2, and 3, but not output 1
700+
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0, 2, 3]})]
701+
702+
dec_tx = [self.nodes[3].decoderawtransaction(result[0]['hex']),
703+
self.nodes[3].decoderawtransaction(result[1]['hex'])]
704+
705+
# Nested list of non-change output amounts for each transaction
706+
output = [[out['value'] for i, out in enumerate(d['vout']) if i != r['changepos']]
707+
for d, r in zip(dec_tx, result)]
708+
709+
# List of differences in output amounts between normal and subtractFee transactions
710+
share = [o0 - o1 for o0, o1 in zip(output[0], output[1])]
711+
712+
# output 1 is the same in both transactions
713+
assert_equal(share[1], 0)
714+
715+
# the other 3 outputs are smaller as a result of subtractFeeFromOutputs
716+
assert_greater_than(share[0], 0)
717+
assert_greater_than(share[2], 0)
718+
assert_greater_than(share[3], 0)
719+
720+
# outputs 2 and 3 take the same share of the fee
721+
assert_equal(share[2], share[3])
722+
723+
# output 0 takes at least as much share of the fee, and no more than 2 satoshis more, than outputs 2 and 3
724+
assert_greater_than_or_equal(share[0], share[2])
725+
assert_greater_than_or_equal(share[2] + Decimal(2e-8), share[0])
726+
727+
# the fee is the same in both transactions
728+
assert_equal(result[0]['fee'], result[1]['fee'])
729+
730+
# the total subtracted from the outputs is equal to the fee
731+
assert_equal(share[0] + share[2] + share[3], result[0]['fee'])
732+
663733
if __name__ == '__main__':
664734
RawTransactionsTest().main()

qa/rpc-tests/test_framework/util.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,18 @@ def assert_fee_amount(fee, tx_size, fee_per_kB):
524524
if fee > (tx_size + 2) * fee_per_kB / 1000:
525525
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)"%(str(fee), str(target_fee)))
526526

527-
def assert_equal(thing1, thing2):
528-
if thing1 != thing2:
529-
raise AssertionError("%s != %s"%(str(thing1),str(thing2)))
527+
def assert_equal(thing1, thing2, *args):
528+
if thing1 != thing2 or any(thing1 != arg for arg in args):
529+
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
530530

531531
def assert_greater_than(thing1, thing2):
532532
if thing1 <= thing2:
533533
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
534534

535+
def assert_greater_than_or_equal(thing1, thing2):
536+
if thing1 < thing2:
537+
raise AssertionError("%s < %s"%(str(thing1),str(thing2)))
538+
535539
def assert_raises(exc, fun, *args, **kwds):
536540
assert_raises_message(exc, None, fun, *args, **kwds)
537541

src/wallet/rpcwallet.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2479,7 +2479,8 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
24792479
throw runtime_error(
24802480
"fundrawtransaction \"hexstring\" ( options )\n"
24812481
"\nAdd inputs to a transaction until it has enough in value to meet its out value.\n"
2482-
"This will not modify existing inputs, and will add one change output to the outputs.\n"
2482+
"This will not modify existing inputs, and will add at most one change output to the outputs.\n"
2483+
"No existing outputs will be modified unless \"subtractFeeFromOutputs\" is specified.\n"
24832484
"Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n"
24842485
"The inputs added will not be signed, use signrawtransaction for that.\n"
24852486
"Note that all existing inputs must have their previous output transaction be in the wallet.\n"
@@ -2491,11 +2492,17 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
24912492
"1. \"hexstring\" (string, required) The hex string of the raw transaction\n"
24922493
"2. options (object, optional)\n"
24932494
" {\n"
2494-
" \"changeAddress\" (string, optional, default pool address) The bitcoin address to receive the change\n"
2495-
" \"changePosition\" (numeric, optional, default random) The index of the change output\n"
2496-
" \"includeWatching\" (boolean, optional, default false) Also select inputs which are watch only\n"
2497-
" \"lockUnspents\" (boolean, optional, default false) Lock selected unspent outputs\n"
2498-
" \"feeRate\" (numeric, optional, default not set: makes wallet determine the fee) Set a specific feerate (" + CURRENCY_UNIT + " per KB)\n"
2495+
" \"changeAddress\" (string, optional, default pool address) The bitcoin address to receive the change\n"
2496+
" \"changePosition\" (numeric, optional, default random) The index of the change output\n"
2497+
" \"includeWatching\" (boolean, optional, default false) Also select inputs which are watch only\n"
2498+
" \"lockUnspents\" (boolean, optional, default false) Lock selected unspent outputs\n"
2499+
" \"feeRate\" (numeric, optional, default not set: makes wallet determine the fee) Set a specific feerate (" + CURRENCY_UNIT + " per KB)\n"
2500+
" \"subtractFeeFromOutputs\" (array, optional) A json array of integers.\n"
2501+
" The fee will be equally deducted from the amount of each specified output.\n"
2502+
" The outputs are specified by their zero-based index, before any change output is added.\n"
2503+
" Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
2504+
" If no outputs are specified here, the sender pays the fee.\n"
2505+
" [vout_index,...]\n"
24992506
" }\n"
25002507
" for backward compatibility: passing in a true instead of an object will result in {\"includeWatching\":true}\n"
25012508
"\nResult:\n"
@@ -2523,6 +2530,8 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25232530
bool lockUnspents = false;
25242531
CFeeRate feeRate = CFeeRate(0);
25252532
bool overrideEstimatedFeerate = false;
2533+
UniValue subtractFeeFromOutputs;
2534+
set<int> setSubtractFeeFromOutputs;
25262535

25272536
if (request.params.size() > 1) {
25282537
if (request.params[1].type() == UniValue::VBOOL) {
@@ -2541,6 +2550,7 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25412550
{"includeWatching", UniValueType(UniValue::VBOOL)},
25422551
{"lockUnspents", UniValueType(UniValue::VBOOL)},
25432552
{"feeRate", UniValueType()}, // will be checked below
2553+
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
25442554
},
25452555
true, true);
25462556

@@ -2567,6 +2577,9 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25672577
feeRate = CFeeRate(AmountFromValue(options["feeRate"]));
25682578
overrideEstimatedFeerate = true;
25692579
}
2580+
2581+
if (options.exists("subtractFeeFromOutputs"))
2582+
subtractFeeFromOutputs = options["subtractFeeFromOutputs"].get_array();
25702583
}
25712584
}
25722585

@@ -2581,10 +2594,21 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25812594
if (changePosition != -1 && (changePosition < 0 || (unsigned int)changePosition > tx.vout.size()))
25822595
throw JSONRPCError(RPC_INVALID_PARAMETER, "changePosition out of bounds");
25832596

2597+
for (unsigned int idx = 0; idx < subtractFeeFromOutputs.size(); idx++) {
2598+
int pos = subtractFeeFromOutputs[idx].get_int();
2599+
if (setSubtractFeeFromOutputs.count(pos))
2600+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos));
2601+
if (pos < 0)
2602+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos));
2603+
if (pos >= int(tx.vout.size()))
2604+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos));
2605+
setSubtractFeeFromOutputs.insert(pos);
2606+
}
2607+
25842608
CAmount nFeeOut;
25852609
string strFailReason;
25862610

2587-
if(!pwalletMain->FundTransaction(tx, nFeeOut, overrideEstimatedFeerate, feeRate, changePosition, strFailReason, includeWatching, lockUnspents, changeAddress))
2611+
if(!pwalletMain->FundTransaction(tx, nFeeOut, overrideEstimatedFeerate, feeRate, changePosition, strFailReason, includeWatching, lockUnspents, setSubtractFeeFromOutputs, changeAddress))
25882612
throw JSONRPCError(RPC_INTERNAL_ERROR, strFailReason);
25892613

25902614
UniValue result(UniValue::VOBJ);

src/wallet/wallet.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,14 +2192,15 @@ bool CWallet::SelectCoins(const vector<COutput>& vAvailableCoins, const CAmount&
21922192
return res;
21932193
}
21942194

2195-
bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const CTxDestination& destChange)
2195+
bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, const CTxDestination& destChange)
21962196
{
21972197
vector<CRecipient> vecSend;
21982198

21992199
// Turn the txout set into a CRecipient vector
2200-
BOOST_FOREACH(const CTxOut& txOut, tx.vout)
2200+
for (size_t idx = 0; idx < tx.vout.size(); idx++)
22012201
{
2202-
CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, false};
2202+
const CTxOut& txOut = tx.vout[idx];
2203+
CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1};
22032204
vecSend.push_back(recipient);
22042205
}
22052206

@@ -2221,6 +2222,10 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool ov
22212222
if (nChangePosInOut != -1)
22222223
tx.vout.insert(tx.vout.begin() + nChangePosInOut, wtx.tx->vout[nChangePosInOut]);
22232224

2225+
// Copy output sizes from new transaction; they may have had the fee subtracted from them
2226+
for (unsigned int idx = 0; idx < tx.vout.size(); idx++)
2227+
tx.vout[idx].nValue = wtx.tx->vout[idx].nValue;
2228+
22242229
// Add new txins (keeping original txin scriptSig/order)
22252230
BOOST_FOREACH(const CTxIn& txin, wtx.tx->vin)
22262231
{

src/wallet/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
780780
* Insert additional inputs into the transaction by
781781
* calling CreateTransaction();
782782
*/
783-
bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const CTxDestination& destChange = CNoDestination());
783+
bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, const CTxDestination& destChange = CNoDestination());
784784

785785
/**
786786
* Create a new transaction paying the recipients with a set of coins

0 commit comments

Comments
 (0)