diff --git a/doc/release-notes.md b/doc/release-notes.md index fa996c5f363a..8f797359a950 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -80,7 +80,6 @@ The `getwalletinfo` RPC method returns the name of the wallet used for the query Note that while multi-wallet is now fully supported, the RPC multi-wallet interface should be considered unstable for version 6.0.0, and there may backwards-incompatible changes in future versions. - GUI changes ----------- @@ -110,6 +109,7 @@ Results without keys can be queried using an integer in brackets. example: getblock(getblockhash(0),true)[tx][0] ``` + Support for JSON-RPC Named Arguments ------------------------------------ @@ -129,14 +129,11 @@ The order of arguments doesn't matter in this case. Named arguments are also use The RPC server remains fully backwards compatible with positional arguments. -#### Allow to optional specify the directory for the blocks storage - -A new init option flag '-blocksdir' will allow one to keep the blockfiles external from the data directory. - - Low-level RPC changes --------------------- +### Query options for listunspent RPC + - The `listunspent` RPC now takes a `query_options` argument (see [PR #2317](https://github.com/PIVX-Project/PIVX/pull/2317)), which is a JSON object containing one or more of the following members: - `minimumAmount` - a number specifying the minimum value of each UTXO @@ -151,12 +148,37 @@ Low-level RPC changes - the `stop` RPC no longer accepts the (already deprecated, ignored, and undocumented) optional boolean argument `detach`. +### Subtract fee from recipient amount for RPC + +A new argument `subtract_fee_from` is added to `sendmany`/`shieldsendmany` RPC functions. +It can be used to provide the list of recipent addresses paying the fee. +``` +subtract_fee_from (array, optional) +A json array with addresses. +The fee will be equally deducted from the amount of each selected address. + [\"address\" (string) Subtract fee from this address\n" + ,... + ] + +For `fundrawtransaction` a new key (`subtractFeeFromOutputs`) is added to the `options` argument. +It can be used to specify a list of output indexes. +``` +subtractFeeFromOutputs (array, optional) A json array of integers. +The fee will be equally deducted from the amount of each specified output. +The outputs are specified by their zero-based index, before any change output is added. + [vout_index,...] +``` + +For `sendtoaddress`, the new parameter is called `subtract_fee` and it is a simple boolean. -#### Show wallet's auto-combine settings in getwalletinfo +In all cases those recipients will receive less PIV than you enter in their corresponding amount field. +If no outputs/addresses are specified, the sender pays the fee as usual. + +### Show wallet's auto-combine settings in getwalletinfo `getwalletinfo` now has two additional return fields. `autocombine_enabled` (boolean) and `autocombine_threshold` (numeric) that will show the auto-combine threshold and whether or not it is currently enabled. -#### Deprecate the autocombine RPC command +### Deprecate the autocombine RPC command The `autocombine` RPC command has been replaced with specific set/get commands (`setautocombinethreshold` and `getautocombinethreshold`, respectively) to bring this functionality further in-line with our RPC standards. Previously, the `autocombine` command gave no user-facing feedback when the setting was changed. This is now resolved with the introduction of the two new commands as detailed below: @@ -191,26 +213,18 @@ The `autocombine` RPC command has been replaced with specific set/get commands ( } ``` -#### Build system changes +Build system changes +-------------------- The minimum supported miniUPnPc API version is set to 10. This keeps compatibility with Ubuntu 16.04 LTS and Debian 8 `libminiupnpc-dev` packages. Please note, on Debian this package is still vulnerable to [CVE-2017-8798](https://security-tracker.debian.org/tracker/CVE-2017-8798) (in jessie only) and [CVE-2017-1000494](https://security-tracker.debian.org/tracker/CVE-2017-1000494) (both in jessie and in stretch). - -#### Build System - OpenSSL is no longer used by PIVX Core -#### Disable PoW mining RPC Commands - -A new configure flag has been introduced to allow more granular control over weather or not the PoW mining RPC commands are compiled into the wallet. By default they are not. This behavior can be overridden by passing `--enable-mining-rpc` to the `configure` script. - -#### Removed startup options - -- `printstakemodifier` +Configuration changes +--------------------- -Configuration sections for testnet and regtest ----------------------------------------------- +### Configuration sections for testnet and regtest It is now possible for a single configuration file to set different options for different networks. This is done by using sections or by prefixing the option with the network, such as: @@ -226,16 +240,27 @@ It is now possible for a single configuration file to set different options for The `addnode=`, `connect=`, `port=`, `bind=`, `rpcport=`, `rpcbind=`, and `wallet=` options will only apply to mainnet when specified in the configuration file, unless a network is specified. +### Allow to optional specify the directory for the blocks storage -#### Logging +A new init option flag '-blocksdir' will allow one to keep the blockfiles external from the data directory. -The log timestamp format is now ISO 8601 (e.g. "2021-02-28T12:34:56Z"). +### Disable PoW mining RPC Commands +A new configure flag has been introduced to allow more granular control over weather or not the PoW mining RPC commands are compiled into the wallet. By default they are not. This behavior can be overridden by passing `--enable-mining-rpc` to the `configure` script. + +### Removed startup options + +- `printstakemodifier` -#### Automatic Backup File Naming +### Logging + +The log timestamp format is now ISO 8601 (e.g. "2021-02-28T12:34:56Z"). + +### Automatic Backup File Naming The file extension applied to automatic backups is now in ISO 8601 basic notation (e.g. "20210228T123456Z"). The basic notation is used to prevent illegal `:` characters from appearing in the filename. + *version* Change log ============== diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index ce681222beae..567ccf24b680 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -598,16 +598,17 @@ OperationResult WalletModel::PrepareShieldedTransaction(WalletModelTransaction* const CCoinControl* coinControl) { // Load shieldedAddrRecipients. + bool fSubtractFeeFromAmount{false}; std::vector recipients; for (const auto& recipient : modelTransaction->getRecipients()) { if (recipient.isShieldedAddr) { auto pa = KeyIO::DecodeSaplingPaymentAddress(recipient.address.toStdString()); if (!pa) return errorOut("Error, invalid shielded address"); - recipients.emplace_back(*pa, recipient.amount, recipient.message.toStdString()); + recipients.emplace_back(*pa, recipient.amount, recipient.message.toStdString(), fSubtractFeeFromAmount); } else { auto dest = DecodeDestination(recipient.address.toStdString()); if (!IsValidDestination(dest)) return errorOut("Error, invalid transparent address"); - recipients.emplace_back(dest, recipient.amount); + recipients.emplace_back(dest, recipient.amount, fSubtractFeeFromAmount); } } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 002d4b91e6a0..157a2adf8fc1 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -134,8 +134,10 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "rescanblockchain", 1, "stop_height"}, { "sendmany", 1, "amounts" }, { "sendmany", 2, "minconf" }, + { "sendmany", 5, "subtract_fee_from" }, { "sendrawtransaction", 1, "allowhighfees" }, { "sendtoaddress", 1, "amount" }, + { "sendtoaddress", 4, "subtract_fee" }, { "setautocombinethreshold", 0, "enable" }, { "setautocombinethreshold", 1, "threshold" }, { "setban", 2, "bantime" }, @@ -149,6 +151,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "shieldsendmany", 1, "amounts" }, { "shieldsendmany", 2, "minconf" }, { "shieldsendmany", 3, "fee" }, + { "shieldsendmany", 4, "subtract_fee_from" }, { "signrawtransaction", 1, "prevtxs" }, { "signrawtransaction", 2, "privkeys" }, { "spork", 1, "value" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 4669b0334a52..4c241861289e 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -484,127 +484,6 @@ static void TxInErrorToJSON(const CTxIn& txin, UniValue& vErrorsRet, const std:: vErrorsRet.push_back(entry); } -UniValue fundrawtransaction(const JSONRPCRequest& request) -{ - CWallet * const pwallet = GetWalletForJSONRPCRequest(request); - - if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) - return NullUniValue; - - if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) - throw std::runtime_error( - "fundrawtransaction \"hexstring\" ( options )\n" - "\nAdd inputs to a transaction until it has enough in value to meet its out value.\n" - "This will not modify existing inputs, and will add one change output to the outputs.\n" - "Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n" - "The inputs added will not be signed, use signrawtransaction for that.\n" - "Note that all existing inputs must have their previous output transaction be in the wallet.\n" - "Note that all inputs selected must be of standard form and P2SH scripts must be " - "in the wallet using importaddress or addmultisigaddress (to calculate fees).\n" - "You can see whether this is the case by checking the \"solvable\" field in the listunspent output.\n" - "Only pay-to-pubkey, multisig, and P2SH versions thereof are currently supported for watch-only\n" - - "\nArguments:\n" - "1. \"hexstring\" (string, required) The hex string of the raw transaction\n" - "2. options (object, optional)\n" - " {\n" - " \"changeAddress\" (string, optional, default pool address) The PIVX address to receive the change\n" - " \"changePosition\" (numeric, optional, default random) The index of the change output\n" - " \"includeWatching\" (boolean, optional, default false) Also select inputs which are watch only\n" - " \"lockUnspents\" (boolean, optional, default false) Lock selected unspent outputs\n" - " \"feeRate\" (numeric, optional, default 0=estimate) Set a specific feerate (PIV per KB)\n" - " }\n" - "\nResult:\n" - "{\n" - " \"hex\": \"value\", (string) The resulting raw transaction (hex-encoded string)\n" - " \"fee\": n, (numeric) The fee added to the transaction\n" - " \"changepos\": n (numeric) The position of the added change output, or -1\n" - "}\n" - "\"hex\" \n" - "\nExamples:\n" - "\nCreate a transaction with no inputs\n" - + HelpExampleCli("createrawtransaction", "\"[]\" \"{\\\"myaddress\\\":0.01}\"") + - "\nAdd sufficient unsigned inputs to meet the output value\n" - + HelpExampleCli("fundrawtransaction", "\"rawtransactionhex\"") + - "\nSign the transaction\n" - + HelpExampleCli("signrawtransaction", "\"fundedtransactionhex\"") + - "\nSend the transaction\n" - + HelpExampleCli("sendrawtransaction", "\"signedtransactionhex\"") - ); - - // Make sure the results are valid at least up to the most recent block - // the user could have gotten from another RPC command prior to now - pwallet->BlockUntilSyncedToCurrentChain(); - - RPCTypeCheck(request.params, {UniValue::VSTR}); - - CTxDestination changeAddress = CNoDestination(); - int changePosition = -1; - bool includeWatching = false; - bool lockUnspents = false; - CFeeRate feeRate = CFeeRate(0); - bool overrideEstimatedFeerate = false; - - if (request.params.size() > 1) { - RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VOBJ}); - UniValue options = request.params[1]; - RPCTypeCheckObj(options, - { - {"changeAddress", UniValueType(UniValue::VSTR)}, - {"changePosition", UniValueType(UniValue::VNUM)}, - {"includeWatching", UniValueType(UniValue::VBOOL)}, - {"lockUnspents", UniValueType(UniValue::VBOOL)}, - {"feeRate", UniValueType()}, // will be checked below - }, - true, true); - - if (options.exists("changeAddress")) { - changeAddress = DecodeDestination(options["changeAddress"].get_str()); - - if (!IsValidDestination(changeAddress)) - throw JSONRPCError(RPC_INVALID_PARAMETER, "changeAddress must be a valid PIVX address"); - } - - if (options.exists("changePosition")) - changePosition = options["changePosition"].get_int(); - - if (options.exists("includeWatching")) - includeWatching = options["includeWatching"].get_bool(); - - if (options.exists("lockUnspents")) - lockUnspents = options["lockUnspents"].get_bool(); - - if (options.exists("feeRate")) { - feeRate = CFeeRate(AmountFromValue(options["feeRate"])); - overrideEstimatedFeerate = true; - } - } - - // parse hex string from parameter - CMutableTransaction origTx; - if (!DecodeHexTx(origTx, request.params[0].get_str())) - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); - - if (origTx.vout.size() == 0) - throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output"); - - if (changePosition != -1 && (changePosition < 0 || (unsigned int) changePosition > origTx.vout.size())) - throw JSONRPCError(RPC_INVALID_PARAMETER, "changePosition out of bounds"); - - CMutableTransaction tx(origTx); - CAmount nFeeOut; - std::string strFailReason; - if(!pwallet->FundTransaction(tx, nFeeOut, overrideEstimatedFeerate, feeRate, changePosition, strFailReason, includeWatching, lockUnspents, changeAddress)) - throw JSONRPCError(RPC_INTERNAL_ERROR, strFailReason); - - UniValue result(UniValue::VOBJ); - result.pushKV("hex", EncodeHexTx(tx)); - result.pushKV("changepos", changePosition); - result.pushKV("fee", ValueFromAmount(nFeeOut)); - - return result; -} - UniValue signrawtransaction(const JSONRPCRequest& request) { CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -1000,7 +879,6 @@ static const CRPCCommand commands[] = { "rawtransactions", "createrawtransaction", &createrawtransaction, true, {"inputs","outputs","locktime"} }, { "rawtransactions", "decoderawtransaction", &decoderawtransaction, true, {"hexstring"} }, { "rawtransactions", "decodescript", &decodescript, true, {"hexstring"} }, - { "rawtransactions", "fundrawtransaction", &fundrawtransaction, false, {"hexstring","options"} }, { "rawtransactions", "getrawtransaction", &getrawtransaction, true, {"txid","verbose","blockhash"} }, { "rawtransactions", "sendrawtransaction", &sendrawtransaction, false, {"hexstring","allowhighfees"} }, { "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ diff --git a/src/rpc/rpcevo.cpp b/src/rpc/rpcevo.cpp index fb5d621ed573..bb2e46223f99 100644 --- a/src/rpc/rpcevo.cpp +++ b/src/rpc/rpcevo.cpp @@ -224,7 +224,7 @@ static void FundSpecialTx(CWallet* pwallet, CMutableTransaction& tx, SpecialTxPa int nChangePos = -1; std::string strFailReason; std::set setSubtractFeeFromOutputs; - if (!pwallet->FundTransaction(tx, nFee, false, feeRate, nChangePos, strFailReason, false, false)) + if (!pwallet->FundTransaction(tx, nFee, false, feeRate, nChangePos, strFailReason, false, false, {})) throw JSONRPCError(RPC_INTERNAL_ERROR, strFailReason); if (dummyTxOutAdded && tx.vout.size() > 1) { diff --git a/src/sapling/sapling_operation.cpp b/src/sapling/sapling_operation.cpp index 9d2a43cbcdfb..0b9b5e5e9b09 100644 --- a/src/sapling/sapling_operation.cpp +++ b/src/sapling/sapling_operation.cpp @@ -67,10 +67,11 @@ TxValues calculateTarget(const std::vector& recipients, const { TxValues txValues; for (const SendManyRecipient &t : recipients) { - if (t.IsTransparent()) - txValues.transOutTotal += t.transparentRecipient->nValue; - else - txValues.shieldedOutTotal += t.shieldedRecipient->amount; + if (t.IsTransparent()) { + txValues.transOutTotal += t.getAmount(); + } else { + txValues.shieldedOutTotal += t.getAmount(); + } } txValues.target = txValues.shieldedOutTotal + txValues.transOutTotal + fee; return txValues; @@ -123,11 +124,17 @@ OperationResult SaplingOperation::build() return errorOut("Minconf cannot be zero when sending from shielded address"); } + // Check outputs to subtract fee from + unsigned int nSubtractFeeFromAmount = 0; + for (const SendManyRecipient& rec : recipients) { + if (rec.IsSubtractFee()) nSubtractFeeFromAmount++; + } + CAmount nFeeRet = (fee > 0 ? fee : minRelayTxFee.GetFeePerK()); int tries = 0; while (true) { // First calculate target values - TxValues txValues = calculateTarget(recipients, nFeeRet); + TxValues txValues = calculateTarget(recipients, nSubtractFeeFromAmount == 0 ? nFeeRet : 0); OperationResult result(false); uint256 ovk; if (isFromShielded) { @@ -143,16 +150,27 @@ OperationResult SaplingOperation::build() } // Add outputs + bool fFirst = true; for (const SendManyRecipient &t : recipients) { + CAmount amount = t.getAmount(); + // Subtract from fee calculation + if (t.IsSubtractFee()) { + // Subtract fee equally from each selected recipient + amount -= nFeeRet / nSubtractFeeFromAmount; + if (fFirst) { + // first receiver pays the remainder not divisible by output count + fFirst = false; + amount -= nFeeRet % nSubtractFeeFromAmount; + } + } + // Append output if (t.IsTransparent()) { - txBuilder.AddTransparentOutput(*t.transparentRecipient); + txBuilder.AddTransparentOutput(CTxOut(amount, t.getScript())); } else { - const auto& address = t.shieldedRecipient->address; - const CAmount& amount = t.shieldedRecipient->amount; - const std::string& memo = t.shieldedRecipient->memo; + const auto& address = t.getSapPaymentAddr(); assert(IsValidPaymentAddress(address)); std::array vMemo = {}; - if (!(result = GetMemoFromString(memo, vMemo))) + if (!(result = GetMemoFromString(t.getMemo(), vMemo))) return result; txBuilder.AddSaplingOutput(ovk, address, amount, vMemo); } @@ -543,11 +561,11 @@ OperationResult CheckTransactionSize(std::vector& recipients, nTransparentOuts++; continue; } - if (IsValidPaymentAddress(t.shieldedRecipient->address)) { + if (IsValidPaymentAddress(t.getSapPaymentAddr())) { mtx.sapData->vShieldedOutput.emplace_back(); } else { return errorOut(strprintf("invalid recipient shielded address %s", - KeyIO::EncodePaymentAddress(t.shieldedRecipient->address))); + KeyIO::EncodePaymentAddress(t.getSapPaymentAddr()))); } } CTransaction tx(mtx); diff --git a/src/sapling/sapling_operation.h b/src/sapling/sapling_operation.h index 38ff8aa97877..6db2b01db7df 100644 --- a/src/sapling/sapling_operation.h +++ b/src/sapling/sapling_operation.h @@ -18,52 +18,56 @@ class CCoinControl; struct TxValues; -struct ShieldedRecipient +class ShieldedRecipient final : public CRecipientBase { +public: const libzcash::SaplingPaymentAddress address; - const CAmount amount; const std::string memo; - ShieldedRecipient(const libzcash::SaplingPaymentAddress& _address, const CAmount& _amount, const std::string& _memo) : - address(_address), - amount(_amount), - memo(_memo) - {} + ShieldedRecipient(const libzcash::SaplingPaymentAddress& _address, const CAmount& _amount, const std::string& _memo, bool _fSubtractFeeFromAmount) : + CRecipientBase(_amount, _fSubtractFeeFromAmount), address(_address), memo(_memo) {} + bool isTransparent() const override { return false; } + Optional getSapPaymentAddr() const override { return {address}; }; + std::string getMemo() const override { return memo; } }; struct SendManyRecipient { - const Optional shieldedRecipient{nullopt}; - const Optional transparentRecipient{nullopt}; + const std::shared_ptr recipient; - bool IsTransparent() const { return transparentRecipient != nullopt; } + bool IsTransparent() const { return recipient->isTransparent(); } + bool IsSubtractFee() const { return recipient->fSubtractFeeFromAmount; } + CAmount getAmount() const { return recipient->nAmount; }; + CScript getScript() const { assert(IsTransparent()); return *recipient->getScript(); } + libzcash::SaplingPaymentAddress getSapPaymentAddr() const { assert(!IsTransparent()); return *recipient->getSapPaymentAddr(); } + std::string getMemo() const { return recipient->getMemo(); } // Prevent default empty initialization SendManyRecipient() = delete; // Shielded recipient - SendManyRecipient(const libzcash::SaplingPaymentAddress& address, const CAmount& amount, const std::string& memo): - shieldedRecipient(ShieldedRecipient(address, amount, memo)) + SendManyRecipient(const libzcash::SaplingPaymentAddress& address, const CAmount& amount, const std::string& memo, bool fSubtractFeeFromAmount): + recipient(new ShieldedRecipient(address, amount, memo, fSubtractFeeFromAmount)) {} // Transparent recipient: P2PKH - SendManyRecipient(const CTxDestination& dest, const CAmount& amount): - transparentRecipient(CTxOut(amount, GetScriptForDestination(dest))) + SendManyRecipient(const CTxDestination& dest, const CAmount& amount, bool fSubtractFeeFromAmount): + recipient(new CRecipient(GetScriptForDestination(dest), amount, fSubtractFeeFromAmount)) {} // Transparent recipient: P2CS SendManyRecipient(const CKeyID& ownerKey, const CKeyID& stakerKey, const CAmount& amount, bool fV6Enforced): - transparentRecipient(CTxOut(amount, fV6Enforced ? GetScriptForStakeDelegation(stakerKey, ownerKey) - : GetScriptForStakeDelegationLOF(stakerKey, ownerKey))) + recipient(new CRecipient(fV6Enforced ? GetScriptForStakeDelegation(stakerKey, ownerKey) + : GetScriptForStakeDelegationLOF(stakerKey, ownerKey), amount, false)) {} // Transparent recipient: multisig SendManyRecipient(int nRequired, const std::vector& keys, const CAmount& amount): - transparentRecipient(CTxOut(amount, GetScriptForMultisig(nRequired, keys))) + recipient(new CRecipient(GetScriptForMultisig(nRequired, keys), amount, false)) {} // Transparent recipient: OP_RETURN SendManyRecipient(const uint256& message): - transparentRecipient(CTxOut(0, GetScriptForOpReturn(message))) + recipient(new CRecipient(GetScriptForOpReturn(message), 0, false)) {} }; diff --git a/src/test/librust/sapling_rpc_wallet_tests.cpp b/src/test/librust/sapling_rpc_wallet_tests.cpp index 281695547d56..b56ad0b32b6f 100644 --- a/src/test/librust/sapling_rpc_wallet_tests.cpp +++ b/src/test/librust/sapling_rpc_wallet_tests.cpp @@ -349,7 +349,7 @@ BOOST_AUTO_TEST_CASE(saplingOperationTests) // there are no utxos to spend { - std::vector recipients = { SendManyRecipient(zaddr1, COIN, "DEADBEEF") }; + std::vector recipients = { SendManyRecipient(zaddr1, COIN, "DEADBEEF", false) }; SaplingOperation operation(consensusParams, 1, pwalletMain.get()); operation.setFromAddress(taddr1); auto res = operation.setRecipients(recipients)->buildAndSend(ret); @@ -359,7 +359,7 @@ BOOST_AUTO_TEST_CASE(saplingOperationTests) // minconf cannot be zero when sending from zaddr { - std::vector recipients = { SendManyRecipient(zaddr1, COIN, "DEADBEEF") }; + std::vector recipients = { SendManyRecipient(zaddr1, COIN, "DEADBEEF", false) }; SaplingOperation operation(consensusParams, 1, pwalletMain.get()); operation.setFromAddress(zaddr1); auto res = operation.setRecipients(recipients)->setMinDepth(0)->buildAndSend(ret); @@ -369,7 +369,7 @@ BOOST_AUTO_TEST_CASE(saplingOperationTests) // there are no unspent notes to spend { - std::vector recipients = { SendManyRecipient(taddr1, COIN) }; + std::vector recipients = { SendManyRecipient(taddr1, COIN, false) }; SaplingOperation operation(consensusParams, 1, pwalletMain.get()); operation.setFromAddress(zaddr1); auto res = operation.setRecipients(recipients)->buildAndSend(ret); @@ -455,7 +455,7 @@ BOOST_AUTO_TEST_CASE(rpc_shieldsendmany_taddr_to_sapling) pwalletMain->BlockConnected(std::make_shared(block), mi->second); BOOST_CHECK_MESSAGE(pwalletMain->GetAvailableBalance() > 0, "tx not confirmed"); - std::vector recipients = { SendManyRecipient(zaddr1, 1 * COIN, "ABCD") }; + std::vector recipients = { SendManyRecipient(zaddr1, 1 * COIN, "ABCD", false) }; SaplingOperation operation(consensusParams, nextBlockHeight, pwalletMain.get()); operation.setFromAddress(taddr); BOOST_CHECK(operation.setRecipients(recipients) @@ -463,7 +463,7 @@ BOOST_AUTO_TEST_CASE(rpc_shieldsendmany_taddr_to_sapling) ->build()); // try from auto-selected transparent address - std::vector recipients2 = { SendManyRecipient(zaddr1, 1 * COIN, "ABCD") }; + std::vector recipients2 = { SendManyRecipient(zaddr1, 1 * COIN, "ABCD", false) }; SaplingOperation operation2(consensusParams, nextBlockHeight, pwalletMain.get()); BOOST_CHECK(operation2.setSelectTransparentCoins(true) ->setRecipients(recipients2) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index f9eeeb32496d..2e60550fc04a 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -997,7 +997,7 @@ UniValue setlabel(const JSONRPCRequest& request) return NullUniValue; } -static void SendMoney(CWallet* const pwallet, const CTxDestination& address, CAmount nValue, CTransactionRef& tx) +static void SendMoney(CWallet* const pwallet, const CTxDestination& address, CAmount nValue, bool fSubtractFeeFromAmount, CTransactionRef& tx) { LOCK2(cs_main, pwallet->cs_wallet); @@ -1018,8 +1018,12 @@ static void SendMoney(CWallet* const pwallet, const CTxDestination& address, CAm CReserveKey reservekey(pwallet); CAmount nFeeRequired; std::string strError; - if (!pwallet->CreateTransaction(scriptPubKey, nValue, tx, reservekey, nFeeRequired, strError, nullptr, (CAmount)0)) { - if (nValue + nFeeRequired > pwallet->GetAvailableBalance()) + std::vector vecSend; + int nChangePosRet = -1; + CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount}; + vecSend.push_back(recipient); + if (!pwallet->CreateTransaction(vecSend, tx, reservekey, nFeeRequired, nChangePosRet, strError)) { + if (!fSubtractFeeFromAmount && nValue + nFeeRequired > pwallet->GetAvailableBalance()) strError = strprintf("Error: This transaction requires a transaction fee of at least %s because of its amount, complexity, or use of recently received funds!", FormatMoney(nFeeRequired)); LogPrintf("%s: %s\n", __func__, strError); throw JSONRPCError(RPC_WALLET_ERROR, strError); @@ -1039,7 +1043,8 @@ static UniValue ShieldSendManyTo(CWallet * const pwallet, const std::string& commentStr, const std::string& toStr, int nMinDepth, - bool fIncludeDelegated) + bool fIncludeDelegated, + UniValue subtractFeeFromAmount) { // convert params to 'shieldsendmany' format JSONRPCRequest req; @@ -1058,6 +1063,8 @@ static UniValue ShieldSendManyTo(CWallet * const pwallet, } req.params.push_back(recipients); req.params.push_back(nMinDepth); + req.params.push_back(0); + req.params.push_back(subtractFeeFromAmount); // send SaplingOperation operation = CreateShieldedTransaction(pwallet, req); @@ -1086,9 +1093,9 @@ UniValue sendtoaddress(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() < 2 || request.params.size() > 4) + if (request.fHelp || request.params.size() < 2 || request.params.size() > 5) throw std::runtime_error( - "sendtoaddress \"address\" amount ( \"comment\" \"comment-to\" )\n" + "sendtoaddress \"address\" amount ( \"comment\" \"comment-to\" subtract_fee )\n" "\nSend an amount to a given address. The amount is a real and is rounded to the nearest 0.00000001\n" + HelpRequiringPassphrase(pwallet) + "\n" @@ -1100,6 +1107,8 @@ UniValue sendtoaddress(const JSONRPCRequest& request) "4. \"comment-to\" (string, optional) A comment to store the name of the person or organization \n" " to which you're sending the transaction. This is not part of the \n" " transaction, just kept in your wallet.\n" + "5. subtract_fee (boolean, optional, default=false) The fee will be deducted from the amount being sent.\n" + " The recipient will receive less bitcoins than you enter in the amount field.\n" "\nResult:\n" "\"transactionid\" (string) The transaction id.\n" @@ -1107,6 +1116,7 @@ UniValue sendtoaddress(const JSONRPCRequest& request) "\nExamples:\n" + HelpExampleCli("sendtoaddress", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" 0.1") + HelpExampleCli("sendtoaddress", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" 0.1 \"donation\" \"seans outpost\"") + + HelpExampleCli("sendtoaddress", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" 0.1 \"\" \"\" true") + HelpExampleRpc("sendtoaddress", "\"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\", 0.1, \"donation\", \"seans outpost\"")); EnsureWalletIsUnlocked(pwallet); @@ -1124,11 +1134,16 @@ UniValue sendtoaddress(const JSONRPCRequest& request) request.params[2].get_str() : ""; const std::string toStr = (request.params.size() > 3 && !request.params[3].isNull()) ? request.params[3].get_str() : ""; + bool fSubtractFeeFromAmount = request.params.size() > 4 && request.params[4].get_bool(); if (isShielded) { UniValue sendTo(UniValue::VOBJ); sendTo.pushKV(addrStr, request.params[1]); - return ShieldSendManyTo(pwallet, sendTo, commentStr, toStr, 1, false); + UniValue subtractFeeFromAmount(UniValue::VARR); + if (fSubtractFeeFromAmount) { + subtractFeeFromAmount.push_back(addrStr); + } + return ShieldSendManyTo(pwallet, sendTo, commentStr, toStr, 1, false, subtractFeeFromAmount); } const CTxDestination& address = *Standard::GetTransparentDestination(destination); @@ -1137,7 +1152,7 @@ UniValue sendtoaddress(const JSONRPCRequest& request) CAmount nAmount = AmountFromValue(request.params[1]); CTransactionRef tx; - SendMoney(pwallet, address, nAmount, tx); + SendMoney(pwallet, address, nAmount, fSubtractFeeFromAmount, tx); // Wallet comments CWalletTx& wtx = pwallet->mapWallet.at(tx->GetHash()); @@ -1655,6 +1670,9 @@ static SaplingOperation CreateShieldedTransaction(CWallet* const pwallet, const } } + // Param 4: subtractFeeFromAmount addresses + const UniValue subtractFeeFromAmount = request.params[4]; + // Param 1: array of outputs UniValue outputs = request.params[1].get_array(); if (outputs.empty()) @@ -1712,10 +1730,19 @@ static SaplingOperation CreateShieldedTransaction(CWallet* const pwallet, const if (nAmount < 0) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive"); + bool fSubtractFeeFromAmount = false; + for (unsigned int idx = 0; idx < subtractFeeFromAmount.size(); idx++) { + const UniValue& addr = subtractFeeFromAmount[idx]; + if (addr.get_str() == address) { + fSubtractFeeFromAmount = true; + break; + } + } + if (saddr) { - recipients.emplace_back(*saddr, nAmount, memo); + recipients.emplace_back(*saddr, nAmount, memo, fSubtractFeeFromAmount); } else { - recipients.emplace_back(taddr, nAmount); + recipients.emplace_back(taddr, nAmount, fSubtractFeeFromAmount); } nTotalOut += nAmount; @@ -1745,11 +1772,13 @@ static SaplingOperation CreateShieldedTransaction(CWallet* const pwallet, const // If not set, SaplingOperation will set the minimum fee (based on minRelayFee and tx size) if (request.params.size() > 3) { CAmount nFee = AmountFromValue(request.params[3]); - if (nFee <= 0) { + if (nFee < 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid fee. Must be positive."); + } else if (nFee > 0) { + // If the user-selected fee is not enough (or too much), the build operation will fail. + operation.setFee(nFee); } - // If the user-selected fee is not enough (or too much), the build operation will fail. - operation.setFee(nFee); + // If nFee=0 leave the default (build operation will compute the minimum fee) } if (fromSapling && nMinDepth == 0) { @@ -1775,9 +1804,9 @@ UniValue shieldsendmany(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() < 2 || request.params.size() > 4) + if (request.fHelp || request.params.size() < 2 || request.params.size() > 5) throw std::runtime_error( - "shieldsendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf fee )\n" + "shieldsendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf fee subtract_fee_from )\n" "\nSend to many recipients. Amounts are decimal numbers with at most 8 digits of precision." "\nChange generated from a transparent addr flows to a new transparent addr address, while change generated from a shield addr returns to itself." "\nWhen sending coinbase UTXOs to a shield addr, change is not allowed. The entire value of the UTXO(s) must be consumed." @@ -1797,8 +1826,16 @@ UniValue shieldsendmany(const JSONRPCRequest& request) " }, ... ]\n" "3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n" "4. fee (numeric, optional), The fee amount to attach to this transaction.\n" - " If not specified, the wallet will try to compute the minimum possible fee for a shield TX,\n" + " If not specified, or set to 0, the wallet will try to compute the minimum possible fee for a shield TX,\n" " based on the expected transaction size and the current value of -minRelayTxFee.\n" + "5. subtract_fee_from (array, optional) A json array with addresses.\n" + " The fee will be equally deducted from the amount of each selected address.\n" + " Those recipients will receive less PIV than you enter in their corresponding amount field.\n" + " If no addresses are specified here, the sender pays the fee.\n" + " [\n" + " \"address\" (string) Subtract fee from this address\n" + " ,...\n" + " ]\n" "\nResult:\n" "\"id\" (string) transaction hash in the network\n" "\nExamples:\n" @@ -2253,7 +2290,7 @@ UniValue getunconfirmedbalance(const JSONRPCRequest& request) /* * Only used for t->t transactions (via sendmany RPC) */ -static UniValue legacy_sendmany(CWallet* const pwallet, const UniValue& sendTo, int nMinDepth, std::string comment, bool fIncludeDelegated) +static UniValue legacy_sendmany(CWallet* const pwallet, const UniValue& sendTo, int nMinDepth, std::string comment, bool fIncludeDelegated, const UniValue& subtractFeeFromAmount) { LOCK2(cs_main, pwallet->cs_wallet); @@ -2282,7 +2319,16 @@ static UniValue legacy_sendmany(CWallet* const pwallet, const UniValue& sendTo, CAmount nAmount = AmountFromValue(sendTo[name_]); totalAmount += nAmount; - vecSend.emplace_back(scriptPubKey, nAmount, false); + bool fSubtractFeeFromAmount = false; + for (unsigned int idx = 0; idx < subtractFeeFromAmount.size(); idx++) { + const UniValue& addr = subtractFeeFromAmount[idx]; + if (addr.get_str() == name_) { + fSubtractFeeFromAmount = true; + break; + } + } + + vecSend.emplace_back(scriptPubKey, nAmount, fSubtractFeeFromAmount); } // Check funds @@ -2326,7 +2372,7 @@ UniValue sendmany(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() < 2 || request.params.size() > 5) + if (request.fHelp || request.params.size() < 2 || request.params.size() > 6) throw std::runtime_error( "sendmany \"\" {\"address\":amount,...} ( minconf \"comment\" include_delegated )\n" "\nSend to multiple destinations. Recipients are transparent or shield PIVX addresses.\n" @@ -2344,6 +2390,14 @@ UniValue sendmany(const JSONRPCRequest& request) "3. minconf (numeric, optional, default=1) Only use the balance confirmed at least this many times.\n" "4. \"comment\" (string, optional) A comment\n" "5. include_delegated (bool, optional, default=false) Also include balance delegated to cold stakers\n" + "6. subtract_fee_from (array, optional) A json array with addresses.\n" + " The fee will be equally deducted from the amount of each selected address.\n" + " Those recipients will receive less PIV than you enter in their corresponding amount field.\n" + " If no addresses are specified here, the sender pays the fee.\n" + " [\n" + " \"address\" (string) Subtract fee from this address\n" + " ,...\n" + " ]\n" "\nResult:\n" "\"transactionid\" (string) The transaction id for the send. Only 1 transaction is created regardless of \n" @@ -2374,7 +2428,11 @@ UniValue sendmany(const JSONRPCRequest& request) const int nMinDepth = request.params.size() > 2 ? request.params[2].get_int() : 1; const std::string comment = (request.params.size() > 3 && !request.params[3].isNull() && !request.params[3].get_str().empty()) ? request.params[3].get_str() : ""; - const bool fIncludeDelegated = (request.params.size() > 5 && request.params[5].get_bool()); + const bool fIncludeDelegated = (request.params.size() > 4 && request.params[4].get_bool()); + + UniValue subtractFeeFromAmount(UniValue::VARR); + if (request.params.size() > 5 && !request.params[5].isNull()) + subtractFeeFromAmount = request.params[5].get_array(); // Check if any recipient address is shield bool fShieldSend = false; @@ -2388,11 +2446,11 @@ UniValue sendmany(const JSONRPCRequest& request) } if (fShieldSend) { - return ShieldSendManyTo(pwallet, sendTo, comment, "", nMinDepth, fIncludeDelegated); + return ShieldSendManyTo(pwallet, sendTo, comment, "", nMinDepth, fIncludeDelegated, subtractFeeFromAmount); } // All recipients are transparent: use Legacy sendmany t->t - return legacy_sendmany(pwallet, sendTo, nMinDepth, comment, fIncludeDelegated); + return legacy_sendmany(pwallet, sendTo, nMinDepth, comment, fIncludeDelegated, subtractFeeFromAmount); } // Defined in rpc/misc.cpp @@ -3792,6 +3850,154 @@ UniValue listunspent(const JSONRPCRequest& request) return results; } +UniValue fundrawtransaction(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) + return NullUniValue; + + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) + throw std::runtime_error( + "fundrawtransaction \"hexstring\" ( options )\n" + "\nAdd inputs to a transaction until it has enough in value to meet its out value.\n" + "This will not modify existing inputs, and will add one change output to the outputs.\n" + "Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n" + "The inputs added will not be signed, use signrawtransaction for that.\n" + "Note that all existing inputs must have their previous output transaction be in the wallet.\n" + "Note that all inputs selected must be of standard form and P2SH scripts must be " + "in the wallet using importaddress or addmultisigaddress (to calculate fees).\n" + "You can see whether this is the case by checking the \"solvable\" field in the listunspent output.\n" + "Only pay-to-pubkey, multisig, and P2SH versions thereof are currently supported for watch-only\n" + + "\nArguments:\n" + "1. \"hexstring\" (string, required) The hex string of the raw transaction\n" + "2. options (object, optional)\n" + " {\n" + " \"changeAddress\" (string, optional, default pool address) The PIVX address to receive the change\n" + " \"changePosition\" (numeric, optional, default random) The index of the change output\n" + " \"includeWatching\" (boolean, optional, default false) Also select inputs which are watch only\n" + " \"lockUnspents\" (boolean, optional, default false) Lock selected unspent outputs\n" + " \"feeRate\" (numeric, optional, default 0=estimate) Set a specific feerate (PIV per KB)\n" + " \"subtractFeeFromOutputs\" (array, optional) A json array of integers.\n" + " The fee will be equally deducted from the amount of each specified output.\n" + " The outputs are specified by their zero-based index, before any change output is added.\n" + " Those recipients will receive less PIV than you enter in their corresponding amount field.\n" + " If no outputs are specified here, the sender pays the fee.\n" + " [vout_index,...]\n" + " }\n" + "\nResult:\n" + "{\n" + " \"hex\": \"value\", (string) The resulting raw transaction (hex-encoded string)\n" + " \"fee\": n, (numeric) The fee added to the transaction\n" + " \"changepos\": n (numeric) The position of the added change output, or -1\n" + "}\n" + "\"hex\" \n" + "\nExamples:\n" + "\nCreate a transaction with no inputs\n" + + HelpExampleCli("createrawtransaction", "\"[]\" \"{\\\"myaddress\\\":0.01}\"") + + "\nAdd sufficient unsigned inputs to meet the output value\n" + + HelpExampleCli("fundrawtransaction", "\"rawtransactionhex\"") + + "\nSign the transaction\n" + + HelpExampleCli("signrawtransaction", "\"fundedtransactionhex\"") + + "\nSend the transaction\n" + + HelpExampleCli("sendrawtransaction", "\"signedtransactionhex\"") + ); + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + RPCTypeCheck(request.params, {UniValue::VSTR}); + + CTxDestination changeAddress = CNoDestination(); + int changePosition = -1; + bool includeWatching = false; + bool lockUnspents = false; + UniValue subtractFeeFromOutputs; + std::set setSubtractFeeFromOutputs; + CFeeRate feeRate = CFeeRate(0); + bool overrideEstimatedFeerate = false; + + if (request.params.size() > 1) { + RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VOBJ}); + UniValue options = request.params[1]; + RPCTypeCheckObj(options, + { + {"changeAddress", UniValueType(UniValue::VSTR)}, + {"changePosition", UniValueType(UniValue::VNUM)}, + {"includeWatching", UniValueType(UniValue::VBOOL)}, + {"lockUnspents", UniValueType(UniValue::VBOOL)}, + {"feeRate", UniValueType()}, // will be checked below + {"subtractFeeFromOutputs", UniValueType(UniValue::VARR)}, + }, + true, true); + + if (options.exists("changeAddress")) { + changeAddress = DecodeDestination(options["changeAddress"].get_str()); + + if (!IsValidDestination(changeAddress)) + throw JSONRPCError(RPC_INVALID_PARAMETER, "changeAddress must be a valid PIVX address"); + } + + if (options.exists("changePosition")) + changePosition = options["changePosition"].get_int(); + + if (options.exists("includeWatching")) + includeWatching = options["includeWatching"].get_bool(); + + if (options.exists("lockUnspents")) + lockUnspents = options["lockUnspents"].get_bool(); + + if (options.exists("feeRate")) { + feeRate = CFeeRate(AmountFromValue(options["feeRate"])); + overrideEstimatedFeerate = true; + } + + if (options.exists("subtractFeeFromOutputs")) { + subtractFeeFromOutputs = options["subtractFeeFromOutputs"].get_array(); + } + } + + // parse hex string from parameter + CMutableTransaction origTx; + if (!DecodeHexTx(origTx, request.params[0].get_str())) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + + if (origTx.vout.size() == 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output"); + + if (changePosition != -1 && (changePosition < 0 || (unsigned int) changePosition > origTx.vout.size())) + throw JSONRPCError(RPC_INVALID_PARAMETER, "changePosition out of bounds"); + + for (unsigned int idx = 0; idx < subtractFeeFromOutputs.size(); idx++) { + int pos = subtractFeeFromOutputs[idx].get_int(); + if (setSubtractFeeFromOutputs.count(pos)) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos)); + if (pos < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos)); + if (pos >= int(origTx.vout.size())) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos)); + setSubtractFeeFromOutputs.insert(pos); + } + + CMutableTransaction tx(origTx); + CAmount nFeeOut; + std::string strFailReason; + if(!pwallet->FundTransaction(tx, nFeeOut, overrideEstimatedFeerate, feeRate, + changePosition, strFailReason, includeWatching, + lockUnspents, setSubtractFeeFromOutputs, changeAddress)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, strFailReason); + } + + UniValue result(UniValue::VOBJ); + result.pushKV("hex", EncodeHexTx(tx)); + result.pushKV("changepos", changePosition); + result.pushKV("fee", ValueFromAmount(nFeeOut)); + + return result; +} + UniValue lockunspent(const JSONRPCRequest& request) { CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -4523,6 +4729,7 @@ static const CRPCCommand commands[] = { "wallet", "dumpprivkey", &dumpprivkey, true, {"address"} }, { "wallet", "dumpwallet", &dumpwallet, true, {"filename"} }, { "wallet", "encryptwallet", &encryptwallet, true, {"passphrase"} }, + { "wallet", "fundrawtransaction", &fundrawtransaction, false, {"hexstring","options"} }, { "wallet", "getbalance", &getbalance, false, {"minconf","include_watchonly","include_delegated","include_shield"} }, { "wallet", "getcoldstakingbalance", &getcoldstakingbalance, false, {} }, { "wallet", "getdelegatedbalance", &getdelegatedbalance, false, {} }, @@ -4555,8 +4762,8 @@ static const CRPCCommand commands[] = { "wallet", "listwallets", &listwallets, true, {} }, { "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} }, { "wallet", "rawdelegatestake", &rawdelegatestake, false, {"staking_addr","amount","owner_addr","ext_owner","include_delegated","from_shield","force"} }, - { "wallet", "sendmany", &sendmany, false, {"dummy","amounts","minconf","comment","include_delegated"} }, - { "wallet", "sendtoaddress", &sendtoaddress, false, {"address","amount","comment","comment-to"} }, + { "wallet", "sendmany", &sendmany, false, {"dummy","amounts","minconf","comment","include_delegated","subtract_fee_from"} }, + { "wallet", "sendtoaddress", &sendtoaddress, false, {"address","amount","comment","comment-to","subtract_fee"} }, { "wallet", "settxfee", &settxfee, true, {"amount"} }, { "wallet", "setstakesplitthreshold", &setstakesplitthreshold, false, {"value"} }, { "wallet", "signmessage", &signmessage, true, {"address","message"} }, @@ -4579,7 +4786,7 @@ static const CRPCCommand commands[] = { "wallet", "getshieldbalance", &getshieldbalance, false, {"address","minconf","include_watchonly"} }, { "wallet", "listshieldunspent", &listshieldunspent, false, {"minconf","maxconf","include_watchonly","addresses"} }, { "wallet", "rawshieldsendmany", &rawshieldsendmany, false, {"fromaddress","amounts","minconf","fee"} }, - { "wallet", "shieldsendmany", &shieldsendmany, false, {"fromaddress","amounts","minconf","fee"} }, + { "wallet", "shieldsendmany", &shieldsendmany, false, {"fromaddress","amounts","minconf","fee","subtract_fee_from"} }, { "wallet", "listreceivedbyshieldaddress", &listreceivedbyshieldaddress, false, {"address","minconf"} }, { "wallet", "viewshieldtransaction", &viewshieldtransaction, false, {"txid"} }, { "wallet", "getsaplingnotescount", &getsaplingnotescount, false, {"minconf"} }, diff --git a/src/wallet/test/wallet_sapling_transactions_validations_tests.cpp b/src/wallet/test/wallet_sapling_transactions_validations_tests.cpp index 6e026fba8f28..76c99f72ecf4 100644 --- a/src/wallet/test/wallet_sapling_transactions_validations_tests.cpp +++ b/src/wallet/test/wallet_sapling_transactions_validations_tests.cpp @@ -110,7 +110,7 @@ BOOST_AUTO_TEST_CASE(test_in_block_and_mempool_notes_double_spend) // single recipient std::vector recipients; libzcash::SaplingPaymentAddress pa = pwalletMain->GenerateNewSaplingZKey("sapling1"); - recipients.emplace_back(pa, CAmount(100 * COIN), ""); + recipients.emplace_back(pa, CAmount(100 * COIN), "", false); // Create the operation and build the transaction SaplingOperation operation = createOperationAndBuildTx(pwalletMain, recipients, tipHeight + 1, true); @@ -135,14 +135,14 @@ BOOST_AUTO_TEST_CASE(test_in_block_and_mempool_notes_double_spend) CTxDestination tDest2; pwalletMain->getNewAddress(tDest2, "receiveValid"); std::vector recipients2; - recipients2.emplace_back(tDest2, CAmount(90 * COIN)); + recipients2.emplace_back(tDest2, CAmount(90 * COIN), false); SaplingOperation operation2 = createOperationAndBuildTx(pwalletMain, recipients2, tipHeight + 1, false); // Create a second transaction that spends the same note with a different output now CTxDestination tDest3; pwalletMain->getNewAddress(tDest3, "receiveInvalid"); std::vector recipients3; - recipients3.emplace_back(tDest3, CAmount(5 * COIN)); + recipients3.emplace_back(tDest3, CAmount(5 * COIN), false); SaplingOperation operation3 = createOperationAndBuildTx(pwalletMain, recipients3, tipHeight + 1, false); // Now that both transactions were created, broadcast the first one diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 3e084d7eda06..3cbb4d3038e6 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2937,13 +2937,14 @@ bool CWallet::CreateBudgetFeeTX(CTransactionRef& tx, const uint256& hash, CReser return true; } -bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const CTxDestination& destChange) +bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const std::set& setSubtractFeeFromOutputs, const CTxDestination& destChange) { std::vector vecSend; - // Turn the txout set into a CRecipient vector - for (const CTxOut& txOut : tx.vout) { - vecSend.emplace_back(txOut.scriptPubKey, txOut.nValue, false); + // Turn the txout set into a CRecipient vector. + for (size_t idx = 0; idx < tx.vout.size(); idx++) { + const CTxOut& txOut = tx.vout[idx]; + vecSend.emplace_back(txOut.scriptPubKey, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1); } CCoinControl coinControl; @@ -2977,6 +2978,12 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool ov reservekey.KeepKey(); } + // Copy output sizes from new transaction; they may have had the fee + // subtracted from them. + for (unsigned int idx = 0; idx < tx.vout.size(); idx++) { + tx.vout[idx].nValue = wtx->vout[idx].nValue; + } + // Add new txins while keeping original txin scriptSig/order. for (const CTxIn& txin : wtx->vin) { if (!coinControl.IsSelected(txin.prevout)) { @@ -3003,16 +3010,18 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, { CAmount nValue = 0; int nChangePosRequest = nChangePosInOut; - + unsigned int nSubtractFeeFromAmount = 0; for (const CRecipient& rec : vecSend) { - if (rec.nAmount < 0) { + if (nValue < 0 || rec.nAmount < 0) { strFailReason = _("Transaction amounts must be positive"); return false; } nValue += rec.nAmount; + if (rec.fSubtractFeeFromAmount) + nSubtractFeeFromAmount++; } - if (vecSend.empty() || nValue < 0) { - strFailReason = _("Transaction amounts must be positive"); + if (vecSend.empty()) { + strFailReason = _("Transaction must have at least one recipient"); return false; } @@ -3036,11 +3045,25 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, nChangePosInOut = nChangePosRequest; txNew.vin.clear(); txNew.vout.clear(); - CAmount nTotalValue = nValue + nFeeRet; + bool fFirst = true; + + CAmount nValueToSelect = nValue; + if (nSubtractFeeFromAmount == 0) + nValueToSelect += nFeeRet; // Fill outputs for (const CRecipient& rec : vecSend) { CTxOut txout(rec.nAmount, rec.scriptPubKey); + if (rec.fSubtractFeeFromAmount) { + assert(nSubtractFeeFromAmount != 0); + txout.nValue -= nFeeRet / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient + + if (fFirst) { + // first receiver pays the remainder not divisible by output count + fFirst = false; + txout.nValue -= nFeeRet % nSubtractFeeFromAmount; + } + } if (IsDust(txout, dustRelayFee)) { strFailReason = _("Transaction amount too small"); return false; @@ -3052,13 +3075,13 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, CAmount nValueIn = 0; setCoins.clear(); - if (!SelectCoinsToSpend(vAvailableCoins, nTotalValue, setCoins, nValueIn, coinControl)) { + if (!SelectCoinsToSpend(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coinControl)) { strFailReason = _("Insufficient funds."); return false; } // Change - CAmount nChange = nValueIn - nValue - nFeeRet; + CAmount nChange = nValueIn - nValueToSelect; if (nChange > 0) { // Fill a vout to ourself // TODO: pass in scriptChange instead of reservekey so diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 3b354245dfd8..7c8ab44df549 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -206,17 +206,26 @@ class CStakerStatus bool IsActive() const { return (nTime + 30) >= GetTime(); } }; -struct CRecipient -{ - CScript scriptPubKey; +class CRecipientBase { +public: CAmount nAmount; bool fSubtractFeeFromAmount; + CRecipientBase(const CAmount& _nAmount, bool _fSubtractFeeFromAmount) : + nAmount(_nAmount), fSubtractFeeFromAmount(_fSubtractFeeFromAmount) {} + virtual bool isTransparent() const { return true; }; + virtual Optional getScript() const { return nullopt; } + virtual Optional getSapPaymentAddr() const { return nullopt; } + virtual std::string getMemo() const { return ""; } +}; - CRecipient(const CScript& _scriptPubKey, const CAmount& _nAmount, bool _fSubtractFeeFromAmount): - scriptPubKey(_scriptPubKey), - nAmount(_nAmount), - fSubtractFeeFromAmount(_fSubtractFeeFromAmount) - {} +class CRecipient final : public CRecipientBase +{ +public: + CScript scriptPubKey; + CRecipient(const CScript& _scriptPubKey, const CAmount& _nAmount, bool _fSubtractFeeFromAmount) : + CRecipientBase(_nAmount, _fSubtractFeeFromAmount), scriptPubKey(_scriptPubKey) {} + bool isTransparent() const override { return true; } + Optional getScript() const override { return {scriptPubKey}; } }; class CAddressBookIterator @@ -1026,7 +1035,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface CAmount GetUnconfirmedWatchOnlyBalance() const; CAmount GetImmatureWatchOnlyBalance() const; CAmount GetLegacyBalance(const isminefilter& filter, int minDepth) const; - bool FundTransaction(CMutableTransaction& tx, CAmount &nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const CTxDestination& destChange = CNoDestination()); + bool FundTransaction(CMutableTransaction& tx, CAmount &nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const std::set& setSubtractFeeFromOutputs, const CTxDestination& destChange = CNoDestination()); /** * Create a new transaction paying the recipients with a set of coins * selected by SelectCoins(); Also create the change output, when needed diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index d00dcfd880d3..6913386dbf0f 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -9,6 +9,7 @@ assert_equal, assert_raises_rpc_error, assert_greater_than, + assert_greater_than_or_equal, connect_nodes, count_bytes, find_vout_for_address, @@ -77,9 +78,8 @@ def run_test(self): self.sync_all() # ensure that setting changePosition in fundraw with an exact match is handled properly - out_no_change = 250.0 - 0.0000374 # one coinbase input minus min fee - rawmatch = self.nodes[0].createrawtransaction([], {self.nodes[2].getnewaddress(): DecimalAmt(out_no_change)}) - rawmatch = self.nodes[0].fundrawtransaction(rawmatch, {"changePosition": 1}) + rawmatch = self.nodes[1].createrawtransaction([], {self.nodes[2].getnewaddress(): DecimalAmt(250.0)}) + rawmatch = self.nodes[1].fundrawtransaction(rawmatch, {"changePosition": 1, "subtractFeeFromOutputs": [0]}) assert_equal(rawmatch["changepos"], -1) watchonly_address = self.nodes[0].getnewaddress() @@ -127,6 +127,7 @@ def run_test(self): self.test_watchonly() self.test_all_watched_funds() self.test_option_feerate() + self.test_option_subtract_fee_from_outputs() def test_simple(self): self.log.info("simple test") @@ -521,6 +522,74 @@ def test_option_feerate(self): assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate) assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate) + def test_option_subtract_fee_from_outputs(self): + self.log.info("Test fundrawtxn subtractFeeFromOutputs option") + + # Make sure there is exactly one input so coin selection can't skew the result. + assert_equal(len(self.nodes[3].listunspent(1)), 1) + + inputs = [] + outputs = {self.nodes[2].getnewaddress(): 1} + rawtx = self.nodes[3].createrawtransaction(inputs, outputs) + + # Test subtract fee from outputs with feeRate + result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by settxfee) + self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list + self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee) + self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee}), + self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee, "subtractFeeFromOutputs": [0]}),] + dec_tx = [self.nodes[3].decoderawtransaction(tx_['hex']) for tx_ in result] + output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)] + change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)] + + assert_equal(result[0]['fee'], result[1]['fee'], result[2]['fee']) + assert_equal(result[3]['fee'], result[4]['fee']) + assert_equal(change[0], change[1]) + assert_equal(output[0], output[1]) + assert_equal(output[0], output[2] + result[2]['fee']) + assert_equal(change[0] + result[0]['fee'], change[2]) + assert_equal(output[3], output[4] + result[4]['fee']) + assert_equal(change[3] + result[3]['fee'], change[4]) + + inputs = [] + outputs = {self.nodes[2].getnewaddress(): value for value in (1.0, 1.1, 1.2, 1.3)} + rawtx = self.nodes[3].createrawtransaction(inputs, outputs) + + result = [self.nodes[3].fundrawtransaction(rawtx), + # split the fee between outputs 0, 2, and 3, but not output 1 + self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0, 2, 3]})] + + dec_tx = [self.nodes[3].decoderawtransaction(result[0]['hex']), + self.nodes[3].decoderawtransaction(result[1]['hex'])] + + # Nested list of non-change output amounts for each transaction. + output = [[out['value'] for i, out in enumerate(d['vout']) if i != r['changepos']] + for d, r in zip(dec_tx, result)] + + # List of differences in output amounts between normal and subtractFee transactions. + share = [o0 - o1 for o0, o1 in zip(output[0], output[1])] + + # Output 1 is the same in both transactions. + assert_equal(share[1], 0) + + # The other 3 outputs are smaller as a result of subtractFeeFromOutputs. + assert_greater_than(share[0], 0) + assert_greater_than(share[2], 0) + assert_greater_than(share[3], 0) + + # Outputs 2 and 3 take the same share of the fee. + assert_equal(share[2], share[3]) + + # Output 0 takes at least as much share of the fee, and no more than 2 + # satoshis more, than outputs 2 and 3. + assert_greater_than_or_equal(share[0], share[2]) + assert_greater_than_or_equal(share[2] + Decimal(2e-8), share[0]) + + # The fee is the same in both transactions. + assert_equal(result[0]['fee'], result[1]['fee']) + + # The total subtracted from the outputs is equal to the fee. + assert_equal(share[0] + share[2] + share[3], result[0]['fee']) if __name__ == '__main__': diff --git a/test/functional/sapling_wallet_send.py b/test/functional/sapling_wallet_send.py new file mode 100755 index 000000000000..89334bd8b471 --- /dev/null +++ b/test/functional/sapling_wallet_send.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Zcash developers +# Copyright (c) 2020 The PIVX developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +from test_framework.test_framework import PivxTestFramework +from test_framework.util import ( + assert_equal, +) + +from decimal import Decimal + +class SaplingWalletSend(PivxTestFramework): + + def set_test_params(self): + self.num_nodes = 3 + self.setup_clean_chain = True + saplingUpgrade = ['-nuparams=v5_shield:201'] + self.extra_args = [saplingUpgrade, saplingUpgrade, saplingUpgrade] + + def run_test(self): + self.log.info("Mining...") + self.nodes[0].generate(2) + self.sync_all() + self.nodes[2].generate(200) + self.sync_all() + assert_equal(self.nodes[1].getblockcount(), 202) + taddr1 = self.nodes[1].getnewaddress() + saplingAddr1 = self.nodes[1].getnewshieldaddress() + + # Verify addresses + assert(saplingAddr1 in self.nodes[1].listshieldaddresses()) + assert_equal(self.nodes[1].getshieldbalance(saplingAddr1), Decimal('0')) + assert_equal(self.nodes[1].getreceivedbyaddress(taddr1), Decimal('0')) + + # Test subtract fee from recipient + self.log.info("Checking sendto[shield]address with subtract-fee-from-amt") + node_0_bal = self.nodes[0].getbalance() + node_1_bal = self.nodes[1].getbalance() + txid = self.nodes[0].sendtoaddress(saplingAddr1, 10, "", "", True) + node_0_bal -= Decimal('10') + assert_equal(self.nodes[0].getbalance(), node_0_bal) + self.sync_mempools() + self.nodes[2].generate(1) + self.sync_all() + feeTx = self.nodes[0].gettransaction(txid)["fee"] # fee < 0 + saplingAddr1_bal = (Decimal('10') + feeTx) + node_1_bal += saplingAddr1_bal + assert_equal(self.nodes[1].getbalance(), node_1_bal) + + self.log.info("Checking shieldsendmany with subtract-fee-from-amt") + node_2_bal = self.nodes[2].getbalance() + recipients1 = [{"address": saplingAddr1, "amount": Decimal('10')}, + {"address": self.nodes[0].getnewshieldaddress(), "amount": Decimal('5')}] + subtractfeefrom = [saplingAddr1] + txid = self.nodes[2].shieldsendmany("from_transparent", recipients1, 1, 0, subtractfeefrom) + node_2_bal -= Decimal('15') + assert_equal(self.nodes[2].getbalance(), node_2_bal) + self.nodes[2].generate(1) + self.sync_all() + feeTx = self.nodes[2].gettransaction(txid)["fee"] # fee < 0 + node_1_bal += (Decimal('10') + feeTx) + saplingAddr1_bal += (Decimal('10') + feeTx) + assert_equal(self.nodes[1].getbalance(), node_1_bal) + node_0_bal += Decimal('5') + assert_equal(self.nodes[0].getbalance(), node_0_bal) + + self.log.info("Checking sendmany to shield with subtract-fee-from-amt") + node_2_bal = self.nodes[2].getbalance() + txid = self.nodes[2].sendmany('', {saplingAddr1: 10, taddr1: 10}, + 1, "", False, [saplingAddr1, taddr1]) + node_2_bal -= Decimal('20') + assert_equal(self.nodes[2].getbalance(), node_2_bal) + self.nodes[2].generate(1) + self.sync_all() + feeTx = self.nodes[2].gettransaction(txid)["fee"] # fee < 0 + node_1_bal += (Decimal('20') + feeTx) + assert_equal(self.nodes[1].getbalance(), node_1_bal) + taddr1_bal = Decimal('10') + feeTx/2 + saplingAddr1_bal += Decimal('10') + feeTx / 2 + assert_equal(self.nodes[1].getreceivedbyaddress(taddr1), taddr1_bal) + assert_equal(self.nodes[1].getshieldbalance(saplingAddr1), saplingAddr1_bal) + + +if __name__ == '__main__': + SaplingWalletSend().main() \ No newline at end of file diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index b90a8e482608..8e320c490310 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -159,6 +159,7 @@ 'sapling_wallet_nullifiers.py', # ~ 190 sec 'sapling_wallet_listreceived.py', # ~ 157 sec 'sapling_changeaddresses.py', # ~ 151 sec + 'sapling_wallet_send.py', # ~ 126 sec 'sapling_mempool.py', # ~ 98 sec 'sapling_wallet_persistence.py', # ~ 90 sec 'sapling_supply.py', # ~ 58 sec diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 3d5b04d62730..ddea9344ed8a 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -138,10 +138,11 @@ def run_test(self): assert_equal(node_2_bal, node_2_expected_bal) # Send 10 PIV normal + self.log.info("test sendtoaddress") address = self.nodes[0].getnewaddress("test") self.nodes[2].settxfee(float(fee_per_kbyte)) txid = self.nodes[2].sendtoaddress(address, 10, "", "") - fee = self.nodes[2].gettransaction(txid)["fee"] + fee = self.nodes[2].gettransaction(txid)["fee"] # fee < 0 node_2_bal -= (Decimal('10') - fee) assert_equal(self.nodes[2].getbalance(), node_2_bal) self.nodes[2].generate(1) @@ -150,6 +151,7 @@ def run_test(self): assert_equal(node_0_bal, Decimal('10')) # Sendmany 10 PIV + self.log.info("test sendmany") txid = self.nodes[2].sendmany('', {address: 10}, 0, "") fee = self.nodes[2].gettransaction(txid)["fee"] self.nodes[2].generate(1) @@ -160,9 +162,6 @@ def run_test(self): assert_equal(self.nodes[0].getbalance(), node_0_bal) assert_fee_amount(-fee, self.get_vsize(self.nodes[2].getrawtransaction(txid)), fee_per_kbyte) - # This will raise an exception since generate does not accept a string - assert_raises_rpc_error(-1, "not an integer", self.nodes[0].generate, "2") - # Import address and private key to check correct behavior of spendable unspents # 1. Send some coins to generate new UTXO address_to_import = self.nodes[2].getnewaddress() @@ -171,6 +170,7 @@ def run_test(self): self.sync_all(self.nodes[0:3]) # 2. Import address from node2 to node1 + self.log.info("test importaddress") self.nodes[1].importaddress(address_to_import) # 3. Validate that the imported address is watch-only on node1 @@ -184,6 +184,7 @@ def run_test(self): # 5. Import private key of the previously imported address on node1 priv_key = self.nodes[2].dumpprivkey(address_to_import) + self.log.info("test importprivkey") self.nodes[1].importprivkey(priv_key) # 6. Check that the unspents are now spendable on node1 @@ -258,7 +259,5 @@ def run_test(self): assert_equal(9, self.len_listunspent({"minimumSumAmount": 2500.00})) - - if __name__ == '__main__': WalletTest().main() diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 20222cc4b968..fef16eec3eb1 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -10,6 +10,7 @@ from test_framework.test_framework import PivxTestFramework from test_framework.util import ( assert_array_result, + assert_equal, hex_str_to_bytes, ) @@ -94,6 +95,44 @@ def run_test(self): txs = [tx for tx in self.nodes[0].listtransactions("*", 100, 0, True) if "label" in tx and tx['label'] == 'watchonly'] assert_array_result(txs, {"category": "receive", "amount": Decimal("0.1")}, {"txid": txid}) + # Send 10 PIV with subtract fee from amount + node_0_bal = self.nodes[0].getbalance() + node_1_bal = self.nodes[1].getbalance() + self.log.info("test sendtoaddress with subtract-fee-from-amt") + txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 10, "", "", True) + node_0_bal -= Decimal('10') + assert_equal(self.nodes[0].getbalance(), node_0_bal) + self.nodes[0].generate(1) + self.sync_all() + fee = self.nodes[0].gettransaction(txid)["fee"] # fee < 0 + node_1_bal += (Decimal('10') + fee) + assert_equal(self.nodes[1].getbalance(), node_1_bal) + assert_array_result(self.nodes[0].listtransactions(), + {"txid": txid}, + {"category": "send", "amount": - Decimal('10') - fee, "confirmations": 1}) + assert_array_result(self.nodes[1].listtransactions(), + {"txid": txid}, + {"category": "receive", "amount": + Decimal('10') + fee, "confirmations": 1}) + + # Sendmany 10 PIV with subtract fee from amount + node_0_bal = self.nodes[0].getbalance() + node_1_bal = self.nodes[1].getbalance() + self.log.info("test sendmany with subtract-fee-from-amt") + address = self.nodes[1].getnewaddress() + txid = self.nodes[0].sendmany('', {address: 10}, 1, "", False, [address]) + node_0_bal -= Decimal('10') + assert_equal(self.nodes[0].getbalance(), node_0_bal) + self.nodes[0].generate(1) + self.sync_all() + fee = self.nodes[0].gettransaction(txid)["fee"] # fee < 0 + node_1_bal += (Decimal('10') + fee) + assert_equal(self.nodes[1].getbalance(), node_1_bal) + assert_array_result(self.nodes[0].listtransactions(), + {"txid": txid}, + {"category": "send", "amount": - Decimal('10') - fee, "confirmations": 1}) + assert_array_result(self.nodes[1].listtransactions(), + {"txid": txid}, + {"category": "receive", "amount": + Decimal('10') + fee, "confirmations": 1}) if __name__ == '__main__': ListTransactionsTest().main()