fix(wallet): surface the real z_getnewaddress error; sweep coinbase in migrate-to-seed
From a legacy-wallet migration support case (mined T-funds, 3y-old Silent Dragon wallet.dat): the balance
shows but new-address and migrate both fail with unhelpful/silent results. Two fixes:
1) Real address-creation error. createNewAddress() swallowed the daemon's z_getnewaddress error and the UI
showed a generic "Failed to create shielded address". It now keeps the daemon's reason (friendly-mapped
via friendlyRpcError) in last_address_error_, which the Receive tab and request-payment dialog show
instead of the generic string — so a legacy/degraded wallet sees the actual "HD seed not found". And when
that specific error appears it flips wallet_degraded_ + warns once, even for an EXTERNAL daemon
(detectWalletDegraded() only scans the embedded daemon's log and would otherwise miss it).
2) Migrate-to-seed sweep skipped coinbase. beginSweepToSeedWallet()'s z_mergetoaddress ignores coinbase
UTXOs, so a miner's coinbase-only balance produced "Could not find any funds to merge", the legacy
balance never reached ~0, and the adopt gate stalled silently. The sweep now falls back to
z_shieldcoinbase("*", dest, fee, 50) to the SAME shielded destination when the merge reports no funds.
z_shieldcoinbase returns an opid just like the merge, so the single-opid adopt gate (opid -> txid ->
confirm -> balance ~0 -> adopt) is unchanged, and a batch limit / mixed wallet drains via the existing
"Sweep remaining" re-sweep.
Fund-path: the z_mergetoaddress call and its args are byte-identical; the fallback only ADDS a
z_shieldcoinbase to the same dest when merge finds nothing, and nothing is adopted until the swept funds
confirm and the legacy balance is ~0 (gate unchanged). Runtime-verified behavior (clean build + ctest; the
RPC paths are not pure-testable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -453,6 +453,9 @@ public:
|
|||||||
// Address operations
|
// Address operations
|
||||||
void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
|
void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
|
||||||
void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);
|
void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);
|
||||||
|
// The daemon's actual reason the last new-address RPC failed (friendly-mapped), or "" on success. Lets
|
||||||
|
// the UI show e.g. "HD seed not found" (legacy/degraded wallet) instead of a generic "couldn't create".
|
||||||
|
const std::string& lastAddressError() const { return last_address_error_; }
|
||||||
|
|
||||||
// Hide/unhide addresses from the address list (persisted in settings)
|
// Hide/unhide addresses from the address list (persisted in settings)
|
||||||
void hideAddress(const std::string& addr);
|
void hideAddress(const std::string& addr);
|
||||||
@@ -1350,6 +1353,7 @@ private:
|
|||||||
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||||||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||||
|
std::string last_address_error_; // daemon reason the last z_getnewaddress/getnewaddress failed ("" = ok)
|
||||||
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
||||||
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
||||||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||||
|
|||||||
+40
-3
@@ -3957,15 +3957,32 @@ void App::createNewAddress(bool shielded, std::function<void(const std::string&)
|
|||||||
const char* traceLabel = shielded ? "Receive tab / New shielded address"
|
const char* traceLabel = shielded ? "Receive tab / New shielded address"
|
||||||
: "Receive tab / New transparent address";
|
: "Receive tab / New transparent address";
|
||||||
worker_->post([this, callback, shielded, typeStr, rpcMethod, traceLabel]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, callback, shielded, typeStr, rpcMethod, traceLabel]() -> rpc::RPCWorker::MainCb {
|
||||||
std::string addr;
|
std::string addr, genErr;
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace(traceLabel);
|
rpc::RPCClient::TraceScope trace(traceLabel);
|
||||||
json result = rpc_->call(rpcMethod);
|
json result = rpc_->call(rpcMethod);
|
||||||
addr = result.get<std::string>();
|
addr = result.get<std::string>();
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("%s error: %s\n", rpcMethod, e.what());
|
DEBUG_LOGF("%s error: %s\n", rpcMethod, e.what());
|
||||||
|
genErr = e.what(); // keep the real reason so the UI can show it instead of a generic toast
|
||||||
|
}
|
||||||
|
return [this, callback, addr, genErr, shielded, typeStr]() {
|
||||||
|
if (addr.empty()) {
|
||||||
|
// Surface the daemon's actual reason (friendly-mapped) rather than a generic "couldn't create".
|
||||||
|
last_address_error_ = genErr.empty() ? std::string() : util::friendlyRpcError(genErr);
|
||||||
|
// "HD seed not found" means a legacy / degraded wallet can't derive new HD keys. Flip degraded
|
||||||
|
// mode here so the UI explains it even for an EXTERNAL daemon — detectWalletDegraded() only
|
||||||
|
// reads the embedded daemon's startup log and would otherwise miss this.
|
||||||
|
if (shielded && genErr.find("HD seed not found") != std::string::npos) {
|
||||||
|
wallet_degraded_ = true;
|
||||||
|
if (!wallet_degraded_warned_) {
|
||||||
|
wallet_degraded_warned_ = true;
|
||||||
|
ui::Notifications::instance().warning(TR("wallet_degraded_notify"), 30.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_address_error_.clear();
|
||||||
}
|
}
|
||||||
return [this, callback, addr, shielded, typeStr]() {
|
|
||||||
if (!addr.empty()) {
|
if (!addr.empty()) {
|
||||||
// Inject immediately so UI can select the address next frame
|
// Inject immediately so UI can select the address next frame
|
||||||
AddressInfo info;
|
AddressInfo info;
|
||||||
@@ -6489,7 +6506,27 @@ void App::beginSweepToSeedWallet()
|
|||||||
auto res = rpc_->call("z_mergetoaddress", {fromAddrs, dest, DRAGONX_DEFAULT_FEE, 0, 0});
|
auto res = rpc_->call("z_mergetoaddress", {fromAddrs, dest, DRAGONX_DEFAULT_FEE, 0, 0});
|
||||||
opid = res.value("opid", std::string());
|
opid = res.value("opid", std::string());
|
||||||
if (opid.empty()) err = "The daemon returned no operation id for the sweep.";
|
if (opid.empty()) err = "The daemon returned no operation id for the sweep.";
|
||||||
} catch (const std::exception& e) { err = e.what(); }
|
} catch (const std::exception& e) {
|
||||||
|
// z_mergetoaddress deliberately ignores coinbase UTXOs, so a miner whose legacy balance is all
|
||||||
|
// mined coinbase gets "Could not find any funds to merge". Fall back to z_shieldcoinbase, which
|
||||||
|
// DOES move coinbase, to the same shielded destination. It returns an opid exactly like the merge,
|
||||||
|
// so the adopt gate (one opid -> txid -> confirm -> balance ~0 -> adopt) is unchanged; a batch
|
||||||
|
// limit or a mixed coinbase+other wallet is drained by the existing "Sweep remaining" re-sweep.
|
||||||
|
const std::string mergeErr = e.what();
|
||||||
|
if (mergeErr.find("find any funds to merge") != std::string::npos) {
|
||||||
|
try {
|
||||||
|
// Fixed-decimal fee string (ParseFixedPoint rejects a double that serializes as "5e-05").
|
||||||
|
const std::string feeStr = util::formatAmountFixed(DRAGONX_DEFAULT_FEE);
|
||||||
|
auto sres = rpc_->call("z_shieldcoinbase", {std::string("*"), dest, feeStr, 50});
|
||||||
|
opid = sres.value("opid", std::string());
|
||||||
|
if (opid.empty()) err = "The daemon returned no operation id for the coinbase shield.";
|
||||||
|
} catch (const std::exception& e2) {
|
||||||
|
err = e2.what(); // e.g. "Could not find any coinbase funds to shield" = genuinely empty
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = mergeErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
return [this, opid, err]() {
|
return [this, opid, err]() {
|
||||||
if (!err.empty() || opid.empty()) {
|
if (!err.empty() || opid.empty()) {
|
||||||
// Submit failed — nothing was broadcast, so drop the "sweep in flight" marker.
|
// Submit failed — nothing was broadcast, so drop the "sweep in flight" marker.
|
||||||
|
|||||||
@@ -333,10 +333,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
} else {
|
} else {
|
||||||
s_generating_address = true;
|
s_generating_address = true;
|
||||||
if (s_addr_type_filter != 2) {
|
if (s_addr_type_filter != 2) {
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
app->createNewZAddress([app](const std::string& addr) {
|
||||||
s_generating_address = false;
|
s_generating_address = false;
|
||||||
if (addr.empty())
|
if (addr.empty())
|
||||||
Notifications::instance().error(TR("failed_create_shielded"));
|
Notifications::instance().error(app->lastAddressError().empty()
|
||||||
|
? std::string(TR("failed_create_shielded")) : app->lastAddressError());
|
||||||
else {
|
else {
|
||||||
s_pending_select_address = addr;
|
s_pending_select_address = addr;
|
||||||
Notifications::instance().success(TR("new_shielded_created"));
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
@@ -400,10 +401,11 @@ static void RenderMiningAddressNotice(App* app, const WalletState& state, float
|
|||||||
s_cached_qr_data.clear();
|
s_cached_qr_data.clear();
|
||||||
} else {
|
} else {
|
||||||
s_generating_address = true; // no shielded address yet: make one, as "New" does
|
s_generating_address = true; // no shielded address yet: make one, as "New" does
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
app->createNewZAddress([app](const std::string& addr) {
|
||||||
s_generating_address = false;
|
s_generating_address = false;
|
||||||
if (addr.empty())
|
if (addr.empty())
|
||||||
Notifications::instance().error(TR("failed_create_shielded"));
|
Notifications::instance().error(app->lastAddressError().empty()
|
||||||
|
? std::string(TR("failed_create_shielded")) : app->lastAddressError());
|
||||||
else {
|
else {
|
||||||
s_pending_select_address = addr;
|
s_pending_select_address = addr;
|
||||||
Notifications::instance().success(TR("new_shielded_created"));
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
|
|||||||
@@ -119,10 +119,11 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
if (material::TactileButton(TR("request_create_shielded"), ImVec2(0, 0), S.resolveFont(actionBtn.font))) {
|
if (material::TactileButton(TR("request_create_shielded"), ImVec2(0, 0), S.resolveFont(actionBtn.font))) {
|
||||||
if (app->isConnected()) {
|
if (app->isConnected()) {
|
||||||
s_creating_address = true;
|
s_creating_address = true;
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
app->createNewZAddress([app](const std::string& addr) {
|
||||||
s_creating_address = false;
|
s_creating_address = false;
|
||||||
if (addr.empty()) {
|
if (addr.empty()) {
|
||||||
Notifications::instance().error(TR("failed_create_shielded"));
|
Notifications::instance().error(app->lastAddressError().empty()
|
||||||
|
? std::string(TR("failed_create_shielded")) : app->lastAddressError());
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().success(TR("new_shielded_created"));
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
// Pre-select the new address if the dialog is still open and none chosen.
|
// Pre-select the new address if the dialog is still open and none chosen.
|
||||||
|
|||||||
Reference in New Issue
Block a user