diff options
| author | mtg0001 <106211915+mtg0001@users.noreply.github.com> | 2026-08-03 10:43:42 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-03 10:43:42 -0300 |
| commit | 57441faffb02f4f2eea37d31b29728de25f18bef (patch) | |
| tree | edade03dddf472790e8bc7e3bc52d3e8f8eac87b | |
| parent | 5e37647023463cec4331dc48dc360ba60b22c888 (diff) | |
Add wallet recovery balance audit (#117)
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
| -rw-r--r-- | src/gui.cpp | 54 | ||||
| -rw-r--r-- | src/main.cpp | 87 | ||||
| -rw-r--r-- | src/main.h | 31 | ||||
| -rw-r--r-- | src/main_gui.cpp | 12 | ||||
| -rw-r--r-- | src/selftest.cpp | 36 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 59 | ||||
| -rw-r--r-- | src/walletcmd.h | 4 |
7 files changed, 280 insertions, 3 deletions
diff --git a/src/gui.cpp b/src/gui.cpp index e3a8499..2335bf9 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -65,6 +65,8 @@ static std::string g_sendStatus; | |||
| 65 | static char g_backupPath[512] = {}; | 65 | static char g_backupPath[512] = {}; |
| 66 | static std::string g_backupStatus; | 66 | static std::string g_backupStatus; |
| 67 | static int64 g_lastWalletBackup = 0; | 67 | static int64 g_lastWalletBackup = 0; |
| 68 | static WalletRecoveryAudit g_recoveryAudit; | ||
| 69 | static int64 g_recoveryAuditTime = 0; | ||
| 68 | static bool g_walletSafetyLoaded = false; | 70 | static bool g_walletSafetyLoaded = false; |
| 69 | static char g_participantPool[256] = {}; | 71 | static char g_participantPool[256] = {}; |
| 70 | static char g_poolName[128] = {}; | 72 | static char g_poolName[128] = {}; |
| @@ -184,6 +186,15 @@ static void LoadWalletSafetyState() | |||
| 184 | g_walletSafetyLoaded = true; | 186 | g_walletSafetyLoaded = true; |
| 185 | } | 187 | } |
| 186 | 188 | ||
| 189 | static void RefreshRecoveryAudit(bool fForce=false) | ||
| 190 | { | ||
| 191 | int64 nNow = GetTime(); | ||
| 192 | if (!fForce && g_recoveryAuditTime != 0 && nNow - g_recoveryAuditTime < 5) | ||
| 193 | return; | ||
| 194 | g_recoveryAudit = GetWalletRecoveryAudit(); | ||
| 195 | g_recoveryAuditTime = nNow; | ||
| 196 | } | ||
| 197 | |||
| 187 | static int KeyPoolCount() | 198 | static int KeyPoolCount() |
| 188 | { | 199 | { |
| 189 | int n = 0; | 200 | int n = 0; |
| @@ -267,7 +278,11 @@ static void RefreshWallet() | |||
| 267 | g_lastWalletRefresh = GetTime(); | 278 | g_lastWalletRefresh = GetTime(); |
| 268 | } | 279 | } |
| 269 | 280 | ||
| 270 | void MainFrameRepaint() { g_needRefresh = true; } | 281 | void MainFrameRepaint() |
| 282 | { | ||
| 283 | g_needRefresh = true; | ||
| 284 | g_recoveryAuditTime = 0; | ||
| 285 | } | ||
| 271 | 286 | ||
| 272 | // DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is | 287 | // DateTimeStr moved to util.cpp: main.cpp logs block times with it, so it is |
| 273 | // needed by builds that have no GUI at all. | 288 | // needed by builds that have no GUI at all. |
| @@ -967,6 +982,7 @@ static void RestoreThread(std::string strPhrase) | |||
| 967 | "Check the words and their order.", nDerived); | 982 | "Check the words and their order.", nDerived); |
| 968 | } | 983 | } |
| 969 | g_restoreRunning.store(false); | 984 | g_restoreRunning.store(false); |
| 985 | g_recoveryAuditTime = 0; | ||
| 970 | g_needRefresh = true; | 986 | g_needRefresh = true; |
| 971 | } | 987 | } |
| 972 | 988 | ||
| @@ -1021,6 +1037,7 @@ static void DrawCreatePhraseDialog() | |||
| 1021 | if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) | 1037 | if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError)) |
| 1022 | { | 1038 | { |
| 1023 | TopUpKeyPool(); | 1039 | TopUpKeyPool(); |
| 1040 | g_recoveryAuditTime = 0; | ||
| 1024 | g_phraseStatus = "Recovery phrase created. New addresses come from it."; | 1041 | g_phraseStatus = "Recovery phrase created. New addresses come from it."; |
| 1025 | } | 1042 | } |
| 1026 | else | 1043 | else |
| @@ -1141,8 +1158,9 @@ static void DrawWalletSafetyDialog() | |||
| 1141 | { | 1158 | { |
| 1142 | if (!g_showWalletSafety) return; | 1159 | if (!g_showWalletSafety) return; |
| 1143 | LoadWalletSafetyState(); | 1160 | LoadWalletSafetyState(); |
| 1161 | RefreshRecoveryAudit(); | ||
| 1144 | 1162 | ||
| 1145 | ImGui::SetNextWindowSize(ImVec2(650.0f, 390.0f), ImGuiCond_Always); | 1163 | ImGui::SetNextWindowSize(ImVec2(650.0f, 450.0f), ImGuiCond_Always); |
| 1146 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | 1164 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), |
| 1147 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); | 1165 | ImGuiCond_Always, ImVec2(0.5f, 0.5f)); |
| 1148 | if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, | 1166 | if (ImGui::Begin("Wallet Safety", &g_showWalletSafety, |
| @@ -1229,6 +1247,38 @@ static void DrawWalletSafetyDialog() | |||
| 1229 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), | 1247 | ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), |
| 1230 | "No recovery phrase. Only a file backup can rebuild this wallet."); | 1248 | "No recovery phrase. Only a file backup can rebuild this wallet."); |
| 1231 | 1249 | ||
| 1250 | ImGui::Text("Phrase-backed spendable balance: %s BTF", | ||
| 1251 | FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); | ||
| 1252 | ImGui::Text("Wallet.dat-only spendable balance: %s BTF", | ||
| 1253 | FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); | ||
| 1254 | ImGui::Text("Phrase-backed immature mining rewards: %s BTF", | ||
| 1255 | FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); | ||
| 1256 | ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", | ||
| 1257 | FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); | ||
| 1258 | if (!g_recoveryAudit.fDeriveComplete) | ||
| 1259 | { | ||
| 1260 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 1261 | ImGui::TextWrapped("%s", g_recoveryAudit.strDeriveError.c_str()); | ||
| 1262 | ImGui::PopStyleColor(); | ||
| 1263 | } | ||
| 1264 | if (g_recoveryAudit.nLegacyCredit > 0 || | ||
| 1265 | g_recoveryAudit.nLegacyImmatureCredit > 0) | ||
| 1266 | { | ||
| 1267 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); | ||
| 1268 | ImGui::TextWrapped( | ||
| 1269 | "Some coins are on keys the phrase does not reproduce. " | ||
| 1270 | "Keep wallet.dat backups until that balance has been moved to a " | ||
| 1271 | "phrase-backed address."); | ||
| 1272 | ImGui::PopStyleColor(); | ||
| 1273 | } | ||
| 1274 | else if (HaveHDSeed() && | ||
| 1275 | g_recoveryAudit.nRecoverableCredit + | ||
| 1276 | g_recoveryAudit.nRecoverableImmatureCredit > 0) | ||
| 1277 | { | ||
| 1278 | ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), | ||
| 1279 | "All known wallet balance is covered by the phrase."); | ||
| 1280 | } | ||
| 1281 | |||
| 1232 | if (!HaveHDSeed()) | 1282 | if (!HaveHDSeed()) |
| 1233 | { | 1283 | { |
| 1234 | if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) | 1284 | if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f))) |
diff --git a/src/main.cpp b/src/main.cpp index 1b09a1a..75647ba 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -3660,6 +3660,93 @@ bool BitcoinMiner(int nThreadId) | |||
| 3660 | // | 3660 | // |
| 3661 | 3661 | ||
| 3662 | 3662 | ||
| 3663 | WalletRecoveryAudit GetWalletRecoveryAudit() | ||
| 3664 | { | ||
| 3665 | WalletRecoveryAudit audit; | ||
| 3666 | set<vector<unsigned char> > setDerivedPubKeys; | ||
| 3667 | |||
| 3668 | CRITICAL_BLOCK(cs_keyPool) | ||
| 3669 | { | ||
| 3670 | audit.fHaveSeed = HaveHDSeed(); | ||
| 3671 | audit.nDerivedKnown = nHDNext; | ||
| 3672 | if (audit.fHaveSeed) | ||
| 3673 | { | ||
| 3674 | string strError; | ||
| 3675 | for (unsigned int i = 0; i < nHDNext; i++) | ||
| 3676 | { | ||
| 3677 | CKey key; | ||
| 3678 | if (!DeriveHDKey(i, key, strError)) | ||
| 3679 | { | ||
| 3680 | audit.fDeriveComplete = false; | ||
| 3681 | audit.strDeriveError = strprintf("derivation failed at index %u: %s", | ||
| 3682 | i, strError.c_str()); | ||
| 3683 | break; | ||
| 3684 | } | ||
| 3685 | setDerivedPubKeys.insert(key.GetPubKey()); | ||
| 3686 | } | ||
| 3687 | } | ||
| 3688 | } | ||
| 3689 | |||
| 3690 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 3691 | { | ||
| 3692 | for (map<uint256, CWalletTx>::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) | ||
| 3693 | { | ||
| 3694 | CWalletTx& wtx = (*it).second; | ||
| 3695 | if (!wtx.IsFinal() || wtx.fSpent) | ||
| 3696 | continue; | ||
| 3697 | bool fImmature = wtx.IsCoinBase() && wtx.GetBlocksToMaturity() > 0; | ||
| 3698 | |||
| 3699 | bool fTxRecoverable = false; | ||
| 3700 | bool fTxLegacy = false; | ||
| 3701 | for (int i = 0; i < (int)wtx.vout.size(); i++) | ||
| 3702 | { | ||
| 3703 | const CTxOut& txout = wtx.vout[i]; | ||
| 3704 | if (!txout.IsMine()) | ||
| 3705 | continue; | ||
| 3706 | |||
| 3707 | vector<unsigned char> vchPubKey; | ||
| 3708 | bool fRecoverable = false; | ||
| 3709 | if (audit.fHaveSeed && ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) | ||
| 3710 | fRecoverable = setDerivedPubKeys.count(vchPubKey) > 0; | ||
| 3711 | |||
| 3712 | if (fRecoverable) | ||
| 3713 | { | ||
| 3714 | if (fImmature) | ||
| 3715 | audit.nRecoverableImmatureCredit += txout.nValue; | ||
| 3716 | else | ||
| 3717 | audit.nRecoverableCredit += txout.nValue; | ||
| 3718 | fTxRecoverable = true; | ||
| 3719 | } | ||
| 3720 | else | ||
| 3721 | { | ||
| 3722 | if (fImmature) | ||
| 3723 | audit.nLegacyImmatureCredit += txout.nValue; | ||
| 3724 | else | ||
| 3725 | audit.nLegacyCredit += txout.nValue; | ||
| 3726 | fTxLegacy = true; | ||
| 3727 | } | ||
| 3728 | } | ||
| 3729 | |||
| 3730 | if (fTxRecoverable) | ||
| 3731 | { | ||
| 3732 | if (fImmature) | ||
| 3733 | audit.nRecoverableImmatureTx++; | ||
| 3734 | else | ||
| 3735 | audit.nRecoverableTx++; | ||
| 3736 | } | ||
| 3737 | if (fTxLegacy) | ||
| 3738 | { | ||
| 3739 | if (fImmature) | ||
| 3740 | audit.nLegacyImmatureTx++; | ||
| 3741 | else | ||
| 3742 | audit.nLegacyTx++; | ||
| 3743 | } | ||
| 3744 | } | ||
| 3745 | } | ||
| 3746 | |||
| 3747 | return audit; | ||
| 3748 | } | ||
| 3749 | |||
| 3663 | int64 GetBalance() | 3750 | int64 GetBalance() |
| 3664 | { | 3751 | { |
| 3665 | int64 nStart, nEnd; | 3752 | int64 nStart, nEnd; |
| @@ -217,6 +217,37 @@ extern CCriticalSection cs_mapTransactions; | |||
| 217 | bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv); | 217 | bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv); |
| 218 | bool SendMessages(CNode* pto); | 218 | bool SendMessages(CNode* pto); |
| 219 | int64 GetBalance(); | 219 | int64 GetBalance(); |
| 220 | struct WalletRecoveryAudit | ||
| 221 | { | ||
| 222 | bool fHaveSeed; | ||
| 223 | bool fDeriveComplete; | ||
| 224 | unsigned int nDerivedKnown; | ||
| 225 | int nRecoverableTx; | ||
| 226 | int nLegacyTx; | ||
| 227 | int nRecoverableImmatureTx; | ||
| 228 | int nLegacyImmatureTx; | ||
| 229 | int64 nRecoverableCredit; | ||
| 230 | int64 nLegacyCredit; | ||
| 231 | int64 nRecoverableImmatureCredit; | ||
| 232 | int64 nLegacyImmatureCredit; | ||
| 233 | string strDeriveError; | ||
| 234 | |||
| 235 | WalletRecoveryAudit() | ||
| 236 | { | ||
| 237 | fHaveSeed = false; | ||
| 238 | fDeriveComplete = true; | ||
| 239 | nDerivedKnown = 0; | ||
| 240 | nRecoverableTx = 0; | ||
| 241 | nLegacyTx = 0; | ||
| 242 | nRecoverableImmatureTx = 0; | ||
| 243 | nLegacyImmatureTx = 0; | ||
| 244 | nRecoverableCredit = 0; | ||
| 245 | nLegacyCredit = 0; | ||
| 246 | nRecoverableImmatureCredit = 0; | ||
| 247 | nLegacyImmatureCredit = 0; | ||
| 248 | } | ||
| 249 | }; | ||
| 250 | WalletRecoveryAudit GetWalletRecoveryAudit(); | ||
| 220 | bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& txNew, int64& nFeeRequiredRet); | 251 | bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& txNew, int64& nFeeRequiredRet); |
| 221 | bool CommitTransactionSpent(const CWalletTx& wtxNew); | 252 | bool CommitTransactionSpent(const CWalletTx& wtxNew); |
| 222 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); | 253 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 32fcdef..1b72813 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -93,7 +93,10 @@ static void PrintUsage() | |||
| 93 | printf(" /restoredepth=N (with /restorephrase: derive at least N\n"); | 93 | printf(" /restoredepth=N (with /restorephrase: derive at least N\n"); |
| 94 | printf(" addresses before giving up)\n"); | 94 | printf(" addresses before giving up)\n"); |
| 95 | printf(" /showderived=N (list the first N addresses a phrase\n"); | 95 | printf(" /showderived=N (list the first N addresses a phrase\n"); |
| 96 | printf(" produces, without installing it)\n"); | 96 | printf(" installed in this wallet derives)\n"); |
| 97 | printf(" /recoveryaudit (show how much spendable balance is\n"); | ||
| 98 | printf(" covered by the recovery phrase;\n"); | ||
| 99 | printf(" exits 2 when wallet.dat is still needed)\n"); | ||
| 97 | printf(" /rescan (walk the chain for coins this wallet owns\n"); | 100 | printf(" /rescan (walk the chain for coins this wallet owns\n"); |
| 98 | printf(" but never recorded, then exit)\n"); | 101 | printf(" but never recorded, then exit)\n"); |
| 99 | printf("\n"); | 102 | printf("\n"); |
| @@ -385,6 +388,13 @@ int main(int argc, char* argv[]) | |||
| 385 | return nRet; | 388 | return nRet; |
| 386 | } | 389 | } |
| 387 | 390 | ||
| 391 | if (arg(argc,argv,"/recoveryaudit") || arg(argc,argv,"-recoveryaudit")) | ||
| 392 | { | ||
| 393 | int nRet = CmdRecoveryAudit(); | ||
| 394 | DBFlush(true); | ||
| 395 | return nRet; | ||
| 396 | } | ||
| 397 | |||
| 388 | if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) | 398 | if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) |
| 389 | { | 399 | { |
| 390 | int nRet = CmdNewPhrase(); | 400 | int nRet = CmdNewPhrase(); |
diff --git a/src/selftest.cpp b/src/selftest.cpp index 67f96f8..df55bcd 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -336,6 +336,7 @@ static int RunWalletHDSelfTest() | |||
| 336 | 336 | ||
| 337 | // The address the wallet was showing before any of this. It has to | 337 | // The address the wallet was showing before any of this. It has to |
| 338 | // survive under a name that says it is not covered by the phrase. | 338 | // survive under a name that says it is not covered by the phrase. |
| 339 | std::vector<unsigned char> vchPreSeedKey = keyUser.GetPubKey(); | ||
| 339 | std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey()); | 340 | std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey()); |
| 340 | 341 | ||
| 341 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), | 342 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), |
| @@ -449,6 +450,41 @@ static int RunWalletHDSelfTest() | |||
| 449 | } | 450 | } |
| 450 | nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; | 451 | nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; |
| 451 | nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; | 452 | nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; |
| 453 | |||
| 454 | // A recovery phrase is only useful if the wallet can tell the user | ||
| 455 | // what today's spendable balance would actually come back from it. | ||
| 456 | CKey keyAuditDerived; | ||
| 457 | if (!DeriveHDKey(0, keyAuditDerived, strError)) | ||
| 458 | throw std::runtime_error("audit derivation failed: " + strError); | ||
| 459 | |||
| 460 | CWalletTx wtxLegacy; | ||
| 461 | wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); | ||
| 462 | CWalletTx wtxDerived; | ||
| 463 | wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); | ||
| 464 | CWalletTx wtxImmatureLegacy; | ||
| 465 | wtxImmatureLegacy.vin.push_back(CTxIn()); | ||
| 466 | wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); | ||
| 467 | |||
| 468 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 469 | { | ||
| 470 | mapWallet.clear(); | ||
| 471 | mapWallet[wtxLegacy.GetHash()] = wtxLegacy; | ||
| 472 | mapWallet[wtxDerived.GetHash()] = wtxDerived; | ||
| 473 | mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; | ||
| 474 | } | ||
| 475 | |||
| 476 | WalletRecoveryAudit audit = GetWalletRecoveryAudit(); | ||
| 477 | nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; | ||
| 478 | nFail += Check(audit.nLegacyCredit == 5 * COIN, | ||
| 479 | "the recovery audit finds wallet.dat-only balance") ? 0 : 1; | ||
| 480 | nFail += Check(audit.nRecoverableCredit == 7 * COIN, | ||
| 481 | "the recovery audit finds phrase-backed balance") ? 0 : 1; | ||
| 482 | nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 1, | ||
| 483 | "the recovery audit counts legacy and phrase-backed transactions") ? 0 : 1; | ||
| 484 | nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, | ||
| 485 | "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; | ||
| 486 | nFail += Check(audit.nLegacyImmatureTx == 1, | ||
| 487 | "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; | ||
| 452 | } | 488 | } |
| 453 | catch (const std::exception& e) | 489 | catch (const std::exception& e) |
| 454 | { | 490 | { |
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6d5f32b..41b4ed2 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -272,6 +272,65 @@ int CmdShowDerived(int nCount) | |||
| 272 | return 0; | 272 | return 0; |
| 273 | } | 273 | } |
| 274 | 274 | ||
| 275 | int CmdRecoveryAudit() | ||
| 276 | { | ||
| 277 | AttachTerminal(); | ||
| 278 | |||
| 279 | WalletRecoveryAudit audit = GetWalletRecoveryAudit(); | ||
| 280 | int64 nTotal = audit.nRecoverableCredit + audit.nLegacyCredit; | ||
| 281 | int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit; | ||
| 282 | |||
| 283 | printf("Wallet recovery audit\n"); | ||
| 284 | printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); | ||
| 285 | if (audit.fHaveSeed) | ||
| 286 | printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); | ||
| 287 | if (!audit.fDeriveComplete) | ||
| 288 | printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); | ||
| 289 | printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); | ||
| 290 | printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n", | ||
| 291 | FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx); | ||
| 292 | printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n", | ||
| 293 | FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx); | ||
| 294 | printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); | ||
| 295 | printf(" phrase-backed immature: %s BTF (%d transaction(s))\n", | ||
| 296 | FormatMoney(audit.nRecoverableImmatureCredit).c_str(), | ||
| 297 | audit.nRecoverableImmatureTx); | ||
| 298 | printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n", | ||
| 299 | FormatMoney(audit.nLegacyImmatureCredit).c_str(), | ||
| 300 | audit.nLegacyImmatureTx); | ||
| 301 | |||
| 302 | if (!audit.fHaveSeed) | ||
| 303 | { | ||
| 304 | printf("\n"); | ||
| 305 | printf("This wallet has no recovery phrase. A file backup is the only backup.\n"); | ||
| 306 | fflush(stdout); | ||
| 307 | return nTotal + nImmatureTotal > 0 ? 2 : 0; | ||
| 308 | } | ||
| 309 | if (!audit.fDeriveComplete) | ||
| 310 | { | ||
| 311 | printf("\n"); | ||
| 312 | printf("Warning: the audit could not derive every known phrase key, so coverage is incomplete.\n"); | ||
| 313 | fflush(stdout); | ||
| 314 | return 2; | ||
| 315 | } | ||
| 316 | if (audit.nLegacyCredit > 0 || audit.nLegacyImmatureCredit > 0) | ||
| 317 | { | ||
| 318 | printf("\n"); | ||
| 319 | printf("Warning: some coins are on keys the phrase does not reproduce.\n"); | ||
| 320 | printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n"); | ||
| 321 | fflush(stdout); | ||
| 322 | return 2; | ||
| 323 | } | ||
| 324 | |||
| 325 | printf("\n"); | ||
| 326 | if (nTotal + nImmatureTotal > 0) | ||
| 327 | printf("All known wallet balance is covered by the recovery phrase.\n"); | ||
| 328 | else | ||
| 329 | printf("No wallet balance found yet.\n"); | ||
| 330 | fflush(stdout); | ||
| 331 | return 0; | ||
| 332 | } | ||
| 333 | |||
| 275 | // Spend, from the command line. | 334 | // Spend, from the command line. |
| 276 | // | 335 | // |
| 277 | // SendMoney() has been in this tree since 0.1.0 and only the window ever called | 336 | // SendMoney() has been in this tree since 0.1.0 and only the window ever called |
diff --git a/src/walletcmd.h b/src/walletcmd.h index 89834d8..796356b 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h | |||
| @@ -40,4 +40,8 @@ int CmdNewAddress(); | |||
| 40 | // Diagnostic: print the first nCount addresses this wallet's phrase derives. | 40 | // Diagnostic: print the first nCount addresses this wallet's phrase derives. |
| 41 | int CmdShowDerived(int nCount); | 41 | int CmdShowDerived(int nCount); |
| 42 | 42 | ||
| 43 | // Diagnostic: print whether the currently spendable wallet balance is covered | ||
| 44 | // by the installed recovery phrase or still depends on wallet.dat-only keys. | ||
| 45 | int CmdRecoveryAudit(); | ||
| 46 | |||
| 43 | #endif | 47 | #endif |