From 41e3f66969e8d5248f2c86b42a85effa33ff9ca1 Mon Sep 17 00:00:00 2001 From: JohnnyLawDGB Date: Sat, 7 Mar 2026 16:04:26 -0600 Subject: [PATCH] fix: auto-consolidate fragmented UTXOs when minting DigiDollar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a wallet has too many small UTXOs (>400) to cover collateral in a single transaction, mintdigidollar now automatically creates a consolidation transaction using the wallet's standard coin selection, then retries the mint using the consolidated output. This fixes the "Too many small UTXOs" error that miners and pool operators encounter when their wallet contains hundreds of small mining reward UTXOs that individually don't cover the collateral requirement within the 400-input transaction limit. The consolidation is transparent — the response includes consolidation_txid and utxos_consolidated fields when auto- consolidation was performed. Both the consolidation and mint transactions are broadcast together and chain in the mempool. Tested with 500 x 600 DGB UTXOs (300,000 DGB total) where tier 0 minting requires ~246,000 DGB collateral. Without this fix, the mint fails because 400 x 600 = 240,000 < 246,000. With this fix, the wallet auto-consolidates and the mint succeeds. Co-Authored-By: Claude Opus 4.6 --- src/rpc/digidollar.cpp | 85 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/rpc/digidollar.cpp b/src/rpc/digidollar.cpp index fcef0743ca..5d0a8084fe 100644 --- a/src/rpc/digidollar.cpp +++ b/src/rpc/digidollar.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -745,7 +746,9 @@ RPCHelpMan mintdigidollar() {RPCResult::Type::NUM, "unlock_height", "Block height when collateral becomes unlockable"}, {RPCResult::Type::NUM, "collateral_ratio", "Effective collateral ratio percentage"}, {RPCResult::Type::STR_AMOUNT, "fee_paid", "Transaction fee paid"}, - {RPCResult::Type::STR, "position_id", "Unique position identifier"} + {RPCResult::Type::STR, "position_id", "Unique position identifier"}, + {RPCResult::Type::STR_HEX, "consolidation_txid", /*optional=*/true, "TXID of auto-consolidation transaction (only present if UTXOs were consolidated)"}, + {RPCResult::Type::BOOL, "utxos_consolidated", /*optional=*/true, "True if wallet UTXOs were auto-consolidated before minting"} } }, RPCExamples{ @@ -941,6 +944,82 @@ RPCHelpMan mintdigidollar() DigiDollar::TxBuilderResult result = builder.BuildMintTransaction(params); + // Auto-consolidate if mint failed due to UTXO fragmentation + std::string consolidation_txid; + if (!result.success && result.error.find("Too many small UTXOs") != std::string::npos) { + LogPrintf("DigiDollar RPC Mint: UTXO fragmentation detected (%zu UTXOs). Auto-consolidating...\n", + availableUtxos.size()); + + // Calculate consolidation target: collateral + fees + 10% margin + CAmount consolidationTarget = result.collateralRequired + + (result.collateralRequired / 10) + 10000000; // +10% + 0.1 DGB fee buffer + + // Cap at available balance + CAmount totalAvailable = 0; + for (const auto& [outpoint, value] : utxoValues) { + totalAvailable += value; + } + if (consolidationTarget > totalAvailable) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient funds for collateral. Need %.2f DGB, have %.2f DGB across %zu small UTXOs.", + result.collateralRequired / 100000000.0, + totalAvailable / 100000000.0, + availableUtxos.size())); + } + + // Create consolidation transaction using wallet's standard coin selection + CTxDestination consolidationDest; + { + LOCK(pwallet->cs_wallet); + auto op_dest = pwallet->GetNewChangeDestination(OutputType::BECH32); + if (!op_dest) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to get consolidation address"); + } + consolidationDest = *op_dest; + } + + wallet::CCoinControl coin_control; + wallet::CRecipient recipient{consolidationDest, consolidationTarget, false}; + std::vector recipients = {recipient}; + + auto consolidation_result = wallet::CreateTransaction(*pwallet, recipients, /*change_pos=*/-1, coin_control, /*sign=*/true); + if (!consolidation_result) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Auto-consolidation failed: %s. Try manually consolidating UTXOs with: " + "sendtoaddress ", + 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 RPC Mint: Consolidation tx broadcast: %s (%.2f DGB)\n", + consolidation_txid, consolidationTarget / 100000000.0); + + // Re-gather UTXOs (now includes unconfirmed consolidation output) + availableUtxos.clear(); + utxoValues.clear(); + { + 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; + } + } + + LogPrintf("DigiDollar RPC Mint: After consolidation: %zu UTXOs available\n", availableUtxos.size()); + + // Rebuild builder with new UTXO map and retry + RpcMintTxBuilder retryBuilder(Params(), currentHeight, oraclePriceMicroUSD, utxoValues); + params.utxos = availableUtxos; + result = retryBuilder.BuildMintTransaction(params); + } + if (!result.success) { throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build mint transaction: " + result.error); } @@ -1024,6 +1103,10 @@ RPCHelpMan mintdigidollar() resultObj.pushKV("collateral_ratio", collateralRatio); resultObj.pushKV("fee_paid", ValueFromAmount(result.totalFees)); resultObj.pushKV("position_id", tx->GetHash().GetHex()); + if (!consolidation_txid.empty()) { + resultObj.pushKV("consolidation_txid", consolidation_txid); + resultObj.pushKV("utxos_consolidated", true); + } return resultObj; },