From 6ea9cde632b04f1ca6da903f3938d9a58c0121ad Mon Sep 17 00:00:00 2001 From: JohnnyLawDGB Date: Tue, 7 Apr 2026 05:45:11 -0500 Subject: [PATCH] fix: add UTXO auto-consolidation to Qt wallet mint path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RPC mintdigidollar handler had auto-consolidation logic (PR #391) that detects when a mint fails due to UTXO fragmentation (>400 inputs needed) and automatically consolidates UTXOs before retrying. The Qt wallet's mintDigiDollar in walletmodel.cpp was missing this logic entirely — it called BuildMintTransaction once and returned the error on failure. This caused Qt GUI users with many small coinbase UTXOs (e.g. ~1,078 DGB each from mining) to fail mints that required more than ~431,400 DGB (400 inputs * ~1,078 DGB), while the same mint via RPC would succeed. Reported by Aussie and DanGB on RC29 testnet. Port the multi-pass consolidation from src/rpc/digidollar.cpp into the Qt mint path with identical parameters (1400 inputs/pass, 10 max passes). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/qt/walletmodel.cpp | 138 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 49920c8b0a..ddf6011d21 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -27,6 +27,7 @@ #include #include // for strprintf #include +#include // for AvailableCoins, CreateTransaction #include // for CRecipient #include #include // for DigiDollarWallet @@ -923,6 +924,143 @@ WalletModel::DigiDollarMintResult WalletModel::mintDigiDollar(CAmount ddAmount, DigiDollar::TxBuilderResult result = builder.BuildMintTransaction(params); + // Auto-consolidate if mint failed due to UTXO fragmentation + // (mirrors the RPC mintdigidollar consolidation logic) + std::string consolidation_txid; + if (!result.success && result.error.find("Too many small UTXOs") != std::string::npos) { + LogPrintf("DigiDollar Qt: UTXO fragmentation detected (%zu UTXOs). Auto-consolidating...\n", + availableUtxos.size()); + + CAmount minRequired = result.collateralRequired + 20000000; // collateral + 0.2 DGB margin + if (totalAvailable < minRequired) { + return DigiDollarMintResult(AmountExceedsBalance, "", "", + QString("Insufficient funds for collateral. Need %1 DGB, have %2 DGB.") + .arg(result.collateralRequired / 100000000.0, 0, 'f', 2) + .arg(totalAvailable / 100000000.0, 0, 'f', 2)); + } + + CTxDestination consolidationDest; + { + LOCK(pWallet->cs_wallet); + auto op_dest = pWallet->GetNewChangeDestination(OutputType::BECH32); + if (!op_dest) { + return DigiDollarMintResult(TransactionCreationFailed, "", "", + "Failed to get consolidation address"); + } + consolidationDest = *op_dest; + } + + // Multi-pass consolidation: MAX_STANDARD_TX_WEIGHT is 400k WU. + // P2WPKH input ~271 WU. Conservative limit: 1400 inputs per pass. + static const size_t MAX_CONSOLIDATION_INPUTS = 1400; + static const int MAX_CONSOLIDATION_PASSES = 10; + int pass = 0; + + while (availableUtxos.size() > MAX_CONSOLIDATION_INPUTS && pass < MAX_CONSOLIDATION_PASSES) { + ++pass; + size_t batch_size = std::min(availableUtxos.size(), MAX_CONSOLIDATION_INPUTS); + LogPrintf("DigiDollar Qt: Consolidation pass %d — sweeping %zu of %zu UTXOs\n", + pass, batch_size, availableUtxos.size()); + + wallet::CCoinControl coin_control; + CAmount batchTotal = 0; + for (size_t i = 0; i < batch_size; ++i) { + coin_control.Select(availableUtxos[i]); + batchTotal += utxoValues[availableUtxos[i]]; + } + coin_control.m_allow_other_inputs = false; + + wallet::CRecipient recipient{consolidationDest, batchTotal, /*subtract_fee=*/true}; + std::vector recipients = {recipient}; + + auto consolidation_result = wallet::CreateTransaction(*pWallet, recipients, /*change_pos=*/-1, coin_control, /*sign=*/true); + if (!consolidation_result) { + return DigiDollarMintResult(TransactionCreationFailed, "", "", + QString("Auto-consolidation pass %1 failed: %2") + .arg(pass) + .arg(QString::fromStdString(util::ErrorString(consolidation_result).original))); + } + + const CTransactionRef& consolidation_tx = consolidation_result->tx; + consolidation_txid = consolidation_tx->GetHash().GetHex(); + { + LOCK(pWallet->cs_wallet); + pWallet->CommitTransaction(consolidation_tx, {}, {}); + } + + LogPrintf("DigiDollar Qt: Consolidation pass %d tx: %s (swept %.2f DGB from %zu inputs)\n", + pass, consolidation_txid, batchTotal / 100000000.0, batch_size); + + // Refresh UTXO set after consolidation + availableUtxos.clear(); + utxoValues.clear(); + totalAvailable = 0; + { + LOCK(pWallet->cs_wallet); + wallet::CoinsResult coins = wallet::AvailableCoins(*pWallet); + for (const wallet::COutput& coin : coins.All()) { + availableUtxos.push_back(coin.outpoint); + utxoValues[coin.outpoint] = coin.txout.nValue; + totalAvailable += coin.txout.nValue; + } + } + LogPrintf("DigiDollar Qt: After pass %d: %zu UTXOs available\n", pass, availableUtxos.size()); + } + + // Single-pass consolidation for <= 1400 UTXOs + if (consolidation_txid.empty() && availableUtxos.size() <= MAX_CONSOLIDATION_INPUTS) { + wallet::CCoinControl coin_control; + CAmount batchTotal = 0; + for (const auto& utxo : availableUtxos) { + coin_control.Select(utxo); + batchTotal += utxoValues[utxo]; + } + coin_control.m_allow_other_inputs = false; + + wallet::CRecipient recipient{consolidationDest, batchTotal, /*subtract_fee=*/true}; + std::vector recipients = {recipient}; + + auto consolidation_result = wallet::CreateTransaction(*pWallet, recipients, /*change_pos=*/-1, coin_control, /*sign=*/true); + if (!consolidation_result) { + return DigiDollarMintResult(TransactionCreationFailed, "", "", + QString("Auto-consolidation failed: %1") + .arg(QString::fromStdString(util::ErrorString(consolidation_result).original))); + } + + const CTransactionRef& consolidation_tx = consolidation_result->tx; + consolidation_txid = consolidation_tx->GetHash().GetHex(); + { + LOCK(pWallet->cs_wallet); + pWallet->CommitTransaction(consolidation_tx, {}, {}); + } + + LogPrintf("DigiDollar Qt: Single-pass consolidation tx: %s (swept %.2f DGB from %zu inputs)\n", + consolidation_txid, batchTotal / 100000000.0, availableUtxos.size()); + + // Refresh UTXO set after consolidation + availableUtxos.clear(); + utxoValues.clear(); + totalAvailable = 0; + { + LOCK(pWallet->cs_wallet); + wallet::CoinsResult coins = wallet::AvailableCoins(*pWallet); + for (const wallet::COutput& coin : coins.All()) { + availableUtxos.push_back(coin.outpoint); + utxoValues[coin.outpoint] = coin.txout.nValue; + totalAvailable += coin.txout.nValue; + } + } + } + + LogPrintf("DigiDollar Qt: After consolidation: %zu UTXOs available (passes: %d)\n", + availableUtxos.size(), pass); + + // Retry mint with consolidated UTXOs + QtMintTxBuilder retryBuilder(Params(), currentHeight, oraclePrice, utxoValues); + params.utxos = availableUtxos; + result = retryBuilder.BuildMintTransaction(params); + } + if (!result.success) { LogPrintf("DigiDollar Qt: ERROR - BuildMintTransaction failed: %s\n", result.error); return DigiDollarMintResult(TransactionCreationFailed, "", "",