upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/gui.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-01 17:57:07 -0300
committerGitHub <noreply@github.com>2026-08-01 17:57:07 -0300
commit7de7ef89d3365f8f9c2eb2b6ed0193c3033e221c (patch)
treedba509ae6b245be76b88ad0ae467aeb997a85b4d /src/gui.cpp
parentce59d8c72c667ce638bd12f37db26d8b3408a5d2 (diff)
wallet: create and restore a recovery phrase (#96)
Adds -newphrase, -restorephrase, -restoredepth, -newaddress and -showderived, plus the window buttons that reach the same code. Restore derives forward in batches of 100 and rescans after each, stopping when a whole batch turns up nothing; the phrase is shown on the terminal only, never in debug.log. Proved end to end on a live wallet: paid a derived address, deleted wallet.dat, restored from the twelve words and recovered the coin. Coins on the pre-seed default address do not come back, and the command says so.
Diffstat (limited to 'src/gui.cpp')
-rw-r--r--src/gui.cpp258
1 files changed, 258 insertions, 0 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
index 51b5d5f..e800244 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -12,6 +12,11 @@
12#include <cstdio> 12#include <cstdio>
13#include <sstream> 13#include <sstream>
14#include <iomanip> 14#include <iomanip>
15#include <atomic>
16#include <mutex>
17#include <thread>
18#include "bip32.h"
19#include "walletcmd.h"
15#include "font_roboto.h" 20#include "font_roboto.h"
16 21
17// --------------------------------------------------------------------------- 22// ---------------------------------------------------------------------------
@@ -910,6 +915,220 @@ static void DrawOptionsDialog()
910// --------------------------------------------------------------------------- 915// ---------------------------------------------------------------------------
911// Wallet safety dialog 916// Wallet safety dialog
912// --------------------------------------------------------------------------- 917// ---------------------------------------------------------------------------
918// ---------------------------------------------------------------------------
919// Recovery phrase
920//
921// The phrase is shown once, and the seed is installed only after the user says
922// they have written it down. Generating it and storing it in the same breath
923// would hand somebody a wallet whose only backup is twelve words they may have
924// closed the window on -- which is worse than no phrase at all, because they
925// would believe they had one.
926//
927// Nothing here writes the words to disk or to debug.log. They live in one
928// std::string that is cleared the moment the dialog closes.
929// ---------------------------------------------------------------------------
930static bool g_showCreatePhrase = false;
931static bool g_showRestorePhrase = false;
932static std::string g_pendingMnemonic;
933static bool g_phraseWrittenDown = false;
934static std::string g_phraseStatus;
935
936static char g_restoreInput[600] = {};
937static std::string g_restoreStatus;
938static std::atomic<bool> g_restoreRunning(false);
939static std::atomic<int> g_restoreDerived(0);
940static std::atomic<int> g_restoreRecovered(0);
941static std::string g_restoreResult;
942static std::mutex g_restoreResultMutex;
943
944static void RestoreProgress(void*, int nDerived, int nRecovered)
945{
946 g_restoreDerived.store(nDerived);
947 g_restoreRecovered.store(nRecovered);
948}
949
950static void RestoreThread(std::string strPhrase)
951{
952 std::string strError;
953 int nRecovered = 0, nDerived = 0;
954 bool fOk = RestoreFromPhrase(strPhrase, 0, RestoreProgress, NULL,
955 strError, nRecovered, nDerived);
956 {
957 std::lock_guard<std::mutex> lock(g_restoreResultMutex);
958 if (!fOk)
959 g_restoreResult = "Nothing was changed. " + strError;
960 else if (nRecovered > 0)
961 g_restoreResult = strprintf(
962 "Restored %d transaction(s) across %d derived addresses. "
963 "Restart the node to see the balance.", nRecovered, nDerived);
964 else
965 g_restoreResult = strprintf(
966 "No transactions found for the first %d addresses of that phrase. "
967 "Check the words and their order.", nDerived);
968 }
969 g_restoreRunning.store(false);
970 g_needRefresh = true;
971}
972
973static void DrawCreatePhraseDialog()
974{
975 if (!g_showCreatePhrase) return;
976
977 ImGui::SetNextWindowSize(ImVec2(640.0f, 340.0f), ImGuiCond_Always);
978 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
979 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
980 if (ImGui::Begin("Recovery Phrase", &g_showCreatePhrase,
981 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
982 {
983 ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
984 "Write these twelve words down, in order, on paper.");
985 ImGui::TextWrapped(
986 "They are the only thing that can rebuild this wallet if the file is lost. "
987 "Anyone who reads them can spend your coins. They are not stored anywhere, "
988 "and this is the only time they will be shown.");
989 ImGui::Spacing();
990
991 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.10f, 1.0f));
992 ImGui::BeginChild("##phrase", ImVec2(0.0f, 70.0f), true);
993 ImGui::PushTextWrapPos(0.0f);
994 ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f), "%s", g_pendingMnemonic.c_str());
995 ImGui::PopTextWrapPos();
996 ImGui::EndChild();
997 ImGui::PopStyleColor();
998
999 ImGui::Spacing();
1000 ImGui::TextWrapped(
1001 "Keys already in this wallet are NOT covered by the phrase -- they existed "
1002 "before it did. Keep your file backups as well.");
1003 ImGui::Spacing();
1004
1005 ImGui::Checkbox("I have written these words down", &g_phraseWrittenDown);
1006 ImGui::Spacing();
1007
1008 if (!g_phraseWrittenDown)
1009 ImGui::BeginDisabled();
1010 if (ImGui::Button("Finish", ImVec2(120.0f, 0.0f)))
1011 {
1012 std::string strError;
1013 if (SetHDSeedFromMnemonic(g_pendingMnemonic, strError))
1014 {
1015 TopUpKeyPool();
1016 g_phraseStatus = "Recovery phrase created. New addresses come from it.";
1017 }
1018 else
1019 {
1020 g_phraseStatus = "Could not install the seed: " + strError;
1021 }
1022 g_pendingMnemonic.clear();
1023 g_phraseWrittenDown = false;
1024 g_showCreatePhrase = false;
1025 g_needRefresh = true;
1026 }
1027 if (!g_phraseWrittenDown)
1028 ImGui::EndDisabled();
1029
1030 ImGui::SameLine();
1031 if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f)))
1032 {
1033 // Nothing was installed, so there is nothing to undo. The words go
1034 // away with the string.
1035 g_pendingMnemonic.clear();
1036 g_phraseWrittenDown = false;
1037 g_showCreatePhrase = false;
1038 g_phraseStatus = "Cancelled. No recovery phrase was created.";
1039 }
1040 }
1041 ImGui::End();
1042
1043 // Closing with the window X is the same as cancelling.
1044 if (!g_showCreatePhrase && !g_pendingMnemonic.empty())
1045 {
1046 g_pendingMnemonic.clear();
1047 g_phraseWrittenDown = false;
1048 g_phraseStatus = "Cancelled. No recovery phrase was created.";
1049 }
1050}
1051
1052static void DrawRestorePhraseDialog()
1053{
1054 if (!g_showRestorePhrase) return;
1055
1056 ImGui::SetNextWindowSize(ImVec2(640.0f, 330.0f), ImGuiCond_Always);
1057 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1058 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1059 if (ImGui::Begin("Restore From Phrase", &g_showRestorePhrase,
1060 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1061 {
1062 ImGui::TextWrapped(
1063 "Type the twelve words, in order, separated by spaces. The node will "
1064 "derive the addresses they describe and search the chain for their coins.");
1065 ImGui::Spacing();
1066
1067 if (HaveHDSeed())
1068 {
1069 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f));
1070 ImGui::TextWrapped(
1071 "This wallet already has a recovery phrase. Restoring replaces it: "
1072 "coins on addresses from the current phrase would no longer come back "
1073 "from the words you wrote down for it. Back up wallet.dat first.");
1074 ImGui::PopStyleColor();
1075 ImGui::Spacing();
1076 }
1077
1078 ImGui::SetNextItemWidth(-1.0f);
1079 ImGui::InputTextMultiline("##phrase-in", g_restoreInput, sizeof(g_restoreInput),
1080 ImVec2(0.0f, 60.0f));
1081
1082 ImGui::Spacing();
1083 bool fBusy = g_restoreRunning.load();
1084 if (fBusy)
1085 ImGui::BeginDisabled();
1086 if (ImGui::Button("Restore", ImVec2(140.0f, 0.0f)))
1087 {
1088 std::string strWhy;
1089 if (!CanScanWalletTransactions(strWhy))
1090 {
1091 std::lock_guard<std::mutex> lock(g_restoreResultMutex);
1092 g_restoreResult = strWhy;
1093 }
1094 else
1095 {
1096 {
1097 std::lock_guard<std::mutex> lock(g_restoreResultMutex);
1098 g_restoreResult.clear();
1099 }
1100 g_restoreDerived.store(0);
1101 g_restoreRecovered.store(0);
1102 g_restoreRunning.store(true);
1103 // On its own thread: a restore walks the whole chain once per
1104 // batch and would otherwise freeze the window for as long as it
1105 // takes.
1106 std::thread(RestoreThread, std::string(g_restoreInput)).detach();
1107 }
1108 }
1109 if (fBusy)
1110 ImGui::EndDisabled();
1111
1112 ImGui::SameLine();
1113 if (ImGui::Button("Close", ImVec2(100.0f, 0.0f)) && !fBusy)
1114 g_showRestorePhrase = false;
1115
1116 ImGui::Spacing();
1117 if (fBusy)
1118 {
1119 ImGui::Text("Searching: %d addresses checked, %d transaction(s) so far",
1120 g_restoreDerived.load(), g_restoreRecovered.load());
1121 }
1122 else
1123 {
1124 std::lock_guard<std::mutex> lock(g_restoreResultMutex);
1125 if (!g_restoreResult.empty())
1126 ImGui::TextWrapped("%s", g_restoreResult.c_str());
1127 }
1128 }
1129 ImGui::End();
1130}
1131
913static void DrawWalletSafetyDialog() 1132static void DrawWalletSafetyDialog()
914{ 1133{
915 if (!g_showWalletSafety) return; 1134 if (!g_showWalletSafety) return;
@@ -994,6 +1213,43 @@ static void DrawWalletSafetyDialog()
994 } 1213 }
995 1214
996 ImGui::Spacing(); 1215 ImGui::Spacing();
1216 ImGui::SeparatorText("Recovery Phrase");
1217 if (HaveHDSeed())
1218 ImGui::TextColored(ImVec4(0.55f, 1.0f, 0.6f, 1.0f),
1219 "This wallet has a recovery phrase.");
1220 else
1221 ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f),
1222 "No recovery phrase. Only a file backup can rebuild this wallet.");
1223
1224 if (!HaveHDSeed())
1225 {
1226 if (ImGui::Button("Create recovery phrase", ImVec2(200.0f, 0.0f)))
1227 {
1228 std::string strError;
1229 if (bitflash::BIP39GenerateMnemonic(g_pendingMnemonic, strError))
1230 {
1231 g_phraseWrittenDown = false;
1232 g_phraseStatus.clear();
1233 g_showCreatePhrase = true;
1234 }
1235 else
1236 {
1237 g_phraseStatus = "Could not generate a phrase: " + strError;
1238 }
1239 }
1240 ImGui::SameLine();
1241 }
1242 if (ImGui::Button("Restore from phrase", ImVec2(190.0f, 0.0f)))
1243 g_showRestorePhrase = true;
1244
1245 if (!g_phraseStatus.empty())
1246 ImGui::TextWrapped("%s", g_phraseStatus.c_str());
1247
1248 ImGui::TextWrapped(
1249 "A phrase covers addresses created after it. Keys that already existed "
1250 "are not derived from it, so keep the file backups above as well.");
1251
1252 ImGui::Spacing();
997 ImGui::SeparatorText("Key Pool"); 1253 ImGui::SeparatorText("Key Pool");
998 int nPool = KeyPoolCount(); 1254 int nPool = KeyPoolCount();
999 ImGui::Text("Pre-generated keys ready: %d / %d", nPool, KEYPOOL_SIZE); 1255 ImGui::Text("Pre-generated keys ready: %d / %d", nPool, KEYPOOL_SIZE);
@@ -1157,6 +1413,8 @@ int RunGUI(int argc, char* argv[])
1157 DrawSendDialog(); 1413 DrawSendDialog();
1158 DrawOptionsDialog(); 1414 DrawOptionsDialog();
1159 DrawWalletSafetyDialog(); 1415 DrawWalletSafetyDialog();
1416 DrawCreatePhraseDialog();
1417 DrawRestorePhraseDialog();
1160 DrawDiagnosticsDialog(); 1418 DrawDiagnosticsDialog();
1161 DrawAboutDialog(); 1419 DrawAboutDialog();
1162 DrawStatusBar(); 1420 DrawStatusBar();