forked from DigiByte-Core/digibyte
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdigidollar.cpp
More file actions
4496 lines (4027 loc) · 233 KB
/
digidollar.cpp
File metadata and controls
4496 lines (4027 loc) · 233 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2025 The DigiByte Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <rpc/server.h>
#include <rpc/util.h>
#include <rpc/server_util.h>
#include <rpc/blockchain.h>
#include <rpc/digidollar_transactions.h>
#include <random.h>
#include <oracle/bundle_manager.h>
#include <primitives/oracle.h>
#include <oracle/node.h>
#include <oracle/mock_oracle.h>
#include <consensus/digidollar.h>
#include <consensus/dca.h>
#include <consensus/err.h>
#include <digidollar/digidollar.h>
#include <digidollar/health.h>
#include <index/digidollarstatsindex.h>
#include <chainparams.h>
#include <kernel/chainparams.h>
#include <node/context.h>
#include <core_io.h>
#include <util/strencodings.h>
#include <validation.h>
#include <versionbits.h>
#include <wallet/wallet.h>
#include <wallet/context.h>
#include <wallet/rpc/util.h>
#include <wallet/spend.h>
#include <wallet/coincontrol.h>
#include <wallet/coinselection.h>
#include <wallet/digidollarwallet.h>
#include <wallet/walletdb.h>
#include <wallet/scriptpubkeyman.h>
#include <interfaces/wallet.h>
#include <digidollar/txbuilder.h>
#include <node/transaction.h>
#include <base58.h>
#include <script/standard.h>
#include <script/signingprovider.h>
#include <rpc/protocol.h>
#include <versionbits.h>
#include <deploymentstatus.h>
#include <key_io.h>
#include <util/time.h>
#include <univalue.h>
using namespace DigiDollar;
using namespace DigiDollar::DCA;
// Mock utility functions for RPC-only implementation
namespace {
// Aligned with consensus/digidollar.h (10 tiers, 0-9)
int GetLockDaysForTier(uint32_t tier) {
switch (tier) {
case 0: return 0; // Special: 1 hour (240 blocks) - handled separately
case 1: return 30; // 30 days
case 2: return 90; // 90 days (3 months)
case 3: return 180; // 180 days (6 months)
case 4: return 365; // 1 year
case 5: return 730; // 2 years
case 6: return 1095; // 3 years
case 7: return 1825; // 5 years
case 8: return 2555; // 7 years
case 9: return 3650; // 10 years
default: return 0;
}
}
// Aligned with consensus/digidollar.h collateralRatios (10 tiers, 0-9)
int GetMinCollateralRatio(uint32_t tier) {
switch (tier) {
case 0: return 1000; // 1000% for 1 hour (testing only)
case 1: return 500; // 500% for 30 days
case 2: return 400; // 400% for 90 days
case 3: return 350; // 350% for 180 days
case 4: return 300; // 300% for 1 year
case 5: return 275; // 275% for 2 years
case 6: return 250; // 250% for 3 years
case 7: return 225; // 225% for 5 years
case 8: return 212; // 212% for 7 years
case 9: return 200; // 200% for 10 years
default: return 500;
}
}
/**
* Generate an HD-derived key from the wallet for DigiDollar use.
* This allows DD keys to be recovered from the wallet seed.
*
* @param pwallet The wallet to derive the key from
* @param label Label to identify the key's purpose (e.g., "dd-owner", "dd-address")
* @return CKey The derived key, or an invalid key if derivation fails
*/
CKey GetHDKeyForDigiDollar(wallet::CWallet* pwallet, const std::string& label) {
AssertLockHeld(pwallet->cs_wallet);
CKey key;
// Try to get an HD-derived destination from the wallet
// Try BECH32M (Taproot) first for modern wallets
auto op_dest = pwallet->GetNewDestination(OutputType::BECH32M, label);
if (!op_dest) {
// Fallback to BECH32 (SegWit v0) for legacy descriptor wallets
LogPrintf("DigiDollar: BECH32M not available, trying BECH32 for label '%s'\n", label);
op_dest = pwallet->GetNewDestination(OutputType::BECH32, label);
}
if (op_dest) {
CTxDestination dest = *op_dest;
CScript script = GetScriptForDestination(dest);
// Check if this is a Taproot (WitnessV1Taproot) address
// Taproot addresses need special handling - use GetKeyByXOnly instead of GetKey
if (auto* taproot_dest = std::get_if<WitnessV1Taproot>(&dest)) {
XOnlyPubKey output_key(*taproot_dest);
LogPrintf("DigiDollar: GetHDKeyForDigiDollar - descriptor produced output_key=%s for label '%s'\n",
HexStr(output_key), label);
// For Taproot, we need to find the key using GetSigningProviderWithKeys (includes private keys)
// GetSolvingProvider doesn't include private keys, so GetKeyByXOnly would fail
for (auto* spk_man : pwallet->GetAllScriptPubKeyMans()) {
if (auto* desc_spk = dynamic_cast<wallet::DescriptorScriptPubKeyMan*>(spk_man)) {
// Use GetSigningProviderWithKeys to get provider with private keys
auto provider = desc_spk->GetSigningProviderWithKeys(script);
if (provider) {
TaprootSpendData spenddata;
if (provider->GetTaprootSpendData(output_key, spenddata)) {
LogPrintf("DigiDollar: GetHDKeyForDigiDollar - spenddata.internal_key=%s\n",
HexStr(spenddata.internal_key));
if (spenddata.internal_key.IsFullyValid()) {
if (provider->GetKeyByXOnly(spenddata.internal_key, key)) {
// DEBUG: Verify the key we got matches what DD will produce
XOnlyPubKey key_xonly(key.GetPubKey());
auto key_tweaked = key_xonly.CreateTapTweak(nullptr);
if (key_tweaked) {
LogPrintf("DigiDollar: GetHDKeyForDigiDollar - returned key pubkey_xonly=%s, tweaked=%s (matches descriptor: %s)\n",
HexStr(key_xonly), HexStr(key_tweaked->first),
(key_tweaked->first == output_key) ? "YES" : "NO");
}
LogPrintf("DigiDollar: Successfully derived HD key from Taproot descriptor wallet for label '%s'\n", label);
return key;
}
}
}
// Fallback: try to get key by output key directly
if (provider->GetKeyByXOnly(output_key, key)) {
LogPrintf("DigiDollar: Successfully derived HD key from Taproot (output key) for label '%s' - WARNING: using output key directly!\n", label);
return key;
}
}
}
}
LogPrintf("DigiDollar: WARNING - Could not extract Taproot key from HD destination for label '%s'\n", label);
} else {
// Non-Taproot addresses (SegWit v0, legacy)
// Try to extract the key from the destination
// This works for both legacy and descriptor wallets
for (auto* spk_man : pwallet->GetAllScriptPubKeyMans()) {
// Try legacy wallet path first (direct GetKey access)
if (auto* legacy_spk = dynamic_cast<wallet::LegacyScriptPubKeyMan*>(spk_man)) {
CKeyID keyid = GetKeyForDestination(*legacy_spk, dest);
if (!keyid.IsNull() && legacy_spk->GetKey(keyid, key)) {
LogPrintf("DigiDollar: Successfully derived HD key from legacy wallet for label '%s'\n", label);
return key;
}
}
// Try descriptor wallet path (use GetSigningProviderWithKeys for private keys)
if (auto* desc_spk = dynamic_cast<wallet::DescriptorScriptPubKeyMan*>(spk_man)) {
auto provider = desc_spk->GetSigningProviderWithKeys(script);
if (provider) {
CKeyID keyid = GetKeyForDestination(*provider, dest);
if (!keyid.IsNull() && provider->GetKey(keyid, key)) {
LogPrintf("DigiDollar: Successfully derived HD key from descriptor wallet for label '%s'\n", label);
return key;
}
}
}
}
LogPrintf("DigiDollar: WARNING - Could not extract key from HD destination for label '%s'\n", label);
}
} else {
LogPrintf("DigiDollar: WARNING - Could not get HD destination for label '%s': %s\n",
label, util::ErrorString(op_dest).original);
}
// Fallback to random key if HD derivation fails
// This maintains backward compatibility with wallets that don't support HD
LogPrintf("DigiDollar: Falling back to random key for label '%s'\n", label);
key.MakeNewKey(true);
return key;
}
}
RPCHelpMan getdigidollarstats()
{
return RPCHelpMan{"getdigidollarstats",
"\nGet current DigiDollar system health information.\n"
"Returns the overall health of the DigiDollar stablecoin system,\n"
"including collateralization ratio and DCA tier status.\n",
{},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::NUM, "health_percentage", "System health as percentage (e.g., 150 = 150% collateralized)"},
{RPCResult::Type::STR, "health_status", "Health tier status: healthy, warning, critical, or emergency"},
{RPCResult::Type::NUM, "total_collateral_dgb", "Total DGB locked as collateral"},
{RPCResult::Type::NUM, "total_dd_supply", "Total DigiDollar supply in circulation (in cents)"},
{RPCResult::Type::NUM, "oracle_price_cents", "Current DGB/USD price from oracle (in cents per DGB)"},
{RPCResult::Type::NUM, "oracle_price_micro_usd", "Current DGB/USD price from oracle in micro-USD (1,000,000 = $1.00)"},
{RPCResult::Type::BOOL, "is_emergency", "True if system is in emergency state (<100% collateralized)"},
{RPCResult::Type::NUM, "system_collateral_ratio", "Alias for health_percentage (for backward compatibility)"},
{RPCResult::Type::NUM, "total_collateral_locked", "Alias for total_collateral_dgb (in satoshis)"},
{RPCResult::Type::NUM, "active_positions", "Number of active DD positions"},
{RPCResult::Type::NUM, "oracle_price_age", "Blocks since last oracle update"},
{RPCResult::Type::OBJ, "dca_tier", "Current DCA tier information",
{
{RPCResult::Type::NUM, "min_collateral", "Minimum collateral % for this tier"},
{RPCResult::Type::NUM, "max_collateral", "Maximum collateral % for this tier"},
{RPCResult::Type::NUM, "multiplier", "DCA multiplier for new mints in this tier"},
{RPCResult::Type::STR, "status", "Tier status description"}
}
},
{RPCResult::Type::OBJ, "err_tier", "Current Emergency Redemption Ratio (ERR) tier information",
{
{RPCResult::Type::NUM, "ratio", "ERR ratio (0.80-1.0) - lower = more DD burn required"},
{RPCResult::Type::NUM, "burn_multiplier", "DD burn multiplier (1.0-1.25x) - how much MORE DD to burn"},
{RPCResult::Type::STR, "description", "ERR tier description with burn multiplier"}
}
}
}
},
RPCExamples{
HelpExampleCli("getdigidollarstats", "")
+ HelpExampleRpc("getdigidollarstats", "")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// Check DigiDollar activation
{
const node::NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node);
const CBlockIndex* tip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
if (!DigiDollar::IsDigiDollarEnabled(tip, chainman)) {
throw JSONRPCError(RPC_MISC_ERROR, "DigiDollar is not yet active on this blockchain");
}
}
// NETWORK-WIDE TRACKING: Use DigiDollar stats index for efficient tracking
// This ensures all nodes see identical stats regardless of which wallets are loaded
CAmount totalCollateral = 0;
CAmount totalDD = 0;
// Get node context for chainstate access
const node::NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node);
// Use the DigiDollar stats index for efficient network-wide tracking
if (g_digidollar_stats_index) {
if (!g_digidollar_stats_index->BlockUntilSyncedToCurrentChain()) {
const IndexSummary summary{g_digidollar_stats_index->GetSummary()};
throw JSONRPCError(RPC_INTERNAL_ERROR,
strprintf("DigiDollar stats index is syncing. Current height: %d", summary.best_block_height));
}
const CBlockIndex* pindex;
{
LOCK(cs_main);
pindex = chainman.ActiveChain().Tip();
}
if (pindex) {
auto stats = g_digidollar_stats_index->LookUpStats(*pindex);
if (stats) {
totalDD = stats->total_dd_supply;
totalCollateral = stats->total_collateral;
}
}
} else {
// Fallback: Use UTXO scanning (slow but works without index)
LogPrintf("DigiDollar: getdigidollarstats - DigiDollar stats index not available, falling back to UTXO scan\n");
// Access the UTXO set (like gettxoutsetinfo does)
// CRITICAL: Must flush OUTSIDE the lock, then re-acquire lock for scanning
Chainstate& active_chainstate = chainman.ActiveChainstate();
// Step 1: Force flush all cached coins to disk (like gettxoutsetinfo does)
LogPrintf("DigiDollar: getdigidollarstats - About to ForceFlushStateToDisk...\n");
active_chainstate.ForceFlushStateToDisk();
LogPrintf("DigiDollar: getdigidollarstats - ForceFlushStateToDisk completed\n");
// Step 2: Now acquire lock and access the flushed CoinsDB
// CRITICAL: Hold cs_main lock during ScanUTXOSet to prevent race conditions
CCoinsView* coins_view;
node::BlockManager* blockman;
const CTxMemPool* mempool = node.mempool.get();
{
LOCK(::cs_main);
coins_view = &active_chainstate.CoinsDB();
blockman = &active_chainstate.m_blockman;
// Scan UTXO set to find ALL DigiDollar vaults network-wide
// Pass BlockManager for full transaction access
// Pass both CoinsDB (for iteration) and CoinsTip (for validation)
LogPrintf("DigiDollar: getdigidollarstats - About to call ScanUTXOSet...\n");
DigiDollar::SystemHealthMonitor::ScanUTXOSet(coins_view, &active_chainstate.CoinsTip(), blockman, mempool);
LogPrintf("DigiDollar: getdigidollarstats - ScanUTXOSet completed\n");
}
// Get metrics from scanner
DigiDollar::SystemMetrics metrics = DigiDollar::SystemHealthMonitor::GetSystemMetrics();
totalCollateral = metrics.totalCollateral;
totalDD = metrics.totalDDSupply;
}
// Get current oracle price in micro-USD from the real oracle system
// micro-USD format: 1,000,000 = $1.00, so 6310 = $0.00631
OracleBundleManager& oracle_manager = OracleBundleManager::GetInstance();
CAmount oraclePriceMicroUSD = oracle_manager.GetLatestPrice();
// Fall back to MockOracleManager for regtest/testing if no real oracle data
if (oraclePriceMicroUSD <= 0 && Params().GetChainType() == ChainType::REGTEST) {
// MockOracleManager already returns micro-USD (see mock_oracle.cpp)
oraclePriceMicroUSD = MockOracleManager::GetInstance().GetCurrentPrice();
}
// Convert micro-USD to millicents for CalculateSystemHealth
// micro-USD / 10 = millicents (e.g., 6310 micro-USD / 10 = 631 millicents = $0.00631)
CAmount oraclePriceMillicents = oraclePriceMicroUSD / 10;
// Calculate cents for display (rounded). Allow 0 for sub-cent prices —
// oracle_price_micro_usd and price_usd fields have full precision.
CAmount oraclePriceCents = (oraclePriceMicroUSD + 5000) / 10000;
// Calculate system health
// IMPORTANT: Return 0% if no DD minted network-wide (instead of default 30000%)
int systemHealth;
if (totalDD == 0) {
systemHealth = 0; // No DD minted = 0% health, not 30000%
} else {
systemHealth = DynamicCollateralAdjustment::CalculateSystemHealth(
totalCollateral, totalDD, oraclePriceMillicents);
}
// Get current tier information
auto tier = DynamicCollateralAdjustment::GetCurrentTier(systemHealth);
// Check emergency status
bool isEmergency = DynamicCollateralAdjustment::IsSystemEmergency(systemHealth);
UniValue result(UniValue::VOBJ);
result.pushKV("health_percentage", systemHealth);
result.pushKV("health_status", tier.status);
result.pushKV("total_collateral_dgb", ValueFromAmount(totalCollateral));
result.pushKV("total_dd_supply", int64_t{totalDD});
result.pushKV("oracle_price_cents", int64_t{oraclePriceCents}); // Rounded to cents for display
result.pushKV("oracle_price_micro_usd", int64_t{oraclePriceMicroUSD}); // Full precision micro-USD
result.pushKV("is_emergency", isEmergency);
// Add fields expected by tests
result.pushKV("system_collateral_ratio", systemHealth);
result.pushKV("total_collateral_locked", ValueFromAmount(totalCollateral));
// Get active position count from the DigiDollar stats index.
// The stats index tracks vault_count (incremented on mint,
// decremented on redeem) so this reflects real network state.
// Falls back to 0 if the stats index isn't available yet.
uint64_t activePositions = 0;
if (g_digidollar_stats_index) {
ChainstateManager& chainman = EnsureAnyChainman(request.context);
LOCK(cs_main);
const CBlockIndex* pindex = chainman.ActiveChain().Tip();
if (pindex) {
auto ddstats = g_digidollar_stats_index->LookUpStats(*pindex);
if (ddstats) {
activePositions = ddstats->vault_count;
}
}
}
result.pushKV("active_positions", static_cast<int64_t>(activePositions));
result.pushKV("oracle_price_age", 0); // TODO: Calculate age
UniValue dcaTier(UniValue::VOBJ);
dcaTier.pushKV("min_collateral", tier.minCollateral);
dcaTier.pushKV("max_collateral", tier.maxCollateral);
dcaTier.pushKV("multiplier", tier.multiplier);
dcaTier.pushKV("status", tier.status);
result.pushKV("dca_tier", dcaTier);
// Add ERR (Emergency Redemption Ratio) tier information
// ERR increases DD burn requirement, NOT reduces collateral!
// ratio = how much of original DD is "worth" -> burn 1/ratio DD to get FULL collateral
double errRatio = DigiDollar::ERR::EmergencyRedemptionRatio::CalculateERRAdjustment(systemHealth);
double burnMultiplier = 1.0;
std::string errDescription;
if (systemHealth >= 100) {
errDescription = "Normal (1.0x burn)";
errRatio = 1.0;
burnMultiplier = 1.0;
} else if (systemHealth >= 95) {
errDescription = "95-100%: 1.05x DD burn";
burnMultiplier = 1.0 / errRatio; // ~1.053x
} else if (systemHealth >= 90) {
errDescription = "90-95%: 1.11x DD burn";
burnMultiplier = 1.0 / errRatio; // ~1.111x
} else if (systemHealth >= 85) {
errDescription = "85-90%: 1.18x DD burn";
burnMultiplier = 1.0 / errRatio; // ~1.176x
} else {
errDescription = "<85%: 1.25x DD burn (max)";
burnMultiplier = 1.0 / errRatio; // 1.25x
}
UniValue errTier(UniValue::VOBJ);
errTier.pushKV("ratio", errRatio);
errTier.pushKV("burn_multiplier", burnMultiplier);
errTier.pushKV("description", errDescription);
result.pushKV("err_tier", errTier);
return result;
},
};
}
static RPCHelpMan getdcamultiplier()
{
return RPCHelpMan{"getdcamultiplier",
"\nGet current Dynamic Collateral Adjustment (DCA) multiplier.\n"
"Returns the multiplier applied to base collateral ratios for new mints.\n",
{
{"system_health", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Optional: calculate multiplier for specific health % (for testing)"}
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::NUM, "multiplier", "Current DCA multiplier (e.g., 1.0 = no adjustment, 2.0 = double collateral)"},
{RPCResult::Type::NUM, "system_health", "System health percentage used for calculation"},
{RPCResult::Type::STR, "tier_status", "Health tier: healthy, warning, critical, or emergency"},
{RPCResult::Type::STR, "description", "Human-readable description of DCA effect"}
}
},
RPCExamples{
HelpExampleCli("getdcamultiplier", "")
+ HelpExampleCli("getdcamultiplier", "130")
+ HelpExampleRpc("getdcamultiplier", "")
+ HelpExampleRpc("getdcamultiplier", "130")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// Check DigiDollar activation
{
const node::NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node);
const CBlockIndex* tip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
if (!DigiDollar::IsDigiDollarEnabled(tip, chainman)) {
throw JSONRPCError(RPC_MISC_ERROR, "DigiDollar is not yet active on this blockchain");
}
}
int systemHealth;
// Use provided health or calculate current
if (!request.params[0].isNull()) {
systemHealth = request.params[0].getInt<int>();
if (systemHealth < 0 || systemHealth > 30000) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "System health must be between 0 and 30000");
}
} else {
systemHealth = DynamicCollateralAdjustment::GetCurrentSystemHealth();
}
// Get DCA multiplier
double multiplier = DynamicCollateralAdjustment::GetDCAMultiplier(systemHealth);
auto tier = DynamicCollateralAdjustment::GetCurrentTier(systemHealth);
// Create description
std::string description;
if (multiplier == 1.0) {
description = "No additional collateral required (healthy system)";
} else {
description = strprintf("%.1fx base collateral required (%s system)",
multiplier, tier.status);
}
UniValue result(UniValue::VOBJ);
result.pushKV("multiplier", multiplier);
result.pushKV("system_health", systemHealth);
result.pushKV("tier_status", tier.status);
result.pushKV("description", description);
return result;
},
};
}
static RPCHelpMan calculatecollateralrequirement()
{
return RPCHelpMan{"calculatecollateralrequirement",
"\nCalculate DGB collateral requirement for a DigiDollar mint.\n"
"Uses current system health and DCA multipliers to determine\n"
"the exact amount of DGB needed for a given DD mint amount and lock period.\n",
{
{"dd_amount_cents", RPCArg::Type::NUM, RPCArg::Optional::NO, "DigiDollar amount to mint in cents (e.g., 10000 = $100)"},
{"lock_days", RPCArg::Type::NUM, RPCArg::Optional::NO, "Lock period in days (30, 90, 180, 365, 1095, 1825, 2555, or 3650)"},
{"oracle_price", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "DGB price in cents per DGB (uses current price if omitted)"}
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::NUM, "required_dgb", "Required DGB collateral amount"},
{RPCResult::Type::NUM, "dd_amount_cents", "DD amount being minted (in cents)"},
{RPCResult::Type::NUM, "dd_amount_usd", "DD amount being minted (in USD)"},
{RPCResult::Type::NUM, "lock_days", "Lock period in days"},
{RPCResult::Type::NUM, "lock_blocks", "Lock period in blocks"},
{RPCResult::Type::NUM, "base_ratio", "Base collateral ratio % for this lock period"},
{RPCResult::Type::NUM, "dca_multiplier", "DCA multiplier applied"},
{RPCResult::Type::NUM, "effective_ratio", "Final collateral ratio % (base * DCA)"},
{RPCResult::Type::NUM, "oracle_price_micro_usd", "DGB price used in micro-USD (1,000,000 = $1.00)"},
{RPCResult::Type::NUM, "oracle_price_usd", "DGB price used in USD"},
{RPCResult::Type::NUM, "system_health", "Current system health %"},
{RPCResult::Type::STR, "dca_tier", "Current DCA tier status"}
}
},
RPCExamples{
HelpExampleCli("calculatecollateralrequirement", "10000 365")
+ HelpExampleCli("calculatecollateralrequirement", "50000 1095 4000")
+ HelpExampleRpc("calculatecollateralrequirement", "10000, 365")
+ HelpExampleRpc("calculatecollateralrequirement", "50000, 1095, 4000")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// Check DigiDollar activation
{
const node::NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node);
const CBlockIndex* tip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
if (!DigiDollar::IsDigiDollarEnabled(tip, chainman)) {
throw JSONRPCError(RPC_MISC_ERROR, "DigiDollar is not yet active on this blockchain");
}
}
// Parse parameters
CAmount ddAmount = request.params[0].getInt<int64_t>();
int lockDays = request.params[1].getInt<int>();
// Get oracle price in micro-USD: use provided value or fetch from real oracle system
CAmount oraclePriceMicroUSD;
if (request.params.size() > 2 && !request.params[2].isNull()) {
// User-provided value is in micro-USD (1,000,000 = $1.00)
oraclePriceMicroUSD = request.params[2].getInt<int64_t>();
} else {
// Use real oracle price from OracleIntegration (returns micro-USD)
oraclePriceMicroUSD = OracleIntegration::GetCurrentOraclePriceMicroUSD();
if (oraclePriceMicroUSD <= 0 && Params().GetChainType() == ChainType::REGTEST) {
// Fall back to mock oracle ONLY in regtest
oraclePriceMicroUSD = MockOracleManager::GetInstance().GetCurrentPrice();
}
if (oraclePriceMicroUSD <= 0) {
throw JSONRPCError(RPC_MISC_ERROR, "No oracle price available. Start the oracle first with startoracle command.");
}
}
// Validate parameters
if (ddAmount <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "DD amount must be positive");
}
if (lockDays <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Lock days must be positive");
}
if (oraclePriceMicroUSD <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Oracle price must be positive");
}
// Get system parameters
const auto& params = Params();
const auto& ddParams = params.GetDigiDollarParams();
// Convert lock days to blocks
int64_t lockBlocks = DigiDollar::LockDaysToBlocks(lockDays);
// Get base collateral ratio
int baseRatio = DigiDollar::GetCollateralRatioForLockTime(lockBlocks, ddParams);
if (baseRatio <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Invalid lock period: %d days. Valid periods: 30, 90, 180, 365, 1095, 1825, 2555, 3650", lockDays));
}
// Get current system health
int systemHealth = DynamicCollateralAdjustment::GetCurrentSystemHealth();
double dcaMultiplier = DynamicCollateralAdjustment::GetDCAMultiplier(systemHealth);
int effectiveRatio = DynamicCollateralAdjustment::ApplyDCA(baseRatio, systemHealth);
auto tier = DynamicCollateralAdjustment::GetCurrentTier(systemHealth);
// Calculate required DGB using micro-USD precision
// Formula: Required_DGB_sats = (DD_cents * COIN * ratio * 100) / oracle_micro_usd
// Example: $100 DD at $0.00631 DGB with 150% ratio (oracle_micro_usd = 6310)
// = (10000 cents * 100000000 * 150 * 100) / 6310
// = 15,000,000,000,000,000 / 6310
// = 2,377,179,080,509 sats = ~23,772 DGB
// Use __int128 to avoid uint64 overflow for large DD amounts
__int128 numerator = static_cast<__int128>(ddAmount) * static_cast<__int128>(COIN) *
static_cast<__int128>(effectiveRatio) * 100;
__int128 result128 = numerator / static_cast<__int128>(oraclePriceMicroUSD);
if (result128 > static_cast<__int128>(MAX_MONEY)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Required collateral exceeds maximum money supply");
}
uint64_t requiredDGB = static_cast<uint64_t>(result128);
UniValue result(UniValue::VOBJ);
result.pushKV("required_dgb", ValueFromAmount(requiredDGB));
result.pushKV("dd_amount_cents", int64_t{ddAmount});
result.pushKV("dd_amount_usd", ddAmount / 100.0); // Convert cents to USD
result.pushKV("lock_days", lockDays);
result.pushKV("lock_blocks", int64_t{lockBlocks});
result.pushKV("base_ratio", baseRatio);
result.pushKV("dca_multiplier", dcaMultiplier);
result.pushKV("effective_ratio", effectiveRatio);
result.pushKV("oracle_price_micro_usd", int64_t{oraclePriceMicroUSD});
result.pushKV("oracle_price_usd", oraclePriceMicroUSD / 1000000.0);
result.pushKV("system_health", systemHealth);
result.pushKV("dca_tier", tier.status);
return result;
},
};
}
static RPCHelpMan getdigidollardeploymentinfo()
{
return RPCHelpMan{"getdigidollardeploymentinfo",
"\nGet DigiDollar BIP9 deployment activation status and information.\n"
"Returns detailed information about DigiDollar soft fork deployment status,\n"
"including activation state, signaling progress, and timeline.\n",
{},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::BOOL, "enabled", "Whether DigiDollar is currently enabled/active"},
{RPCResult::Type::STR, "status", "Deployment status (defined, started, locked_in, active, failed)"},
{RPCResult::Type::NUM, "bit", "Version bit used for BIP9 signaling"},
{RPCResult::Type::NUM, "start_time", "Start time for deployment signaling"},
{RPCResult::Type::NUM, "timeout", "Timeout for deployment"},
{RPCResult::Type::NUM, "min_activation_height", "Minimum activation height"},
{RPCResult::Type::NUM, "activation_height", /*optional=*/true, "Actual activation height (only present when active)"},
{RPCResult::Type::NUM, "blocks_until_timeout", /*optional=*/true, "Blocks remaining until timeout (only during started/locked_in)"},
{RPCResult::Type::NUM, "signaling_blocks", /*optional=*/true, "Blocks signaling support in current period (only during started/locked_in)"},
{RPCResult::Type::NUM, "threshold", /*optional=*/true, "Threshold required for activation (only during started/locked_in)"},
{RPCResult::Type::NUM, "period_blocks", /*optional=*/true, "Number of blocks in signaling period (only during started/locked_in)"},
{RPCResult::Type::NUM, "progress_percent", /*optional=*/true, "Signaling progress as percentage (only during started/locked_in)"}
}
},
RPCExamples{
HelpExampleCli("getdigidollardeploymentinfo", "")
+ HelpExampleRpc("getdigidollardeploymentinfo", "")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// NOTE: getdigidollardeploymentinfo is intentionally NOT gated behind
// activation. Users need this RPC to monitor BIP9 deployment progress
// (DEFINED → STARTED → LOCKED_IN → ACTIVE). Gating it would make it
// impossible to check when DigiDollar will activate.
const ChainstateManager& chainman = EnsureAnyChainman(request.context);
LOCK(cs_main);
const Chainstate& active_chainstate = chainman.ActiveChainstate();
const CBlockIndex* tip = active_chainstate.m_chain.Tip();
UniValue result(UniValue::VOBJ);
// Check if DigiDollar is currently enabled
bool enabled = DigiDollar::IsDigiDollarEnabled(tip, chainman);
result.pushKV("enabled", enabled);
// Get deployment parameters
const Consensus::Params& consensusParams = chainman.GetConsensus();
const Consensus::BIP9Deployment& deployment = consensusParams.vDeployments[Consensus::DEPLOYMENT_DIGIDOLLAR];
result.pushKV("bit", deployment.bit);
result.pushKV("start_time", deployment.nStartTime);
result.pushKV("timeout", deployment.nTimeout);
result.pushKV("min_activation_height", deployment.min_activation_height);
// Get deployment state and statistics
ThresholdState state = chainman.m_versionbitscache.State(tip, consensusParams, Consensus::DEPLOYMENT_DIGIDOLLAR);
const char* status_str = "unknown";
switch (state) {
case ThresholdState::DEFINED: status_str = "defined"; break;
case ThresholdState::STARTED: status_str = "started"; break;
case ThresholdState::LOCKED_IN: status_str = "locked_in"; break;
case ThresholdState::ACTIVE: status_str = "active"; break;
case ThresholdState::FAILED: status_str = "failed"; break;
}
result.pushKV("status", status_str);
// Get statistics for signaling progress
if (tip && (state == ThresholdState::STARTED || state == ThresholdState::LOCKED_IN)) {
BIP9Stats stats = chainman.m_versionbitscache.Statistics(tip, consensusParams, Consensus::DEPLOYMENT_DIGIDOLLAR);
result.pushKV("blocks_until_timeout", stats.period - stats.elapsed);
result.pushKV("signaling_blocks", stats.count);
result.pushKV("threshold", stats.threshold);
result.pushKV("period_blocks", stats.period);
result.pushKV("progress_percent", stats.threshold > 0 ? (100.0 * stats.count) / stats.threshold : 0.0);
}
// Get activation height if active
if (state == ThresholdState::ACTIVE) {
// Find the activation height by searching backwards
const CBlockIndex* pindex = tip;
while (pindex && pindex->pprev) {
if (chainman.m_versionbitscache.State(pindex->pprev, consensusParams, Consensus::DEPLOYMENT_DIGIDOLLAR) != ThresholdState::ACTIVE) {
result.pushKV("activation_height", pindex->nHeight);
break;
}
pindex = pindex->pprev;
}
}
return result;
},
};
}
// =============================================================================
// CORE RPC COMMANDS (Task 5.7)
// =============================================================================
RPCHelpMan mintdigidollar()
{
return RPCHelpMan{"mintdigidollar",
"\nMint new DigiDollar with DGB collateral.\n"
"Creates a new DigiDollar position by locking DGB as collateral.\n"
"The amount of collateral required depends on the lock period and current system health.\n",
{
{"dd_amount", RPCArg::Type::NUM, RPCArg::Optional::NO, "Amount of DigiDollar to mint (in USD cents, e.g., 10000 = $100)", RPCArgOptions{.skip_type_check = true}},
{"lock_tier", RPCArg::Type::NUM, RPCArg::Optional::NO, "Lock tier 0-9 (0=1h testing, 1=30d, 2=90d, 3=180d, 4=1y, 5=2y, 6=3y, 7=5y, 8=7y, 9=10y)", RPCArgOptions{.skip_type_check = true}},
{"fee_rate", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Fee rate in sat/kB (default: 100000)", RPCArgOptions{.skip_type_check = true}}
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR_HEX, "txid", "Transaction ID of the mint transaction"},
{RPCResult::Type::STR_AMOUNT, "dd_minted", "Amount of DigiDollar minted (in cents)"},
{RPCResult::Type::STR_AMOUNT, "dgb_collateral", "DGB locked as collateral"},
{RPCResult::Type::NUM, "lock_tier", "Lock tier used"},
{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_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{
HelpExampleCli("mintdigidollar", "10000 3") +
HelpExampleCli("mintdigidollar", "50000 5 0.001") +
HelpExampleRpc("mintdigidollar", "10000, 3") +
HelpExampleRpc("mintdigidollar", "50000, 5, 0.001")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// Get wallet first (wallet RPCs have WalletContext, not NodeContext)
std::shared_ptr<wallet::CWallet> pwallet = wallet::GetWalletForJSONRPCRequest(request);
if (!pwallet) throw JSONRPCError(RPC_WALLET_NOT_FOUND, "No wallet is loaded");
// Check DigiDollar activation via wallet's chain interface
{
node::NodeContext* node_ctx = pwallet->chain().context();
if (!node_ctx) throw JSONRPCError(RPC_INTERNAL_ERROR, "Node context unavailable");
ChainstateManager& chainman = *node_ctx->chainman;
const CBlockIndex* tip = WITH_LOCK(cs_main, return chainman.ActiveChain().Tip());
if (!DigiDollar::IsDigiDollarEnabled(tip, chainman)) {
throw JSONRPCError(RPC_MISC_ERROR, "DigiDollar is not yet active on this blockchain");
}
}
// Ensure wallet is unlocked
wallet::EnsureWalletIsUnlocked(*pwallet);
// Parse parameters
CAmount ddAmount = request.params[0].getInt<int64_t>();
int lockTier = request.params[1].getInt<int>();
// DigiDollar transactions MUST pay at least 0.1 DGB fee to miners
// Use a high fee rate to ensure the minimum is met for all transaction sizes
// MIN_DD_TX_FEE = 10,000,000 satoshis = 0.1 DGB
// For a typical 300-byte tx, we need feeRate = 10,000,000 / 300 * 1000 = 33,333,333 sat/kB
// We use 35,000,000 sat/kB to ensure minimum is always met
static const CAmount MIN_DD_FEE_RATE = 35000000; // 0.35 DGB/kB ensures min 0.1 DGB for typical tx
CAmount feeRate = request.params.size() > 2 && !request.params[2].isNull() ?
std::max(request.params[2].getInt<int64_t>(), MIN_DD_FEE_RATE) : MIN_DD_FEE_RATE;
// Validate parameters
if (ddAmount <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "DigiDollar amount must be positive");
}
// Get consensus parameters for mint amount validation
const auto& chainParams = Params();
const auto& ddParams = chainParams.GetDigiDollarParams();
// Validate against consensus mint limits
if (!DigiDollar::IsValidMintAmount(ddAmount, ddParams)) {
if (ddAmount < ddParams.minMintAmount) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Minimum mint amount is $%d (%d cents)",
ddParams.minMintAmount / 100, ddParams.minMintAmount));
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Maximum mint amount is $%d (%d cents)",
ddParams.maxMintAmount / 100, ddParams.maxMintAmount));
}
}
if (lockTier < 0 || lockTier > 9) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Lock tier must be between 0 and 9 (0 = 1 hour testing tier)");
}
// Get current height from wallet's chain interface
int currentHeight = pwallet->GetLastBlockHeight();
// Get oracle price in micro-USD from real oracle system first, fall back to mock only in regtest
CAmount oraclePriceMicroUSD = OracleIntegration::GetCurrentOraclePriceMicroUSD();
if (oraclePriceMicroUSD <= 0 && Params().GetChainType() == ChainType::REGTEST) {
oraclePriceMicroUSD = MockOracleManager::GetInstance().GetCurrentPrice();
}
if (oraclePriceMicroUSD <= 0) {
throw JSONRPCError(RPC_MISC_ERROR, "No oracle price available. Start the oracle first with startoracle command.");
}
// ERR CHECK: Block minting during emergency state
// Calculate health directly using wallet's DD positions and current oracle price
// This is more reliable than cached metrics which may be stale
if (pwallet->GetDDWallet()) {
LOCK(pwallet->cs_wallet);
CAmount totalDD = 0;
CAmount totalCollateral = 0;
// Get all active positions from wallet
auto positions = pwallet->GetDDWallet()->GetDDTimeLocks(true); // true = active only
for (const auto& pos : positions) {
totalDD += pos.dd_minted;
totalCollateral += pos.dgb_collateral;
}
// Only check health if there are existing DD positions
if (totalDD > 0 && totalCollateral > 0) {
// Convert micro-USD to millicents for health calculation
CAmount oraclePriceMillicents = oraclePriceMicroUSD / 10;
int systemHealth = DynamicCollateralAdjustment::CalculateSystemHealth(
totalCollateral, totalDD, oraclePriceMillicents);
LogPrintf("DigiDollar RPC Mint: Health check - DD=%lld, collateral=%lld, price=%lld, health=%d%%\n",
static_cast<long long>(totalDD),
static_cast<long long>(totalCollateral),
static_cast<long long>(oraclePriceMillicents),
systemHealth);
// Block minting if system health is below 100% (emergency state)
if (systemHealth < 100) {
throw JSONRPCError(RPC_MISC_ERROR,
strprintf("Minting blocked: System is in emergency state (health: %d%%). "
"Wait for system health to recover above 100%% before minting new DigiDollars.",
systemHealth));
}
}
}
// Convert lock tier to days
int lockDays = GetLockDaysForTier(lockTier);
// Get available UTXOs from wallet and build value map
std::vector<COutPoint> availableUtxos;
std::map<COutPoint, CAmount> utxoValues;
{
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;
}
}
if (availableUtxos.empty()) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "No available UTXOs for collateral");
}
// Generate owner key from wallet using HD derivation
// This allows the key to be recovered from wallet seed
CKey ownerKey;
{
LOCK(pwallet->cs_wallet);
ownerKey = GetHDKeyForDigiDollar(pwallet.get(), "dd-owner");
if (!ownerKey.IsValid()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to generate owner key for DD mint");
}
}
// Create custom MintTxBuilder that can look up actual UTXO values
// This is critical - without this, SelectCoins uses hardcoded placeholder values
// and selects hundreds of UTXOs, wasting millions of DGB!
class RpcMintTxBuilder : public DigiDollar::MintTxBuilder {
private:
const std::map<COutPoint, CAmount>& m_utxo_values;
public:
RpcMintTxBuilder(const CChainParams& params, int height, CAmount price,
const std::map<COutPoint, CAmount>& utxo_values)
: MintTxBuilder(params, height, price), m_utxo_values(utxo_values) {}
CAmount GetDGBFromUTXO(const COutPoint& outpoint) const override {
auto it = m_utxo_values.find(outpoint);
if (it != m_utxo_values.end()) {
return it->second;
}
return 0; // UTXO not found
}
};
// Build mint transaction using custom RpcMintTxBuilder with UTXO value lookup
// Note: MintTxBuilder now expects micro-USD price
RpcMintTxBuilder builder(Params(), currentHeight, oraclePriceMicroUSD, utxoValues);
DigiDollar::TxBuilderMintParams params;
params.ddAmount = ddAmount; // Amount in cents (e.g., 5000 = $50.00)
params.lockDays = lockDays;
params.lockTier = lockTier; // Store tier explicitly in OP_RETURN for exact reconstruction
params.ownerKey = ownerKey;
params.feeRate = feeRate;
params.utxos = availableUtxos;
// CRITICAL FIX: Get a proper change address from the wallet for DGB change output
// This ensures the wallet recognizes the change output as its own!
{
LOCK(pwallet->cs_wallet);
auto op_dest = pwallet->GetNewChangeDestination(OutputType::BECH32);
if (op_dest) {
params.dgbChangeDest = *op_dest;
LogPrintf("DigiDollar RPC Mint: Using wallet change address for DGB change output\n");
} else {
LogPrintf("DigiDollar RPC Mint: WARNING - Could not get change destination!\n");
}
}
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<wallet::CRecipient> 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 <your_address> <amount>",
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",