// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "note_consolidation.h" #include #include #include namespace dragonx { namespace services { std::vector groupSpendableNotes(const nlohmann::json& zListUnspent) { std::map byAddress; if (zListUnspent.is_array()) { for (const auto& n : zListUnspent) { if (!n.is_object()) continue; const std::string address = n.value("address", std::string()); if (address.empty()) continue; if (n.value("confirmations", 0) < 1) continue; // z_mergetoaddress spends confirmed notes only if (!n.value("spendable", true)) continue; // watch-only: no key to spend it if (noteIsLocked(n)) continue; // held by an operation in flight NoteGroup& g = byAddress[address]; g.address = address; ++g.notes; g.amount += n.value("amount", 0.0); } } std::vector out; out.reserve(byAddress.size()); for (auto& kv : byAddress) out.push_back(std::move(kv.second)); std::sort(out.begin(), out.end(), [](const NoteGroup& a, const NoteGroup& b) { if (a.notes != b.notes) return a.notes > b.notes; return a.address < b.address; }); return out; } bool noteIsLocked(const nlohmann::json& zListUnspentEntry) { if (!zListUnspentEntry.is_object()) return false; const auto it = zListUnspentEntry.find("locked"); return it != zListUnspentEntry.end() && it->is_boolean() && it->get(); } bool worthConsolidating(const NoteGroup& group, double fee) { // The daemon rejects a merge whose fee is more than what it nets (merged value < 2 x fee). return group.notes >= 2 && group.amount > 2.0 * fee; } int estimateBatches(int notes, int limit) { if (notes < 2) return 0; if (limit < 2) limit = 2; return notes / limit + ((notes % limit) >= 2 ? 1 : 0); } AddressRun consolidateInPlace(const std::string& address, const MergeCall& call, int maxBatches) { AddressRun run; run.address = address; for (int b = 0; b < maxBatches; ++b) { MergeCallResult r = call(address); if (!r.error.empty()) { run.error = r.error; // earlier batches (if any) are already submitted and stand break; } run.opids.push_back(r.opid); run.notesMerged += r.merging; run.remaining = r.remaining; if (r.remaining < 2) break; // one leftover note can't be merged into its own address } return run; } ScopeWant scopeScanWant(const ScopeScan& scan, const ScopeInputs& in) { if (!in.mergeMode || scan.phase == ScopePhase::Loaded) return ScopeWant::Nothing; if (scan.inFlight || in.busyWithOp) return ScopeWant::Nothing; if (scan.phase == ScopePhase::Failed && !scan.autoRetry) return ScopeWant::Nothing; // waits for Retry if (in.now < scan.retryAt) return ScopeWant::WaitBackoff; if (!in.nodeReady) return ScopeWant::WaitNode; return ScopeWant::Post; } std::uint32_t scopeScanBegin(ScopeScan& scan, double now) { scan.phase = ScopePhase::Loading; scan.inFlight = true; scan.inFlightGen = scan.gen; scan.startedAt = now; return scan.gen; } bool scopeScanFinish(ScopeScan& scan, std::uint32_t gen, const std::string& error, rpc::RpcFailureKind kind, double now) { if (scan.inFlight && gen == scan.inFlightGen) scan.inFlight = false; if (gen != scan.gen) return false; // a scan from an earlier session: its data is not this one's if (error.empty()) { scan.phase = ScopePhase::Loaded; scan.failures = 0; scan.autoRetry = false; scan.retryAt = 0.0; scan.error.clear(); return true; } scan.phase = ScopePhase::Failed; ++scan.failures; scan.error = error; scan.autoRetry = scopeFailureRetries(kind, error) && scan.failures <= kScopeMaxAutoRetries; scan.retryAt = scan.autoRetry ? now + scopeRetryDelaySec(scan.failures) : 0.0; return false; } void scopeScanOpen(ScopeScan& scan, double now) { if (scan.inFlight && now - scan.startedAt > kScopeInFlightMaxSec) scan.inFlight = false; // lost if (scan.inFlight && scan.phase == ScopePhase::Loading) return; // still loading for the last session ++scan.gen; scan.phase = ScopePhase::Idle; scan.failures = 0; scan.autoRetry = false; scan.retryAt = 0.0; scan.error.clear(); } void scopeScanInvalidate(ScopeScan& scan) { ++scan.gen; scan.phase = ScopePhase::Idle; scan.failures = 0; scan.autoRetry = false; scan.retryAt = 0.0; scan.error.clear(); } void scopeScanRetry(ScopeScan& scan) { if (scan.phase != ScopePhase::Failed) return; scan.phase = ScopePhase::Idle; scan.failures = 0; scan.autoRetry = false; scan.retryAt = 0.0; scan.error.clear(); } double scopeRetryDelaySec(int failures) { if (failures <= 1) return 10.0; if (failures == 2) return 30.0; return 60.0; } bool scopeFailureRetries(rpc::RpcFailureKind kind, const std::string& message) { using K = rpc::RpcFailureKind; switch (kind) { case K::NotListening: case K::ConnectTimeout: case K::Warmup: case K::Deferred: case K::ShuttingDown: case K::Unhandled: case K::Aborted: return true; case K::Other: { std::string lower = message; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return lower.find("not connected") != std::string::npos; } default: return false; // ReplyTimeout, QueueFull, AppError, Auth, parse/type errors } } } // namespace services } // namespace dragonx