upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Makefile4
-rw-r--r--src/gui.cpp258
-rw-r--r--src/main_gui.cpp38
-rw-r--r--src/makefile.mingw4
-rw-r--r--src/selftest.cpp30
-rw-r--r--src/util.h27
-rw-r--r--src/walletcmd.cpp237
-rw-r--r--src/walletcmd.h43
8 files changed, 610 insertions, 31 deletions
diff --git a/src/Makefile b/src/Makefile
index bbdc9e2..3eaf2ba 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -42,7 +42,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
42HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 42HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
43 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 43 key.h bignum.h script.h db.h base58.h net.h nostr.h \
44 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ 44 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \
45 selftest.h compat.h 45 selftest.h walletcmd.h compat.h
46 46
47IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 47IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
48 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 48 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
@@ -50,7 +50,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
50NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 50NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
51 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ 51 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \
52 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 52 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
53 obj/selftest.o obj/bip32.o 53 obj/selftest.o obj/bip32.o obj/walletcmd.o
54 54
55GUI_OBJS = obj/gui.o obj/main_gui.o 55GUI_OBJS = obj/gui.o obj/main_gui.o
56 56
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();
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index aa9c23d..c9e7dee 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -2,6 +2,7 @@
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include "selftest.h" 4#include "selftest.h"
5#include "walletcmd.h"
5#include <thread> // hardware_concurrency, to sanity-check /genproclimit 6#include <thread> // hardware_concurrency, to sanity-check /genproclimit
6#ifndef _WIN32 7#ifndef _WIN32
7#include <csignal> 8#include <csignal>
@@ -78,6 +79,11 @@ static void PrintUsage()
78 printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); 79 printf(" /dumpwallet=FILE (export private keys as text -- readable by\n");
79 printf(" anyone, so guard it like cash)\n"); 80 printf(" anyone, so guard it like cash)\n");
80 printf(" /importwallet=FILE (load keys from such a file back in)\n"); 81 printf(" /importwallet=FILE (load keys from such a file back in)\n");
82 printf(" /newaddress (print the next receiving address, then exit)\n");
83 printf(" /newphrase (create a twelve-word recovery phrase for a\n");
84 printf(" wallet that has none, show it once, exit)\n");
85 printf(" /restorephrase=\"WORDS\" (rebuild this wallet from a phrase and scan\n");
86 printf(" the chain for its coins, then exit)\n");
81 printf(" /rescan (walk the chain for coins this wallet owns\n"); 87 printf(" /rescan (walk the chain for coins this wallet owns\n");
82 printf(" but never recorded, then exit)\n"); 88 printf(" but never recorded, then exit)\n");
83 printf("\n"); 89 printf("\n");
@@ -347,6 +353,38 @@ int main(int argc, char* argv[])
347 // /rescan -- walk the chain and pick up anything the wallet's keys own but 353 // /rescan -- walk the chain and pick up anything the wallet's keys own but
348 // the wallet never recorded. Cheap to offer and the only recourse when a 354 // the wallet never recorded. Cheap to offer and the only recourse when a
349 // balance is wrong for this reason. 355 // balance is wrong for this reason.
356 string strShow = argval2(argc, argv, "/showderived", "-showderived");
357 if (!strShow.empty())
358 {
359 int nRet = CmdShowDerived(atoi(strShow.c_str()));
360 DBFlush(true);
361 return nRet;
362 }
363
364 if (arg(argc,argv,"/newaddress") || arg(argc,argv,"-newaddress"))
365 {
366 int nRet = CmdNewAddress();
367 DBFlush(true);
368 return nRet;
369 }
370
371 if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase"))
372 {
373 int nRet = CmdNewPhrase();
374 DBFlush(true);
375 return nRet;
376 }
377
378 string strRestore = argval2(argc, argv, "/restorephrase", "-restorephrase");
379 if (!strRestore.empty())
380 {
381 string strDepth = argval2(argc, argv, "/restoredepth", "-restoredepth");
382 int nMinDepth = strDepth.empty() ? 0 : atoi(strDepth.c_str());
383 int nRet = CmdRestorePhrase(strRestore, nMinDepth);
384 DBFlush(true);
385 return nRet;
386 }
387
350 if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan")) 388 if (arg(argc,argv,"/rescan") || arg(argc,argv,"-rescan"))
351 { 389 {
352 string strScanError; 390 string strScanError;
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 07ae034..94d2146 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -42,14 +42,14 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
42NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 42NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
43 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ 43 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \
44 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 44 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
45 obj/selftest.o obj/bip32.o 45 obj/selftest.o obj/bip32.o obj/walletcmd.o
46 46
47GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 47GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
48 48
49HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 49HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
50 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 50 key.h bignum.h script.h db.h base58.h net.h nostr.h \
51 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ 51 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \
52 selftest.h 52 selftest.h walletcmd.h
53 53
54all: bitflash.exe 54all: bitflash.exe
55 55
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 6792ae7..b733c60 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -286,33 +286,9 @@ static int RunWalletKeyPoolSelfTest()
286 return nFail == 0 ? 0 : 1; 286 return nFail == 0 ? 0 : 1;
287} 287}
288 288
289// A -mwindows binary starts with no console attached, so stdout goes nowhere 289// AttachTerminal() moved to util.h: the wallet phrase commands need it too,
290// even when it was launched from a terminal. Borrow the caller's console for 290// and two copies of a console-attach that must get its one condition right is
291// the duration of the self-test. Nothing to do on Linux, where stdout is 291// one copy too many.
292// already connected.
293static void AttachTerminal()
294{
295#ifdef _WIN32
296 // Only when there is nowhere for stdout to go. If the caller redirected it
297 // to a file or a pipe -- which is what `make tests > log` and any CI does --
298 // that handle is inherited and already works, and reopening it on CONOUT$
299 // would take the output away from the file and put it on the screen. That
300 // was the first version of this fix, and it broke the one case that
301 // mattered.
302 HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
303 if (hOut != NULL && hOut != INVALID_HANDLE_VALUE)
304 return;
305
306 if (AttachConsole(ATTACH_PARENT_PROCESS))
307 {
308 // Return values ignored on purpose: if the reopen fails there is
309 // nowhere left to report it, and the exit status still carries the
310 // result.
311 freopen("CONOUT$", "w", stdout);
312 freopen("CONOUT$", "w", stderr);
313 }
314#endif
315}
316 292
317static int RunWalletHDSelfTest() 293static int RunWalletHDSelfTest()
318{ 294{
diff --git a/src/util.h b/src/util.h
index 25ef118..d95d3ae 100644
--- a/src/util.h
+++ b/src/util.h
@@ -293,6 +293,33 @@ void PrintHex(const T pbegin, const T pend, const char* pszFormat="%s", bool fSp
293// Forward declaration -- defined in main.cpp 293// Forward declaration -- defined in main.cpp
294string GetAppDir(); 294string GetAppDir();
295 295
296// Give a command-line operation somewhere to print.
297//
298// The Windows binary is linked -mwindows and starts with no console, so stdout
299// goes nowhere even when it was launched from a terminal. Commands that report
300// to the person who typed them -- the self-tests, the wallet phrase commands --
301// call this first.
302//
303// Only when there is nowhere for stdout to go already: if the caller redirected
304// it to a file or a pipe, that handle is inherited and works, and reopening it
305// on CONOUT$ would take the output away from the file and put it on the screen.
306inline void AttachTerminal()
307{
308#ifdef _WIN32
309 HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
310 if (hOut != NULL && hOut != INVALID_HANDLE_VALUE)
311 return;
312 if (AttachConsole(ATTACH_PARENT_PROCESS))
313 {
314 // Return values ignored on purpose: if the reopen fails there is
315 // nowhere left to report it, and the exit status still carries the
316 // result.
317 freopen("CONOUT$", "w", stdout);
318 freopen("CONOUT$", "w", stderr);
319 }
320#endif
321}
322
296inline int OutputDebugStringF(const char* pszFormat, ...) BF_FORMAT(1, 2); 323inline int OutputDebugStringF(const char* pszFormat, ...) BF_FORMAT(1, 2);
297 324
298inline int OutputDebugStringF(const char* pszFormat, ...) 325inline int OutputDebugStringF(const char* pszFormat, ...)
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
new file mode 100644
index 0000000..94e9d72
--- /dev/null
+++ b/src/walletcmd.cpp
@@ -0,0 +1,237 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Recovery-phrase commands, from the command line.
5//
6// The phrase reaches the person who typed the command and nowhere else. In this
7// tree printf is OutputDebugStringF, which writes into the data directory's
8// debug.log -- the file we routinely ask users to attach to issues. A wallet's
9// twelve words must never go there, so this file takes the real printf and
10// AttachTerminal() gives it somewhere to land on Windows.
11
12#include "headers_core.h"
13#include "bip32.h"
14#include "walletcmd.h"
15
16#undef printf
17
18// How far ahead to look for used addresses when restoring.
19//
20// A wallet is restored from a phrase alone, so the node has no idea how many
21// addresses the old one handed out. It derives forward in batches and stops
22// once a whole batch turns up unused. A fixed number would be simpler and wrong
23// for exactly this network: a miner burns an address per block, so a node that
24// mined for a week is thousands of keys deep, and stopping at a few hundred
25// would restore a wallet that looks empty.
26static const int RESTORE_BATCH = 100;
27static const int RESTORE_MAX = 10000;
28
29int CmdNewPhrase()
30{
31 AttachTerminal();
32
33 if (HaveHDSeed())
34 {
35 printf("This wallet already has a recovery phrase.\n");
36 printf("Creating another would replace it, and coins on addresses derived from\n");
37 printf("the current one would no longer come back from the phrase you write down.\n");
38 printf("Nothing was changed.\n");
39 return 1;
40 }
41
42 std::string strMnemonic, strError;
43 if (!bitflash::BIP39GenerateMnemonic(strMnemonic, strError))
44 {
45 fprintf(stderr, "Could not generate a phrase: %s\n", strError.c_str());
46 return 1;
47 }
48
49 if (!SetHDSeedFromMnemonic(strMnemonic, strError))
50 {
51 fprintf(stderr, "Could not install the seed: %s\n", strError.c_str());
52 return 1;
53 }
54
55 // Fill the pool from the new seed straight away, so the addresses this
56 // wallet hands out next are ones the phrase can reproduce.
57 TopUpKeyPool();
58
59 printf("\n");
60 printf("Write these twelve words down, in order, on paper.\n");
61 printf("They are the only thing that can rebuild this wallet if the file is lost.\n");
62 printf("Anyone who reads them can spend your coins. They are not stored anywhere,\n");
63 printf("and this is the only time they will be shown.\n");
64 printf("\n");
65 printf(" %s\n", strMnemonic.c_str());
66 printf("\n");
67 printf("Keys already in this wallet are NOT covered by the phrase -- they existed\n");
68 printf("before it did. Keep your file backups as well.\n");
69 fflush(stdout);
70 return 0;
71}
72
73bool RestoreFromPhrase(const std::string& strMnemonic,
74 int nMinDepth,
75 RestoreProgressFn fnProgress,
76 void* pArg,
77 std::string& strErrorRet,
78 int& nRecoveredRet,
79 int& nDerivedRet)
80{
81 strErrorRet.clear();
82 nRecoveredRet = 0;
83 nDerivedRet = 0;
84
85 // Refuse before touching the wallet, not after.
86 //
87 // Without this, a restore on a node that has not synced derives the right
88 // keys, scans a chain that is not there, and reports "no transactions were
89 // found" -- which reads as "your coins are gone" to the one person least
90 // able to argue with it. Measured while testing: a copied data directory
91 // whose Berkeley DB environment failed to load left the node at height 0,
92 // the scan walked one block, and the message said nothing was found across
93 // 600 addresses.
94 if (!CanScanWalletTransactions(strErrorRet))
95 return false;
96
97 if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet))
98 return false;
99
100 // Derive forward in batches, scanning after each, until a whole batch turns
101 // up nothing. Every derived key is written to the wallet before the scan,
102 // because the scan asks the wallet what belongs to it.
103 int nTotalDerived = 0;
104 while (nTotalDerived < RESTORE_MAX)
105 {
106 // Count wallet transactions, not scan hits.
107 //
108 // ScanForWalletTransactions reports what it added *or updated*, and it
109 // walks the whole chain every time, so a transaction found in the first
110 // batch is reported again by every batch after it. Testing this against
111 // a real payment produced exactly +1 per batch and a loop that ran to
112 // the ceiling, deriving ten thousand addresses to rediscover one coin.
113 size_t nWalletBefore = 0;
114 CRITICAL_BLOCK(cs_mapWallet)
115 nWalletBefore = mapWallet.size();
116
117 std::string strDeriveError;
118 for (int i = 0; i < RESTORE_BATCH; i++)
119 {
120 CKey key;
121 if (!DeriveHDKey(nHDNext, key, strDeriveError))
122 break;
123 if (!AddKey(key))
124 break;
125 nHDNext++;
126 nTotalDerived++;
127 }
128 CWalletDB().WriteHDNext(nHDNext);
129
130 ScanForWalletTransactions(pindexGenesisBlock);
131
132 size_t nWalletAfter = 0;
133 CRITICAL_BLOCK(cs_mapWallet)
134 nWalletAfter = mapWallet.size();
135
136 if (fnProgress)
137 fnProgress(pArg, nTotalDerived, (int)nWalletAfter);
138
139 // A whole batch with nothing in it means far enough -- unless the
140 // caller asked to look deeper anyway.
141 if (nWalletAfter == nWalletBefore && nTotalDerived >= nMinDepth)
142 break;
143 }
144
145 // The pool is rebuilt from where derivation stopped, so the next address
146 // this wallet hands out is one the phrase can reproduce as well.
147 CRITICAL_BLOCK(cs_keyPool)
148 mapKeyPool.clear();
149 TopUpKeyPool();
150
151 CRITICAL_BLOCK(cs_mapWallet)
152 nRecoveredRet = (int)mapWallet.size();
153 nDerivedRet = nTotalDerived;
154 return true;
155}
156
157static void PrintRestoreProgress(void*, int nDerived, int nRecovered)
158{
159 printf(" %d addresses checked, %d transaction(s) recovered\n", nDerived, nRecovered);
160 fflush(stdout);
161}
162
163int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth)
164{
165 AttachTerminal();
166
167 printf("Checking the phrase and the chain.\n");
168 fflush(stdout);
169
170 std::string strError;
171 int nRecovered = 0, nDerived = 0;
172 if (!RestoreFromPhrase(strMnemonic, nMinDepth, PrintRestoreProgress, NULL,
173 strError, nRecovered, nDerived))
174 {
175 fprintf(stderr, "Cannot restore: %s\n", strError.c_str());
176 fprintf(stderr, "Nothing was changed.\n");
177 return 1;
178 }
179
180 printf("\n");
181 if (nRecovered > 0)
182 printf("Restored: %d transaction(s) across %d derived addresses.\n",
183 nRecovered, nDerived);
184 else
185 printf("No transactions were found for the first %d addresses of that phrase.\n"
186 "If you expected coins, check the words and the order.\n", nDerived);
187 printf("Start the node normally to see the balance.\n");
188 fflush(stdout);
189 return 0;
190}
191
192int CmdNewAddress()
193{
194 AttachTerminal();
195
196 std::vector<unsigned char> vchPubKey = GetKeyFromPool();
197 if (vchPubKey.empty())
198 {
199 fprintf(stderr, "Could not get a key from the wallet.\n");
200 return 1;
201 }
202
203 printf("%s\n", PubKeyToAddress(vchPubKey).c_str());
204 if (!HaveHDSeed())
205 printf("(this wallet has no recovery phrase, so only a file backup covers this address)\n");
206 fflush(stdout);
207 return 0;
208}
209
210// Diagnostic: print the addresses a phrase produces, so "the restore found
211// nothing" can be told apart from "that address was never derived".
212int CmdShowDerived(int nCount)
213{
214 AttachTerminal();
215
216 if (!HaveHDSeed())
217 {
218 fprintf(stderr, "This wallet has no recovery phrase installed.\n");
219 return 1;
220 }
221 if (nCount <= 0)
222 nCount = 20;
223
224 std::string strError;
225 for (int i = 0; i < nCount; i++)
226 {
227 CKey key;
228 if (!DeriveHDKey((unsigned int)i, key, strError))
229 {
230 fprintf(stderr, "derivation failed at %d: %s\n", i, strError.c_str());
231 return 1;
232 }
233 printf("%6d %s\n", i, PubKeyToAddress(key.GetPubKey()).c_str());
234 }
235 fflush(stdout);
236 return 0;
237}
diff --git a/src/walletcmd.h b/src/walletcmd.h
new file mode 100644
index 0000000..89834d8
--- /dev/null
+++ b/src/walletcmd.h
@@ -0,0 +1,43 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Recovery-phrase commands. Both run against a loaded wallet and then exit;
5// neither starts the node.
6
7#ifndef BITFLASH_WALLETCMD_H
8#define BITFLASH_WALLETCMD_H
9
10#include <string>
11
12// Create a phrase for a wallet that does not have one, install the seed, and
13// show the words once. Refuses if a phrase already exists. Returns 0 on success.
14int CmdNewPhrase();
15
16// Install the seed a phrase describes, derive forward looking for used
17// addresses, and rescan the chain so their coins appear. Returns 0 on success.
18int CmdRestorePhrase(const std::string& strMnemonic, int nMinDepth = 0);
19
20// Called once per batch with how many addresses have been derived so far and
21// how many transactions the wallet now holds.
22typedef void (*RestoreProgressFn)(void* pArg, int nDerived, int nRecovered);
23
24// The restore itself, shared by the command and the GUI so the two cannot
25// drift apart. Returns false without touching the wallet if the chain is not
26// loaded or the phrase is not valid.
27bool RestoreFromPhrase(const std::string& strMnemonic,
28 int nMinDepth,
29 RestoreProgressFn fnProgress,
30 void* pArg,
31 std::string& strErrorRet,
32 int& nRecoveredRet,
33 int& nDerivedRet);
34
35// Take the next address from the key pool and print it. With a recovery phrase
36// installed the address is derived, so the phrase can bring back whatever is
37// paid to it.
38int CmdNewAddress();
39
40// Diagnostic: print the first nCount addresses this wallet's phrase derives.
41int CmdShowDerived(int nCount);
42
43#endif