From 828018de2b007cf9d32255eaf8b4afc1fc377104 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 17:29:10 -0500 Subject: [PATCH] feat(settings): sweep a private key's funds to your own wallet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Sweep to my wallet (don't keep the key)" option to the Import Private Key dialog (spending keys only). Rather than keeping the key, it imports the key, then sends ALL its funds to a destination you control — a freshly generated shielded address by default, or an existing wallet address you pick. Flow (App::sweepPrivateKey): import the key (rescan; the stock node has no address index, so its UTXOs/notes can't be enumerated otherwise) → confirm it holds spendable funds → resolve the destination → z_sendmany balance-minus-fee from the key's address to the destination, via the existing tested submitZSendMany wrapper + async-operation tracker. The imported key is left in the wallet with an empty balance (there is no remove-key RPC) — stated plainly in the UI caveat. Design notes forced by this daemon (verified against external/dragonx): - z_importkey returns null here, so the shielded source address is found by diffing z_listaddresses before/after import (aborts if the pre-snapshot fails, so a wrong address can never be picked). Transparent uses importprivkey's return. - z_sendmany, NOT z_mergetoaddress: moves a single-UTXO transparent source (the common paper-wallet case) and fails loudly instead of silently leaving a remainder. Amount computed in integer satoshis for an exact fixed-decimal string. - Clear errors for no-funds / unconfirmed / dust-below-fee / already-imported. - The mode + destination are locked during a running sweep; a sweep that finishes while the dialog is closed still shows its Done/Error+txid on reopen; sweep UI can't bleed into the watch-only viewing dialog. i18n: 8 new sweep_* keys (en + 8 langs); dynamic status strings are English, matching the beginSweepToSeedWallet precedent. Reviewed across three adversarial multi-agent rounds (fund-safety focus): round 1 caught the feature was broken for shielded keys (z_importkey null) and rejected single-UTXO transparent sources; round 2 caught reopen/double-submit/precision issues; round 3 verified the fixes. NOT yet exercised against a live daemon — needs a small-amount mainnet test before it's trusted, like migrate-to-seed. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 624308 -> 625236 bytes res/lang/de.json | 8 ++ res/lang/es.json | 8 ++ res/lang/fr.json | 8 ++ res/lang/ja.json | 8 ++ res/lang/ko.json | 8 ++ res/lang/pt.json | 8 ++ res/lang/ru.json | 8 ++ res/lang/zh.json | 8 ++ src/app.cpp | 146 ++++++++++++++++++++++++----- src/app.h | 18 +++- src/app_network.cpp | 155 +++++++++++++++++++++++++++++++ src/util/i18n.cpp | 9 ++ 13 files changed, 368 insertions(+), 24 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index b157a610790559989aae874fd27a39bdb3824b23..d9c0cfadea1b7c145b9d8b935984f8fd77b25e2b 100644 GIT binary patch delta 19509 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 0 && startHeight > state_.sync.blocks) startHeight = state_.sync.blocks; // never past the chain tip } - importPrivateKey(std::string(import_key_input_), startHeight, - [this](bool ok, const std::string& err, const std::string& addr) { - import_in_progress_ = false; - import_success_ = ok; - if (ok) { - import_result_address_ = addr; - import_status_.clear(); - sodium_memzero(import_key_input_, sizeof(import_key_input_)); // wipe on success - } else { - import_status_ = err; - } - }); + if (sweepMode) { + sweepPrivateKey(std::string(import_key_input_), startHeight, sweep_dest_mode_, + sweep_dest_mode_ == 1 ? std::string(sweep_dest_pick_) : std::string()); + } else { + import_in_progress_ = true; + import_success_ = false; + import_status_.clear(); + import_result_address_.clear(); + importPrivateKey(std::string(import_key_input_), startHeight, + [this](bool ok, const std::string& err, const std::string& addr) { + import_in_progress_ = false; + import_success_ = ok; + if (ok) { + import_result_address_ = addr; + import_status_.clear(); + sodium_memzero(import_key_input_, sizeof(import_key_input_)); // wipe on success + } else { + import_status_ = err; + } + }); + } } ImGui::EndDisabled(); ImGui::SameLine(); @@ -3138,12 +3227,23 @@ void App::renderImportKeyDialog() // Wipe the key the moment the dialog closes (Close button OR outside-click, which clears // show_import_key_ mid-frame). in_progress is left to the callback to clear. if (!show_import_key_) { - sodium_memzero(import_key_input_, sizeof(import_key_input_)); + sodium_memzero(import_key_input_, sizeof(import_key_input_)); // key already copied into the op sodium_memzero(import_key_scan_height_, sizeof(import_key_scan_height_)); import_status_.clear(); import_result_address_.clear(); import_success_ = false; import_key_reveal_ = false; + // A running sweep is an async daemon op that keeps going in the background — preserve its UI + // state so reopening shows the running progress and, on completion, the Done/Error panel. + if (sweep_step_ != SweepStep::Running) { + import_sweep_mode_ = false; + sweep_dest_mode_ = 0; + sweep_dest_pick_[0] = '\0'; + sweep_status_.clear(); + sweep_txid_.clear(); + sweep_dest_shown_.clear(); + sweep_step_ = SweepStep::Idle; + } } m::EndOverlayDialog(); diff --git a/src/app.h b/src/app.h index a2745a8..47f8e15 100644 --- a/src/app.h +++ b/src/app.h @@ -305,7 +305,13 @@ public: // startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF). void importPrivateKey(const std::string& key, int startHeight, std::function callback); - + + // Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance − fee) to a + // destination you own — a freshly generated shielded address when destMode == 0, else destExisting. + // Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker. + void sweepPrivateKey(const std::string& key, int startHeight, int destMode, + const std::string& destExisting); + // Wallet backup void backupWallet(const std::string& destination, std::function callback); // Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives @@ -812,6 +818,16 @@ private: std::string import_result_address_; // address imported on success (shown as a copy field) bool import_view_mode_ = false; // dialog mode: false = spending key, true = viewing key char import_key_scan_height_[16] = {0}; // optional rescan start height (shielded spend / viewing-key imports) + // --- Sweep: import a spending key then move all its funds to one of your own addresses, instead + // of keeping the key in the wallet (spending-key / non-view mode only). --- + bool import_sweep_mode_ = false; // sweep instead of a plain import + int sweep_dest_mode_ = 0; // destination: 0 = fresh shielded address, 1 = an existing one + char sweep_dest_pick_[128] = {0}; // chosen existing destination (sweep_dest_mode_ == 1) + enum class SweepStep { Idle, Running, Done, Error }; + SweepStep sweep_step_ = SweepStep::Idle; + std::string sweep_status_; // progress / error text + std::string sweep_txid_; // sweep transaction id (on success) + std::string sweep_dest_shown_; // the destination address the funds were swept to std::string backup_status_; bool backup_success_ = false; diff --git a/src/app_network.cpp b/src/app_network.cpp index 756332c..e9e2b70 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -2983,6 +2983,161 @@ void App::importPrivateKey(const std::string& rawKey, int startHeight, }); } +// Sweep a spending key: import it (a full rescan populates its UTXOs/notes — the stock node has no +// address index, so there is no way to enumerate them without importing) then z_sendmany ALL of the +// key's funds (balance − fee) to a destination the user already controls. The imported key is left in +// the wallet with an empty balance (there is no remove-key RPC) — the point is that the funds now sit +// on the user's own key. z_sendmany (not z_mergetoaddress): it moves a single-UTXO transparent source +// and fails loudly rather than silently leaving a remainder. Fund-moving — see the reviewed design. +void App::sweepPrivateKey(const std::string& rawKey, int startHeight, int destMode, + const std::string& destExisting) +{ + if (!state_.connected || !rpc_ || !worker_) { + sweep_status_ = "Not connected to the daemon."; + sweep_step_ = SweepStep::Error; + return; + } + // Trim (manual entry doesn't go through the dialog's Paste trimmer). + std::string key(rawKey); + while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin()); + while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back(); + + // Sweeping requires a SPENDING key (transparent WIF or shielded z-spending key) — a viewing key + // can't sign, and this must never accidentally route to z_importviewingkey. + if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) { + sweep_status_ = "Enter a spending key (a viewing key can't move funds)."; + sweep_step_ = SweepStep::Error; + return; + } + + sweep_step_ = SweepStep::Running; + sweep_status_ = "Importing key & rescanning…"; + sweep_txid_.clear(); + sweep_dest_shown_.clear(); + + const bool shielded = services::WalletSecurityController::classifyPrivateKey(key) + == services::WalletSecurityController::KeyKind::Shielded; + const double fee = DRAGONX_DEFAULT_FEE; + worker_->post([this, key, startHeight, destMode, destExisting, shielded, fee]() -> rpc::RPCWorker::MainCb { + std::string err, dest, sourceAddr, amountStr; + double amount = 0.0; + try { + rpc::RPCClient::TraceScope trace("Settings / Sweep key"); + + // For a shielded key this daemon's z_importkey returns null (no address), so snapshot the + // z-address set before the import and diff it after to find the newly-controlled address. + std::vector preZ; + bool preZok = false; + auto listZ = [&](std::vector& out) { + out.clear(); + auto la = rpc_->call("z_listaddresses"); + if (la.is_array()) for (auto& a : la) if (a.is_string()) out.push_back(a.get()); + }; + if (shielded) { try { listZ(preZ); preZok = true; } catch (...) { preZok = false; } } + + // 1. Import the key (blocks until the rescan completes → its funds become spendable). + std::string method; + nlohmann::json params; + if (shielded) { method = "z_importkey"; params = {key, "yes"}; } + else { method = "importprivkey"; params = {key, "", true}; } + if (startHeight > 0 && shielded) params.push_back(startHeight); + nlohmann::json r = rpc_->call(method, params); + + // 2. Determine the swept address. importprivkey returns the t-address string; z_importkey + // returns null, so diff the z-address list to find the one the key just added. + if (shielded) { + // A failed pre-snapshot would make the diff treat pre-existing addresses as "new" and + // could pick the WRONG source — refuse rather than risk moving another address's funds. + if (!preZok) throw std::runtime_error("Couldn't read the wallet's addresses to identify the swept key. Try again."); + std::vector postZ; + listZ(postZ); + std::vector added; + for (const auto& s : postZ) + if (std::find(preZ.begin(), preZ.end(), s) == preZ.end()) added.push_back(s); + if (added.size() == 1) sourceAddr = added[0]; + else if (added.empty()) throw std::runtime_error("__ALREADY__"); // key already in wallet + else throw std::runtime_error("Could not identify the key's address after import."); + } else { + if (r.is_string()) sourceAddr = r.get(); + else if (r.is_object() && r.contains("address") && r["address"].is_string()) + sourceAddr = r["address"].get(); + } + if (sourceAddr.empty()) throw std::runtime_error("Could not determine the key's address."); + + // 3. Confirm the key holds spendable (confirmed) funds. z_getbalance covers t- and z-addrs. + auto balAt = [&](int minconf) -> double { + try { + nlohmann::json b = rpc_->call("z_getbalance", {sourceAddr, minconf}); + return b.is_number() ? b.get() + : b.is_string() ? std::stod(b.get()) : 0.0; + } catch (...) { return 0.0; } + }; + double bal = balAt(1); + if (bal <= 0.0) { + throw std::runtime_error(balAt(0) > 0.0 ? "__UNCONFIRMED__" : "__NOFUNDS__"); + } + if (bal <= fee) throw std::runtime_error("__DUST__"); + + // 4. Resolve the destination — a fresh shielded address by default, else the picked one. + if (destMode == 0) dest = rpc_->call("z_getnewaddress").get(); + else dest = destExisting; + if (dest.empty()) throw std::runtime_error("No destination address for the sweep."); + + // Send everything; the fee consumes the remainder (no change/residue). Compute in integer + // satoshis so the fixed-decimal amount is exact for all realistic balances (double bal-fee + // is exact below 2^52 sat ≈ 45M DRGX; above that the JSON-parsed balance is itself lossy — + // a swept key that large is implausible and would fail safe with funds left in place). + const long long feeSats = (long long)std::llround(fee * 100000000.0); + const long long amtSats = (long long)std::llround(bal * 100000000.0) - feeSats; + if (amtSats <= 0) throw std::runtime_error("__DUST__"); + char amtBuf[32]; + snprintf(amtBuf, sizeof(amtBuf), "%lld.%08lld", + amtSats / 100000000LL, amtSats % 100000000LL); + amountStr = amtBuf; + amount = (double)amtSats / 100000000.0; // for send bookkeeping / display only + } catch (const std::exception& e) { + err = e.what(); + } catch (...) { + err = "Sweep failed (unknown error)"; + } + return [this, err, sourceAddr, dest, amount, amountStr, fee]() { + invalidateAddressValidationCache(); + refreshAddresses(); + if (!err.empty()) { + if (err == "__NOFUNDS__") sweep_status_ = "This key holds no funds to sweep."; + else if (err == "__UNCONFIRMED__") sweep_status_ = "This key's funds are still unconfirmed — try again shortly."; + else if (err == "__ALREADY__") sweep_status_ = "This key is already in your wallet — use Send to move its funds."; + else if (err == "__DUST__") sweep_status_ = "This key's balance is too small to cover the network fee."; + else sweep_status_ = err; + sweep_step_ = SweepStep::Error; + return; + } + sweep_dest_shown_ = dest; + sweep_status_ = "Sweeping funds to your address…"; + // Send all funds from the swept address to the destination through the tested z_sendmany + // wrapper (fixed-decimal amount + async-op tracking). z_sendmany needs every input in one + // transaction to cover the full balance, so it fails loudly rather than partial-sweeping. + nlohmann::json recipients = nlohmann::json::array(); + nlohmann::json rcp; + rcp["address"] = dest; + rcp["amount"] = amountStr; // exact integer-satoshi fixed-decimal string + recipients.push_back(rcp); + submitZSendMany(sourceAddr, dest, amount, fee, "", recipients, "Settings / Sweep key", + /*markFeeGapRetry*/ false, [this](bool ok, const std::string& result) { + if (ok) { + sweep_txid_ = result; + sweep_status_.clear(); + sweep_step_ = SweepStep::Done; + } else { + sweep_status_ = result.empty() ? "The sweep transaction failed." : result; + sweep_step_ = SweepStep::Error; + } + refreshBalance(); + }); + }; + }); +} + void App::exportSeedPhrase(std::function callback) { if (!state_.connected || !rpc_ || !worker_) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 398de9a..af2e00b 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1371,6 +1371,15 @@ void I18n::loadBuiltinEnglish() strings_["import_scan_transparent"] = "Transparent keys always rescan fully"; strings_["import_scan_tip"] = "current height"; strings_["paste_clip_empty"] = "Clipboard is empty"; + // Sweep (import a spending key, then move all its funds to your own address). + strings_["sweep_toggle"] = "Sweep to my wallet (don't keep the key)"; + strings_["sweep_caveat"] = "Imports the key to sign one transaction moving all its funds to your address. The key stays in your wallet with an empty balance."; + strings_["sweep_dest_label"] = "Send swept funds to"; + strings_["sweep_dest_new"] = "New shielded address (recommended)"; + strings_["sweep_button"] = "Sweep"; + strings_["sweep_done"] = "Done \xE2\x80\x94 funds swept to your address."; + strings_["sweep_to"] = "Swept to:"; + strings_["sweep_tx"] = "Transaction:"; // --- Key Export Dialog --- strings_["key_export_fetching"] = "Fetching key from wallet...";