Skip to content

Commit 41e3f66

Browse files
JohnnyLawDGBclaude
andcommitted
fix: auto-consolidate fragmented UTXOs when minting DigiDollar
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 <noreply@anthropic.com>
1 parent 98edaa0 commit 41e3f66

1 file changed

Lines changed: 84 additions & 1 deletion

File tree

src/rpc/digidollar.cpp

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <wallet/context.h>
3030
#include <wallet/rpc/util.h>
3131
#include <wallet/spend.h>
32+
#include <wallet/coincontrol.h>
3233
#include <wallet/coinselection.h>
3334
#include <wallet/digidollarwallet.h>
3435
#include <wallet/walletdb.h>
@@ -745,7 +746,9 @@ RPCHelpMan mintdigidollar()
745746
{RPCResult::Type::NUM, "unlock_height", "Block height when collateral becomes unlockable"},
746747
{RPCResult::Type::NUM, "collateral_ratio", "Effective collateral ratio percentage"},
747748
{RPCResult::Type::STR_AMOUNT, "fee_paid", "Transaction fee paid"},
748-
{RPCResult::Type::STR, "position_id", "Unique position identifier"}
749+
{RPCResult::Type::STR, "position_id", "Unique position identifier"},
750+
{RPCResult::Type::STR_HEX, "consolidation_txid", /*optional=*/true, "TXID of auto-consolidation transaction (only present if UTXOs were consolidated)"},
751+
{RPCResult::Type::BOOL, "utxos_consolidated", /*optional=*/true, "True if wallet UTXOs were auto-consolidated before minting"}
749752
}
750753
},
751754
RPCExamples{
@@ -941,6 +944,82 @@ RPCHelpMan mintdigidollar()
941944

942945
DigiDollar::TxBuilderResult result = builder.BuildMintTransaction(params);
943946

947+
// Auto-consolidate if mint failed due to UTXO fragmentation
948+
std::string consolidation_txid;
949+
if (!result.success && result.error.find("Too many small UTXOs") != std::string::npos) {
950+
LogPrintf("DigiDollar RPC Mint: UTXO fragmentation detected (%zu UTXOs). Auto-consolidating...\n",
951+
availableUtxos.size());
952+
953+
// Calculate consolidation target: collateral + fees + 10% margin
954+
CAmount consolidationTarget = result.collateralRequired +
955+
(result.collateralRequired / 10) + 10000000; // +10% + 0.1 DGB fee buffer
956+
957+
// Cap at available balance
958+
CAmount totalAvailable = 0;
959+
for (const auto& [outpoint, value] : utxoValues) {
960+
totalAvailable += value;
961+
}
962+
if (consolidationTarget > totalAvailable) {
963+
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
964+
strprintf("Insufficient funds for collateral. Need %.2f DGB, have %.2f DGB across %zu small UTXOs.",
965+
result.collateralRequired / 100000000.0,
966+
totalAvailable / 100000000.0,
967+
availableUtxos.size()));
968+
}
969+
970+
// Create consolidation transaction using wallet's standard coin selection
971+
CTxDestination consolidationDest;
972+
{
973+
LOCK(pwallet->cs_wallet);
974+
auto op_dest = pwallet->GetNewChangeDestination(OutputType::BECH32);
975+
if (!op_dest) {
976+
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to get consolidation address");
977+
}
978+
consolidationDest = *op_dest;
979+
}
980+
981+
wallet::CCoinControl coin_control;
982+
wallet::CRecipient recipient{consolidationDest, consolidationTarget, false};
983+
std::vector<wallet::CRecipient> recipients = {recipient};
984+
985+
auto consolidation_result = wallet::CreateTransaction(*pwallet, recipients, /*change_pos=*/-1, coin_control, /*sign=*/true);
986+
if (!consolidation_result) {
987+
throw JSONRPCError(RPC_WALLET_ERROR,
988+
strprintf("Auto-consolidation failed: %s. Try manually consolidating UTXOs with: "
989+
"sendtoaddress <your_address> <amount>",
990+
util::ErrorString(consolidation_result).original));
991+
}
992+
993+
const CTransactionRef& consolidation_tx = consolidation_result->tx;
994+
consolidation_txid = consolidation_tx->GetHash().GetHex();
995+
{
996+
LOCK(pwallet->cs_wallet);
997+
pwallet->CommitTransaction(consolidation_tx, {}, {});
998+
}
999+
1000+
LogPrintf("DigiDollar RPC Mint: Consolidation tx broadcast: %s (%.2f DGB)\n",
1001+
consolidation_txid, consolidationTarget / 100000000.0);
1002+
1003+
// Re-gather UTXOs (now includes unconfirmed consolidation output)
1004+
availableUtxos.clear();
1005+
utxoValues.clear();
1006+
{
1007+
LOCK(pwallet->cs_wallet);
1008+
wallet::CoinsResult coins = wallet::AvailableCoins(*pwallet);
1009+
for (const wallet::COutput& coin : coins.All()) {
1010+
availableUtxos.push_back(coin.outpoint);
1011+
utxoValues[coin.outpoint] = coin.txout.nValue;
1012+
}
1013+
}
1014+
1015+
LogPrintf("DigiDollar RPC Mint: After consolidation: %zu UTXOs available\n", availableUtxos.size());
1016+
1017+
// Rebuild builder with new UTXO map and retry
1018+
RpcMintTxBuilder retryBuilder(Params(), currentHeight, oraclePriceMicroUSD, utxoValues);
1019+
params.utxos = availableUtxos;
1020+
result = retryBuilder.BuildMintTransaction(params);
1021+
}
1022+
9441023
if (!result.success) {
9451024
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build mint transaction: " + result.error);
9461025
}
@@ -1024,6 +1103,10 @@ RPCHelpMan mintdigidollar()
10241103
resultObj.pushKV("collateral_ratio", collateralRatio);
10251104
resultObj.pushKV("fee_paid", ValueFromAmount(result.totalFees));
10261105
resultObj.pushKV("position_id", tx->GetHash().GetHex());
1106+
if (!consolidation_txid.empty()) {
1107+
resultObj.pushKV("consolidation_txid", consolidation_txid);
1108+
resultObj.pushKV("utxos_consolidated", true);
1109+
}
10271110

10281111
return resultObj;
10291112
},

0 commit comments

Comments
 (0)