diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 6c22206b26..00559b6dfe 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -132,6 +132,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listunspent" , 0 }, { "listunspent" , 1 }, { "listunspent" , 2 }, + { "consolidateunspent" , 1 }, + { "consolidateunspent" , 2 }, { "move" , 2 }, { "move" , 3 }, { "rainbymagnitude" , 0 }, diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp old mode 100644 new mode 100755 index 127e766735..aa82f937f4 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -13,6 +13,7 @@ #include "main.h" #include "net.h" #include "wallet.h" +#include "coincontrol.h" using namespace std; using namespace boost; @@ -604,6 +605,167 @@ UniValue listunspent(const UniValue& params, bool fHelp) } +UniValue consolidateunspent(const UniValue& params, bool fHelp) +{ + if (fHelp || params.size() < 1 || params.size() > 3) + throw runtime_error( + "consolidateunspent
[UTXO size [maximum number of inputs]]\n" + "\n" + "Performs a single transaction to consolidate UTXOs on\n" + "a given address. The optional parameter of UTXO size will result\n" + "in consolidating UTXOs to generate an output of that size or\n" + "the output for the total value of the specified maximum,\n" + "maximum number of smallest inputs, whichever is less.\n"); + + UniValue result(UniValue::VOBJ); + + std::string sAddress = params[0].get_str(); + CBitcoinAddress OptimizeAddress(sAddress); + + int64_t nConsolidateLimit = 0; + // Set default maximum consolidation to 50 inputs if it is not specified. This is based + // on performance tests on the Pi to ensure the transaction returns within a reasonable time. + // The performance tests on the Pi show about 3 UTXO's/second. Intel machines should do + // about 3x that. The GUI will not be responsive during the transaction. + unsigned int nInputNumberLimit = 50; + + if (params.size() > 1) nConsolidateLimit = AmountFromValue(params[1]); + if (params.size() > 2) nInputNumberLimit = params[2].get_int(); + + // Clamp InputNumberLimit to 200. Above 200 risks an invalid transaction due to the size. + nInputNumberLimit = std::min(nInputNumberLimit, (unsigned int) 200); + + if (!OptimizeAddress.IsValid()) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, string("Invalid Gridcoin address: ") + sAddress); + + // Set the consolidation transaction address to the same as the inputs to consolidate. + CScript scriptDestPubKey; + scriptDestPubKey.SetDestination(OptimizeAddress.Get()); + + std::vector vecInputs; + + // A convenient way to do a sort without the bother of writing a comparison operator. + // The map does it for us! It must be a multimap, because it is highly likely one or + // more UTXO's will have the same nValue. + std::multimap mInputs; + + // Have to lock both main and wallet to prevent deadlocks. + LOCK2(cs_main, pwalletMain->cs_wallet); + + // Get the current UTXO's. + pwalletMain->AvailableCoins(vecInputs, false, NULL, false); + + // Filter outputs by matching address and insert into sorted multimap. + for (auto const& out : vecInputs) + { + CTxDestination outaddress; + int64_t nOutValue = out.tx->vout[out.i].nValue; + + if (!ExtractDestination(out.tx->vout[out.i].scriptPubKey, outaddress)) continue; + + if (CBitcoinAddress(outaddress) == OptimizeAddress) + mInputs.insert(std::make_pair(nOutValue, out)); + } + + CWalletTx wtxNew; + + // For min fee calculation. + CTransaction txDummy; + + set> setCoins; + + unsigned int iInputCount = 0; + int64_t nValue = 0; + + // Construct the inputs to the consolidation transaction. Either all of the inputs from above, or 200, + // or when the total reaches/exceeds nConsolidateLimit, whichever is more limiting. The map allows us + // to elegantly select the UTXO's from the smallest upwards. + for (auto const& out : mInputs) + { + // Increment first so the count is 1 based. + ++iInputCount; + + if (fDebug) LogPrintf("INFO: consolidateunspent: input value = %f, confirmations = %" PRId64, ((double) out.first) / (double) COIN, out.second.nDepth); + + setCoins.insert(make_pair(out.second.tx, out.second.i)); + nValue += out.second.tx->vout[out.second.i].nValue; + + if (iInputCount == nInputNumberLimit || (nValue >= nConsolidateLimit && nConsolidateLimit != 0)) break; + } + + // If number of inputs that meet criteria is less than two, then do nothing. + if (iInputCount < 2) + { + result.pushKV("result", true); + result.pushKV("UTXOs consolidated", (uint64_t) 0); + + return result; + } + + CReserveKey reservekey(pwalletMain); + + + // Fee calculation to avoid change. + + // Bytes + // --------- The inputs to the tx - The one output. + int64_t nBytes = iInputCount * 148 + 34 + 10; + + // Min Fee + int64_t nMinFee = txDummy.GetMinFee(1, GMF_SEND, nBytes); + + int64_t nFee = nTransactionFee * (1 + nBytes / 1000); + + int64_t nFeeRequired = max(nMinFee, nFee); + + + if (pwalletMain->IsLocked()) + { + string strError = _("Error: Wallet locked, unable to create transaction."); + LogPrintf("consolidateunspent: %s", strError); + return strError; + } + + if (fWalletUnlockStakingOnly) + { + string strError = _("Error: Wallet unlocked for staking only, unable to create transaction."); + LogPrintf("consolidateunspent: %s", strError); + return strError; + } + + vector > vecSend; + + // Reduce the out value for the transaction by nFeeRequired from the total of the inputs to provide a fee + // to the staker. The fee has been calculated so that no change should be produced from the CreateTransaction + // call. Just in case, the input address is specified as the return address via coincontrol. + vecSend.push_back(std::make_pair(scriptDestPubKey, nValue - nFeeRequired)); + + CCoinControl coinControl; + + // Send the change back to the same address. + coinControl.destChange = OptimizeAddress.Get(); + + if (!pwalletMain->CreateTransaction(vecSend, setCoins, wtxNew, reservekey, nFeeRequired, &coinControl)) + { + string strError; + if (nValue + nFeeRequired > pwalletMain->GetBalance()) + 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)); + else + strError = _("Error: Transaction creation failed "); + LogPrintf("consolidateunspent: %s", strError); + return strError; + } + + if (!pwalletMain->CommitTransaction(wtxNew, reservekey)) + return _("Error: The transaction was rejected. This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."); + + result.pushKV("result", true); + result.pushKV("UTXOs consolidated", (uint64_t) iInputCount); + result.pushKV("Output UTXO value", (double)(nValue - nFeeRequired) / COIN); + + return result; +} + UniValue createrawtransaction(const UniValue& params, bool fHelp) { diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index b8b1484484..c8a8edec5f 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -312,6 +312,7 @@ static const CRPCCommand vRPCCommands[] = { "listsinceblock", &listsinceblock, cat_wallet }, { "listtransactions", &listtransactions, cat_wallet }, { "listunspent", &listunspent, cat_wallet }, + { "consolidateunspent", &consolidateunspent, cat_wallet }, { "makekeypair", &makekeypair, cat_wallet }, { "move", &movecmd, cat_wallet }, { "rain", &rain, cat_wallet }, diff --git a/src/rpcserver.h b/src/rpcserver.h index a6f5abdb49..18c287cc8e 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -133,6 +133,7 @@ extern UniValue listreceivedbyaddress(const UniValue& params, bool fHelp); extern UniValue listsinceblock(const UniValue& params, bool fHelp); extern UniValue listtransactions(const UniValue& params, bool fHelp); extern UniValue listunspent(const UniValue& params, bool fHelp); +extern UniValue consolidateunspent(const UniValue& params, bool fHelp); extern UniValue makekeypair(const UniValue& params, bool fHelp); extern UniValue movecmd(const UniValue& params, bool fHelp); extern UniValue rain(const UniValue& params, bool fHelp); diff --git a/src/wallet.cpp b/src/wallet.cpp index 8ef137cc7f..63a912d6f4 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -1530,19 +1530,19 @@ bool CWallet::SelectCoinsForStaking(int64_t nTargetValueIn, unsigned int nSpendT return true; } -bool CWallet::CreateTransaction(const vector >& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, - int64_t& nFeeRet, const CCoinControl* coinControl) +bool CWallet::CreateTransaction(const vector >& vecSend, set>& setCoins, + CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl* coinControl) { - int64_t nValue = 0; + int64_t nValueOut = 0; for (auto const& s : vecSend) { - if (nValue < 0) + if (nValueOut < 0) return false; - nValue += s.second; + nValueOut += s.second; } - if (vecSend.empty() || nValue < 0) + if (vecSend.empty() || nValueOut < 0) return false; wtxNew.BindWallet(this); @@ -1559,24 +1559,36 @@ bool CWallet::CreateTransaction(const vector >& vecSend, wtxNew.vout.clear(); wtxNew.fFromMe = true; - int64_t nTotalValue = nValue + nFeeRet; + int64_t nTotalValue = nValueOut + nFeeRet; double dPriority = 0; // vouts to the payees for (auto const& s : vecSend) wtxNew.vout.push_back(CTxOut(s.second, s.first)); - // Choose coins to use - set > setCoins; int64_t nValueIn = 0; - if (!SelectCoins(nTotalValue, wtxNew.nTime, setCoins, nValueIn, coinControl)) - return false; + + // If provided coin set is empty, choose coins to use. + if (!setCoins.size()) + { + if (!SelectCoins(nTotalValue, wtxNew.nTime, setCoins, nValueIn, coinControl)) + return false; + } + else + { + // Add up input value for the provided set of coins. + for (auto const& input : setCoins) + { + nValueIn += input.first->vout[input.second].nValue; + } + } + for (auto const& pcoin : setCoins) { int64_t nCredit = pcoin.first->vout[pcoin.second].nValue; dPriority += (double)nCredit * pcoin.first->GetDepthInMainChain(); } - int64_t nChange = nValueIn - nValue - nFeeRet; + int64_t nChange = nValueIn - nValueOut - nFeeRet; // if sub-cent change is required, the fee must be raised to at least MIN_TX_FEE // or until nChange becomes zero // NOTE: this depends on the exact behaviour of GetMinFee @@ -1659,6 +1671,18 @@ bool CWallet::CreateTransaction(const vector >& vecSend, return true; } +bool CWallet::CreateTransaction(const vector >& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, + int64_t& nFeeRet, const CCoinControl* coinControl) +{ + // Initialize setCoins empty to let CreateTransaction choose via SelectCoins... + set> setCoins; + + return CreateTransaction(vecSend, setCoins, wtxNew, reservekey, nFeeRet, coinControl); +} + + + + bool CWallet::CreateTransaction(CScript scriptPubKey, int64_t nValue, CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl* coinControl) { vector< pair > vecSend; diff --git a/src/wallet.h b/src/wallet.h index f6f73a60e6..129810270c 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -7,6 +7,7 @@ #include #include +#include #include #include "main.h" #include "key.h" @@ -199,6 +200,7 @@ class CWallet : public CCryptoKeyStore int64_t GetStake() const; int64_t GetNewMint() const; bool CreateTransaction(const std::vector >& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl *coinControl=NULL); + bool CreateTransaction(const std::vector >& vecSend, std::set>& setCoins, CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl *coinControl=NULL); bool CreateTransaction(CScript scriptPubKey, int64_t nValue, CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl *coinControl=NULL); bool CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey);