Compare commits
2
Commits
5246898968
...
e367eb47ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e367eb47ff | ||
|
|
31d8742ce7 |
@@ -1,549 +0,0 @@
|
||||
# Daemon Startup Hardening — Implementation Plan
|
||||
|
||||
Eight verified edge-case defects in how ObsidianDragon brings up (and watches) the
|
||||
`dragonxd` daemon at launch. Each entry is a buildable fix: the defect (with exact
|
||||
line references), the chosen approach, the call sites, a representative change, and how
|
||||
to verify it.
|
||||
|
||||
- **Scope:** full-node startup path (`--lite` excludes the embedded daemon entirely).
|
||||
- **Source:** line references are exact against branch `dev` @ `45b652f`.
|
||||
- **Provenance:** findings verified by direct source read; each fix designed by an
|
||||
independent agent grounded in the cited files, with a sequencing pass for ordering,
|
||||
shared helpers, and merge conflicts.
|
||||
|
||||
**Severity:** 2 High, 6 Medium · **Effort:** ≈ 25–35 engineering-hours · **7 landing steps.**
|
||||
|
||||
Status legend: ☐ not started · ◐ in progress · ☑ landed & verified
|
||||
|
||||
**Status: all 8 landed & verified** (build-clean, `ctest` green after each) across four commits on
|
||||
`dev` — lifecycle cluster (F1/F2/F4), filesystem+params cluster (F7/F6/F5), F3, and F8. Six new
|
||||
pure-helper unit tests added.
|
||||
|
||||
**Wrap-up done:** release notes added (`CHANGELOG.md`, F8 breaking change front and center); i18n
|
||||
back-fill applied additively to `res/lang/*.json` (42 keys — all 6 for es/de/fr/pt/ru; 6 zh/ja/ko
|
||||
entries whose glyphs aren't in the current `NotoSansCJK-Subset.ttf` were left on English fallback
|
||||
rather than render as tofu).
|
||||
|
||||
**Still owed before release:** a **CJK subset-font rebuild** (`scripts/build_cjk_subset.py`, needs
|
||||
the Noto CJK source font) to cover the 6 deferred zh/ja/ko strings. *(F1 and F2 now have headless
|
||||
integration-test coverage — see the progress log — so their GUI repros are optional, not blocking.)*
|
||||
|
||||
---
|
||||
|
||||
## Recommended rollout sequence
|
||||
|
||||
A real dependency order, not a checklist. The daemon-lifecycle cluster lands first
|
||||
because it makes the `State::Error` / `crash_count_` contract trustworthy — which the
|
||||
connect-stall panel and the lock gate both build on. The filesystem cluster lands
|
||||
around a single shared helper. The connectivity-breaking security flip lands last.
|
||||
|
||||
| Step | Finding(s) | Site | Why here | Status |
|
||||
|------|-----------|------|----------|--------|
|
||||
| 1 | **F1** | `embedded_daemon.cpp` · `isRunning()` | Smallest/highest-severity; establishes the reliable Error/crash-count transition steps 3 & 6 depend on. | ☑ |
|
||||
| 2 | **F2** | `embedded_daemon.cpp` · `startProcess()` | Same file family, different function; test the F1+F2 pair together with `kill -SEGV` / bad-binary repros. | ☑ |
|
||||
| 3 | **F4** | `embedded_daemon.cpp` · `start()` | After F1/F2 so crash-count semantics are settled; its bail deliberately stays out of the crash path. | ☑ |
|
||||
| 4 | **F7** | `util/platform` · `connection.cpp` | Structural owner of the fs-error idiom + `ConnectionConfig` that F5/F6/F8 reuse. | ☑ |
|
||||
| 5 | **F6 + F5** | `app.cpp` · `verifySaplingParams()` | Same `startEmbeddedDaemon` / `verifySaplingParams` block; land together. | ☑ |
|
||||
| 6 | **F3** | `app.cpp` · `renderLoadingOverlay()` | After F1 — panel is guarded off during `State::Error` (owned by the crash-count hint). | ☑ |
|
||||
| 7 | **F8** | `connection.cpp` · `tryConnect()` | Largest; only connectivity-breaking default flip — land last, with release notes. | ☑ |
|
||||
|
||||
---
|
||||
|
||||
## F1 — Double-`waitpid` race can swallow a daemon crash
|
||||
|
||||
**Severity:** High · **Effort:** S (~1–2h) · **Status:** ☑ landed & verified
|
||||
|
||||
### The defect
|
||||
`EmbeddedDaemon::isRunning()` (`embedded_daemon.cpp:1136`, POSIX branch) calls
|
||||
`waitpid(WNOHANG)` — from the **UI thread, nearly every frame** — racing
|
||||
`monitorProcess()`'s own reap at `:1244`. `waitpid` is one-shot: if the UI thread wins,
|
||||
the monitor never decodes the exit, so `crash_count_` never increments, `State::Error`
|
||||
never fires, and the 3-strike auto-restart cap (`app_network.cpp:479`) is defeated. The
|
||||
sibling `XmrigManager::isRunning()` (`xmrig_manager.cpp:512`) already fixed exactly this
|
||||
with an atomic read.
|
||||
|
||||
### The fix
|
||||
Make `isRunning()` read the existing `std::atomic<State> state_` (member at
|
||||
`embedded_daemon.h:253`) instead of calling `waitpid`, leaving `monitorProcess()` as the
|
||||
sole reaper. Predicate is `Running || Stopping` — `Stopping` must stay "alive" because
|
||||
`stop()`'s graceful/SIGTERM wait loops poll `isRunning()` before the process has exited.
|
||||
|
||||
### Files touched
|
||||
- `src/daemon/embedded_daemon.cpp` — `isRunning()`, POSIX branch (~1136)
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
bool EmbeddedDaemon::isRunning() const // POSIX branch
|
||||
{
|
||||
// Read the atomic state_ instead of waitpid() — monitorProcess() is the
|
||||
// sole reaper. Previously both threads reaped; if the UI thread won, the
|
||||
// monitor never saw the exit (crash_count_ / exit code / Error all lost).
|
||||
if (process_pid_ <= 0) return false;
|
||||
|
||||
State s = state_.load(std::memory_order_relaxed);
|
||||
// Stopping stays "alive": stop()'s wait loops poll isRunning() while
|
||||
// state_ == Stopping, before the process has actually terminated.
|
||||
return (s == State::Running || s == State::Stopping);
|
||||
}
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Manual: `kill -SEGV` the daemon 10–20×; the monitor must report the exit and increment `crash_count_` every time (previously intermittent).
|
||||
- Regression: a normal Settings-driven stop still escalates SIGTERM→SIGKILL (the `Stopping` predicate).
|
||||
- Not unit-testable (real fork/exec/waitpid) — consistent with the no-process-spawn harness.
|
||||
|
||||
### Dependencies
|
||||
Mirrors `XmrigManager::isRunning()`. Flags a separate latent hazard (out of scope):
|
||||
`stop()`'s final blocking `waitpid` (`:1220`) can still race a mid-sleep monitor
|
||||
iteration — file as its own ticket.
|
||||
|
||||
---
|
||||
|
||||
## F2 — exec-after-fork silent failure: "Running" for a daemon that never started
|
||||
|
||||
**Severity:** High · **Effort:** S (~2–3h) · **Status:** ☑ landed & verified
|
||||
|
||||
### The defect
|
||||
In `startProcess()` (`embedded_daemon.cpp:957–1061`, POSIX) the parent runs
|
||||
`process_pid_ = pid; return true;` **unconditionally** after `fork()` — with no
|
||||
exec-status handshake. On a non-executable / wrong-arch / corrupt binary the child's
|
||||
`execv` fails and it `_exit(127)`s, but `start()` has already set `State::Running`
|
||||
(`:565`). The real cause never reaches `last_error_`; it surfaces later, generically,
|
||||
as "exited unexpectedly (exit code 127)".
|
||||
|
||||
### The fix
|
||||
Add a **close-on-exec self-pipe** handshake — `pipe() + fcntl(FD_CLOEXEC)`, deliberately
|
||||
**not** `pipe2()` (macOS lacks it; the POSIX branch is shared). The child writes `errno`
|
||||
only on `execv` failure; a successful exec closes the write end for free. Parent reads:
|
||||
EOF ⇒ success; 4 bytes ⇒ reap the zombie, set a precise `last_error_` ("not executable
|
||||
or wrong architecture"), and return `false` so `start()` never reports Running. EINTR-safe
|
||||
on both ends. Also comments the unchecked parent-side `setpgid` at `:1053`.
|
||||
|
||||
### Files touched
|
||||
- `src/daemon/embedded_daemon.cpp` — `startProcess()` parent read path
|
||||
- `src/daemon/embedded_daemon.cpp` — child `execv`-failure write (~1043)
|
||||
- `src/daemon/embedded_daemon.cpp` — `setpgid` best-effort comment (~1053)
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
// Self-pipe exec handshake (pipe()+FD_CLOEXEC; NOT pipe2 — macOS lacks it).
|
||||
int execpipe[2]; pipe(execpipe);
|
||||
fcntl(execpipe[0], F_SETFD, FD_CLOEXEC);
|
||||
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) { // child
|
||||
close(execpipe[0]);
|
||||
/* setpgid / chdir / dup2 / argv … */
|
||||
execv(binary_path.c_str(), argv.data());
|
||||
int e = errno; // execv failed
|
||||
while (write(execpipe[1], &e, sizeof e) < 0 && errno == EINTR) {}
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
close(execpipe[1]); // parent: must close or read() never EOFs
|
||||
int child_errno = 0, total = 0;
|
||||
for (;;) { // EOF ⇒ exec ok; 4 bytes ⇒ exec failed
|
||||
ssize_t n = read(execpipe[0], (char*)&child_errno + total, sizeof(int) - total);
|
||||
if (n == 0) break;
|
||||
if (n < 0) { if (errno == EINTR) continue; break; }
|
||||
if ((total += n) >= (int)sizeof(int)) break;
|
||||
}
|
||||
close(execpipe[0]);
|
||||
if (total >= (int)sizeof(int)) { // exec never happened
|
||||
waitpid(pid, nullptr, 0); // reap the zombie
|
||||
last_error_ = "dragonxd could not be executed: " +
|
||||
std::string(strerror(child_errno)) +
|
||||
" — not executable or wrong architecture";
|
||||
return false; // start() no longer reports Running
|
||||
}
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Point at a `chmod -x` / wrong-arch file → `start()` returns false immediately, precise message, no leftover zombie.
|
||||
- Success path: real binary still starts with no perceptible added latency.
|
||||
- Optional pure `formatExecFailureError(errno)` helper for a `test_phase4.cpp` unit test.
|
||||
|
||||
### Dependencies
|
||||
F1 (same function family; sequence F1→F2). **Highest-risk mistake:** forgetting
|
||||
`FD_CLOEXEC` makes every successful start hang the parent read forever.
|
||||
|
||||
---
|
||||
|
||||
## F4 — Stale datadir-lock start → restart storm that wedges the UI
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||
|
||||
### The defect
|
||||
`start()` (`embedded_daemon.cpp:466`) gates only on the RPC port (`:482`), never on
|
||||
`isDaemonProcessRunning()` (`:1292`). A graceful shutdown frees the port but keeps the
|
||||
datadir `.lock` for up to ~90s. A rapid stop→start spawns a daemon that dies "Cannot
|
||||
obtain a lock on data directory" — routed to the generic crash path. With a ~4s retry
|
||||
cadence, **three lock races in ~12s exhaust the 3-strike budget** and wedge the UI long
|
||||
before the lock actually clears.
|
||||
|
||||
### The fix
|
||||
Fail-fast with a **short bounded local wait (~300ms), not a 90s block**. After the port
|
||||
bail, consult `isDaemonProcessRunning()` — gated by `!skip_port_check_` and exempt when
|
||||
`override_datadir_` is set, so the isolated migrate-to-seed daemon still works. A pure
|
||||
`evaluateDatadirLockGate()` returns a **distinct non-crash Error** that never increments
|
||||
`crash_count_`. The connect loop's own retry then absorbs the transient.
|
||||
|
||||
### Files touched
|
||||
- `src/daemon/embedded_daemon.h` — decision struct, helper decl, poll constants
|
||||
- `src/daemon/embedded_daemon.cpp` — `start()` gate + `evaluateDatadirLockGate()`
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
static StartLockGateDecision evaluateDatadirLockGate(
|
||||
bool skipPortCheck, bool isolatedOverride, bool stillRunningAfterWait) {
|
||||
if (skipPortCheck || isolatedOverride) return {true, ""}; // migrate-to-seed exempt
|
||||
if (!stillRunningAfterWait) return {true, ""};
|
||||
return {false, "A previous dragonxd is still shutting down and holding the "
|
||||
"data directory lock. Retrying shortly…"};
|
||||
}
|
||||
|
||||
// start() — after the isPortInUse() bail, before setState(Starting):
|
||||
if (!skip_port_check_ && override_datadir_.empty()) {
|
||||
bool stillLocked = false; // ~300ms bounded wait, NOT ~90s
|
||||
for (int i = 0; i < kDatadirLockWaitMaxPolls; ++i) {
|
||||
if (!isDaemonProcessRunning()) { stillLocked = false; break; }
|
||||
stillLocked = true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kDatadirLockWaitPollMs));
|
||||
}
|
||||
auto gate = evaluateDatadirLockGate(false, false, stillLocked);
|
||||
if (!gate.proceed) { setState(State::Error, gate.errorMessage); return false; }
|
||||
}
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Unit: `evaluateDatadirLockGate()` across the skip / isolated / still-running matrix.
|
||||
- Manual: rapid restart into a lingering lock → distinct message, no crash-cap wedge.
|
||||
- Migrate-to-seed second daemon still starts (isolated exemption).
|
||||
|
||||
### Dependencies
|
||||
F1/F2 (must not touch `crash_count_`; wording must not collide with the monitor's
|
||||
"exited unexpectedly"). Same TU, different function.
|
||||
|
||||
---
|
||||
|
||||
## F5 — Extraction / copy write-failures never surfaced up front
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~2–3h) · **Status:** ☑ landed & verified
|
||||
|
||||
### The defect
|
||||
`startEmbeddedDaemon()` discards `extractEmbeddedResources()`'s `bool` return
|
||||
(`app.cpp:4152`) and the second copy-fallback loop drops `copy_file`'s `error_code`
|
||||
entirely (`:4236`). Only Sapling params **existence** is re-checked — never the daemon
|
||||
binary/CLI/tx/asmap. A disk-full or truncated `dragonxd` write falls straight through to
|
||||
spawn and fails opaquely. The innermost write already returns `false`
|
||||
(`embedded_resources.cpp:307`) — the signal is simply thrown away.
|
||||
|
||||
### The fix
|
||||
Minimal, surgical wiring — no new abstraction. Capture the extraction return and, on
|
||||
failure, set `daemon_status_ = TR("sb_daemon_extract_failed")` and `return false` before
|
||||
spawning. In the second copy loop, check `ec` after each `copy_file`, track `copyFailed`,
|
||||
and abort with a dir-parameterized `sb_daemon_files_failed`. An **absent source** stays
|
||||
fine (optional files); only an actual `error_code` counts. Written so F6/F7 slot in later
|
||||
without re-touching this control flow.
|
||||
|
||||
### Files touched
|
||||
- `src/app.cpp` — `startEmbeddedDaemon()` extraction check (~4152)
|
||||
- `src/app.cpp` — second copy-fallback loop (~4210–4242)
|
||||
- `src/util/i18n.cpp` + `res/lang/*.json` — 2 additive keys
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
// stop discarding the extraction result (~4152)
|
||||
if (!resources::extractEmbeddedResources()) {
|
||||
daemon_status_ = TR("sb_daemon_extract_failed"); // disk full / permission denied
|
||||
return false; // abort before spawning
|
||||
}
|
||||
|
||||
// second copy-fallback loop — was dropping ec entirely (~4236)
|
||||
bool copyFailed = false;
|
||||
for (const char* name : { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" }) {
|
||||
fs::path dst = fs::path(daemon_dir) / name;
|
||||
if (fs::exists(dst)) continue; // already present — skip
|
||||
for (const auto& dir : searchDirs) {
|
||||
fs::path src = fs::path(dir) / name;
|
||||
if (!fs::exists(src)) continue; // absent source is OK, not a failure
|
||||
fs::copy_file(src, dst, ec);
|
||||
if (ec) { copyFailed = true; ec.clear(); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (copyFailed) {
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof buf, TR("sb_daemon_files_failed"), daemon_dir.c_str());
|
||||
daemon_status_ = buf;
|
||||
return false; // don't fall through to spawn
|
||||
}
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Unit: `extractEmbeddedResources()` returns false without embedded resources.
|
||||
- Extract the copy loop into a testable helper; force one dst write to fail (dst is an existing directory).
|
||||
- Manual: near-full tmpfs / read-only dir → clear status, daemon controller never constructed.
|
||||
|
||||
### Dependencies
|
||||
Shares the `daemon_status_` surfacing convention with F6; its early-return pattern is the
|
||||
template F7 matches. Open item: remove truncated dst files so a retry re-copies.
|
||||
|
||||
---
|
||||
|
||||
## F6 — Sapling params validated by existence/size only, never hashed
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||
|
||||
> **As-built note.** `verifySaplingParams()` now delegates to a public, injectable
|
||||
> `verifySaplingParamsIn(dir, digests)` so the integrity + marker-cache logic is unit-testable
|
||||
> with synthetic small files (the real 48 MB params aren't in the repo). i18n keys for F5 were
|
||||
> added to `i18n.cpp` (English source of truth); the `res/lang/*.json` back-fill via
|
||||
> `scripts/add_missing_translations.py` is deferred to a single run at the end of the batch,
|
||||
> per the cross-cutting note. Non-English locales fall back to English until then.
|
||||
|
||||
### The defect
|
||||
`verifySaplingParams()` (`connection.cpp:123`) only calls `fs::exists()`;
|
||||
`resourceNeedsUpdate()` (`embedded_resources.cpp:250`) is size-only. On Linux (no
|
||||
embedded resources) a **truncated-but-present** param passes and is handed to the daemon,
|
||||
which then fails to build shielded proofs mid-operation — far from the real cause.
|
||||
|
||||
### The fix
|
||||
Add a pinned `{ filename → size, sha256 }` table (one source of truth, cross-referenced
|
||||
to `scripts/build-lite-backend-artifact.sh`) and hash-check each param after the
|
||||
existence check, reusing the existing `util::sha256Hex` (no second implementation). Since
|
||||
these are ~48 MB, **cache the result** via a `.sapling_verified` marker keyed on
|
||||
`size:mtime` — re-hash only when the stat line changes, so startup isn't slowed.
|
||||
|
||||
### Files touched
|
||||
- `src/rpc/connection.h` — `verifySaplingParams` decl
|
||||
- `src/rpc/connection.cpp` — digest table, marker helpers, rewrite
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
// connection.cpp — pinned known-good digests
|
||||
// (source of truth: scripts/build-lite-backend-artifact.sh ensure_sapling_params)
|
||||
constexpr SaplingParamDigest kSaplingParamDigests[] = {
|
||||
{ "sapling-spend.params", 47958396, "8e48ffd2…efc13" },
|
||||
{ "sapling-output.params", 3592860, "2f0ebbcb…fb0e4" },
|
||||
};
|
||||
|
||||
bool Connection::verifySaplingParams() {
|
||||
// existence check (unchanged) …
|
||||
// cache: skip re-hashing a ~48 MB file unless size:mtime changed
|
||||
if (readMarkerMatches(marker, statLines)) return true;
|
||||
for (auto& d : kSaplingParamDigests)
|
||||
if (util::sha256Hex(bytes) != d.sha256) return false; // reuse existing helper
|
||||
writeMarker(marker, statLines);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Unit: good params pass; truncated / wrong-bytes rejected; marker cache short-circuits re-hash unless size/mtime changed. Real temp-file fixtures (matches existing `sha256Hex` tests).
|
||||
|
||||
### Dependencies
|
||||
F7 (reuse fs-error idiom; shares the `startEmbeddedDaemon`/`verifySaplingParams` block).
|
||||
Third caller of the existing `util::sha256Hex`.
|
||||
|
||||
---
|
||||
|
||||
## F7 — Directory-create errors universally ignored on the daemon-env path
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~3–4h) · **Status:** ☑ landed & verified
|
||||
|
||||
> **As-built notes.** Two deviations from the original design, both confirmed against the code:
|
||||
> (1) `embedded_resources.cpp:270` already checks its `error_code` and returns `false` on failure — it was **not** a bug, so it is left untouched.
|
||||
> (2) Of the four `autoDetectConfig` callers, only the primary connect path (`app_network.cpp:243`) was wired to check `dir_error`; the other three degrade gracefully on their own — `app.cpp:4306` and `app_wizard.cpp:912` are stop paths that already gate on empty creds, and `settings_page.cpp:434` is read-only display. `dir_error` is set by `autoDetectConfig`, so they can be wired later if desired.
|
||||
|
||||
### The defect
|
||||
Five startup directory-create sites either drop the `error_code` or use the throwing
|
||||
overload with no `catch`: `main.cpp:730`, `connection.cpp:216` (can throw **uncaught**
|
||||
through its callers), `embedded_resources.cpp:270`, `app.cpp:4172`/`4218`. A read-only
|
||||
home or permission-denied yields a confusing "conf missing" / "binary not found"
|
||||
downstream — or an uncaught `filesystem_error` — instead of a clear cause.
|
||||
|
||||
### The fix
|
||||
One shared, non-throwing `Platform::ensureDirectory(dir, outError)` in
|
||||
`util/platform.{h,cpp}` that produces a single consistent message. Replace all five
|
||||
sites; `autoDetectConfig()` moves off the throwing overload and sets a new
|
||||
`ConnectionConfig::dir_error` that its four callers check and bail on. This is the
|
||||
**structural owner** of the fs-error idiom that F5 and F6 reuse.
|
||||
|
||||
### Files touched
|
||||
- `src/util/platform.h` / `.cpp` — `ensureDirectory()`
|
||||
- `src/rpc/connection.h` / `.cpp` — `dir_error` + `autoDetectConfig`
|
||||
- `main.cpp`, `app.cpp`, `app_network.cpp`, `app_wizard.cpp`, `settings_page.cpp`, `embedded_resources.cpp` — 5 sites + 4 callers
|
||||
- `tests/test_phase4.cpp` — `TestPlatformEnsureDirectory`
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
// util/platform.cpp — one shared, non-throwing helper
|
||||
bool Platform::ensureDirectory(const std::string& dir, std::string* outError) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_directory(dir, ec)) return true;
|
||||
ec.clear();
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
if (outError)
|
||||
*outError = "Cannot create " + dir + ": " + ec.message() +
|
||||
". Check permissions / free space.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Replaces 5 ad-hoc sites; autoDetectConfig() now sets ConnectionConfig::dir_error,
|
||||
// and its 4 callers bail on it.
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Unit `TestPlatformEnsureDirectory`: existing dir → true; fresh nested → created; POSIX unwritable → false + message.
|
||||
- All four `autoDetectConfig` callers tolerate `dir_error`. Pre-App-init site (main.cpp) reports via stderr / MessageBox.
|
||||
|
||||
### Dependencies
|
||||
**Owns** `Platform::ensureDirectory` (used by F5, F6) and the `ConnectionConfig`
|
||||
extension (coordinated with F8). Land before F5/F6/F8.
|
||||
|
||||
---
|
||||
|
||||
## F8 — Plaintext-remote RPC credential transmission is warn-only
|
||||
|
||||
**Severity:** Medium · **Effort:** M (~6–9h) · **Status:** ☑ landed & verified
|
||||
|
||||
> **⚠️ RELEASE NOTES REQUIRED — breaking default flip.** A wallet configured to talk to a
|
||||
> **remote** `rpchost` over **plain HTTP** (no `rpctls=1`) will now be **refused** at connect
|
||||
> time instead of warned. Affected users must add **`rpcallowplaintext=1`** to `DRAGONX.conf`
|
||||
> (or switch to `rpctls=1`) to reconnect. Local/embedded daemons (`127.0.0.0/8`, `localhost`,
|
||||
> `::1`) are unaffected. Call this out prominently in the release notes.
|
||||
>
|
||||
> **As-built note.** Shipped the security-complete core: `isLocalHost` tightened to exact
|
||||
> loopback (`isExactIPv4Loopback` — `127.evil.com` no longer passes), refuse-by-default in
|
||||
> `tryConnect`, and the `rpcallowplaintext` conf-key opt-in. The **Settings toggle UI was
|
||||
> deferred** — the RPC section of `settings_page.cpp` is read-only display and a security
|
||||
> toggle there is riskier surface; the conf-key opt-in fully covers recovery, and the refusal
|
||||
> status/notification tells the user exactly what to add. The toggle can be added later
|
||||
> (persist a `Settings` flag and OR it into `allowsPlaintextRemote`).
|
||||
|
||||
### The defect
|
||||
A remote `rpchost` without `rpctls=1` sends Basic-auth `rpcuser:rpcpassword` over
|
||||
cleartext HTTP. `tryConnect()` (`app_network.cpp:314`) only shows a **dismissible
|
||||
warning** then proceeds — a local-network MITM sees the credentials. Compounding it,
|
||||
`isLocalHost()`'s naive `rfind("127.",0)==0` misclassifies `127.evil.com` as local,
|
||||
suppressing even the warning.
|
||||
|
||||
### The fix
|
||||
Change the policy to **refuse-by-default with an explicit, persisted opt-in** — a
|
||||
`rpcallowplaintext=1` conf key (for hand-editors) and a Settings toggle. Block the
|
||||
connect and show a **blocking modal** explaining the risk and how to enable TLS or opt
|
||||
in; localhost is unaffected. Tighten `isLocalHost()` to exact `127.x.y.z` / `::1` /
|
||||
`localhost` via `isExactIPv4Loopback()`. **Back-compat:** default off ⇒ existing remote
|
||||
users hit a hard stop until they opt in — **ship with prominent release notes.**
|
||||
|
||||
### Files touched
|
||||
- `src/rpc/connection.h` / `.cpp` — `isLocalHost`, `allow_plaintext_remote`, `parseConfFile`
|
||||
- `src/config/settings.h` / `.cpp` — persisted opt-in
|
||||
- `src/app_network.cpp`, `src/app.h` — refuse + modal dispatch
|
||||
- `src/ui/windows/plaintext_remote_rpc_dialog.h` — new blocking modal
|
||||
- `src/ui/pages/settings_page.cpp` — toggle UI
|
||||
|
||||
### Core change
|
||||
```cpp
|
||||
// Tightened loopback test — "127.evil.com" is NOT local
|
||||
bool Connection::isLocalHost(const std::string& host) {
|
||||
std::string h = stripBrackets(lowercase(host));
|
||||
return h == "localhost" || h == "::1" || isExactIPv4Loopback(h); // exact 127.x.y.z
|
||||
}
|
||||
|
||||
// Refuse-by-default with an explicit, persisted opt-in
|
||||
const bool plaintextRemote = rpc::Connection::usesPlaintextRemote(config);
|
||||
const bool plaintextAllowed = config.allow_plaintext_remote // rpcallowplaintext=1
|
||||
|| settings_.getAllowPlaintextRemoteRpc(); // Settings toggle
|
||||
if (plaintextRemote && !plaintextAllowed) {
|
||||
connection_status_ = TR("sb_plaintext_remote_blocked");
|
||||
showPlaintextRemoteRpcDialog(config.host + ":" + config.port); // blocking modal
|
||||
return; // no creds sent
|
||||
}
|
||||
```
|
||||
|
||||
### Verification
|
||||
- Unit: `isLocalHost` — `127.evil.com` false, `127.0.0.1`/`::1`/`localhost` true; `allowsPlaintextRemote` honors conf key + settings flag.
|
||||
- Manual: remote plaintext blocked; modal fires; opt-in persists across restart.
|
||||
|
||||
### Dependencies
|
||||
F7 (second extender of `ConnectionConfig`/`parseConfFile`; land after so the struct grows
|
||||
once). Wire `renderPlaintextRemoteRpcDialog` into the app modal-dispatch list.
|
||||
|
||||
---
|
||||
|
||||
## Shared helpers & coordination points
|
||||
|
||||
| Helper | Purpose | Used by |
|
||||
|--------|---------|---------|
|
||||
| `Platform::ensureDirectory()` | Single non-throwing directory-create with one consistent message; replaces five ad-hoc sites. Owned by F7. | F7, F5, F6 |
|
||||
| `ConnectionConfig` extension | Coordination point, not a function: F7 adds `dir_error`, F8 adds `allow_plaintext_remote`. Land F7→F8 so it grows once per step. | F7, F8 |
|
||||
| `util::sha256Hex` *(existing)* | Already-compiled, curl-free SHA-256. F6 becomes its third caller — no second hash routine. | F6 |
|
||||
| `connectHasStalled()` *(new, pure)* | Stall predicate split out of the ImGui/App code for unit testing, per the `*_updater_core.cpp` precedent. | F3 |
|
||||
| `evaluateDatadirLockGate()` *(new, pure)* | Lock-gate decision as `{proceed, message}` from three booleans — unit-testable without real process/fs I/O. | F4 |
|
||||
|
||||
## F3 — Unbounded connect spinner (deferred to step 6)
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||
|
||||
> **As-built note.** `renderLoadingOverlay()` is a pure draw-list overlay with **no interactive
|
||||
> widgets** (the existing crash case at ~5289 already communicates via guidance *text*, relying on
|
||||
> the sidebar staying reachable). So rather than inject `ActionButton`s — which would fight the
|
||||
> non-interactive overlay — the stall notice follows that same idiom: a "Taking longer than
|
||||
> expected" title + a reassuring body (with elapsed seconds) + a full-node-gated hint ("Open
|
||||
> Settings → Restart Daemon, or check the Console"). This let me drop the planned
|
||||
> `WalletState::connect_stalled` flag too: the stalled state is computed locally in the overlay
|
||||
> from `connect_stall_since_`, so the only new member is `App::connect_stall_since_`.
|
||||
|
||||
The connect loop retries forever while `!state_.connected` (`app.cpp:1239`);
|
||||
`loading_timer_` only animates the spinner. Stamp `connect_stall_since_` when
|
||||
"reachable but not ready" is first seen; a pure `connectHasStalled()` helper (new
|
||||
`util/connect_stall.h`, default 45s from `ui.toml`) flips `state_.connect_stalled` at
|
||||
threshold, and `renderLoadingOverlay()` shows a "Taking longer than expected" panel with
|
||||
Retry / Restart daemon / Open console (full-node gated). The background retry keeps
|
||||
firing — recovery clears the panel automatically. Guarded off while the daemon is in
|
||||
`State::Error` (owned by F1's crash-count hint). Full detail lives in the sequencing/
|
||||
design record; see the shared-helper table above.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting notes
|
||||
|
||||
- **One TU, three functions.** `embedded_daemon.cpp` is edited by F1 (`isRunning`),
|
||||
F2 (`startProcess`) and F4 (`start`) — no literal hunk overlap, but land in order to
|
||||
keep "monitorProcess is the sole reaper" coherent.
|
||||
- **Connection struct grows twice.** `connection.h/.cpp` is touched by F6, F7 and F8;
|
||||
F7 and F8 both extend `ConnectionConfig` and `parseConfFile` — highest collision risk.
|
||||
Sequence F7→F6→F8.
|
||||
- **Testability split.** The three new pure predicates all get `tests/test_phase4.cpp`
|
||||
coverage. F1/F2's fork/exec/waitpid changes are **not** unit-testable — they rely on
|
||||
manual `kill` / non-executable-binary repros, consistent with the no-process-spawn harness.
|
||||
- **i18n is additive-only.** Add each finding's English keys to `strings_`, then run
|
||||
`scripts/add_missing_translations.py` **once at the very end**
|
||||
(`json.dump indent=4, sort_keys=True, ensure_ascii=False`) — never bulk-regenerate a
|
||||
`res/lang/*.json`.
|
||||
- **F8 is a breaking default flip.** Refuse-plaintext-by-default stops existing
|
||||
remote-RPC users cold until they opt in. Lands last, gated behind a persisted opt-in,
|
||||
with release notes calling out the new `rpcallowplaintext` key and the Settings toggle.
|
||||
- **Latent hazard, out of scope.** F1 surfaces (but doesn't fix) a second
|
||||
double-`waitpid` window between `stop()`'s final blocking reap (`:1220`) and a
|
||||
mid-sleep monitor iteration — file it as its own ticket.
|
||||
|
||||
---
|
||||
|
||||
## Progress log
|
||||
|
||||
- **F1/F2 integration tests** — ☑ added `testExecFailureReported` (F2) and `testDaemonCrashDetected` (F1) to `test_phase4.cpp`, driving the **real** `EmbeddedDaemon` fork/exec/waitpid code headlessly (POSIX; required linking `embedded_daemon.cpp` into the test target — its deps were already there). The F1 test hammers `isRunning()` from the test thread while the child exits, so it's a genuine regression test for the reap race. **The F2 test caught a real bug:** `start()`'s failure branch overwrote `startProcess()`'s precise `last_error_` ("…not executable or wrong architecture") with a generic "Failed to start dragonxd process" (because `setState(Error, …)` stores its message into `last_error_`), so the precise reason never reached `getLastError()`/the UI — **fixed** to preserve the detail (now also surfaced via the state callback / crash panel). Build-clean; `ctest` 1/1.
|
||||
|
||||
- **F1** — ☑ landed: `isRunning()` (POSIX) now reads the atomic `state_` (predicate `Running || Stopping`) instead of calling `waitpid`, leaving `monitorProcess()` the sole reaper. Clean build (all targets link); `ctest` 1/1 passing. Not unit-testable — needs the manual `kill -SEGV` repro before release.
|
||||
- **F2** — ☑ landed: `startProcess()` (POSIX) now creates a `FD_CLOEXEC` self-pipe before `fork()`; the child writes `errno` to it on `execv` failure, the parent reads EOF-vs-errno and, on failure, reaps the zombie + sets a precise `last_error_` ("not executable or wrong architecture") + returns `false` (so `start()` no longer reports `Running` for a daemon that never started). Parent-side `setpgid` is now best-effort with a `DEBUG_LOGF` on failure. Clean build; `ctest` 1/1 passing. Not unit-testable — needs the manual non-executable / wrong-arch-binary repro before release.
|
||||
- **F8** — ☑ landed: `isLocalHost()` tightened to exact loopback via `isExactIPv4Loopback` (a `127.`-prefixed *hostname* like `127.evil.com` is no longer misclassified as local). `tryConnect()` now **refuses** a plaintext connection to a remote host instead of warn-and-proceeding — a local-network MITM can no longer capture `rpcuser:rpcpassword` — unless the user opts in with `rpcallowplaintext=1` in `DRAGONX.conf` (new `ConnectionConfig::allow_plaintext_remote` + `allowsPlaintextRemote()` policy). The refusal surfaces via status line + a one-time notification. New `testIsLocalHost` (12 assertions) + `testAllowsPlaintextRemote` (5). Clean build; `ctest` 1/1 passing. **Breaking — needs release notes; Settings-toggle UI deferred (see as-built note).**
|
||||
- **F3** — ☑ landed: the connect loop now stamps `connect_stall_since_ = ImGui::GetTime()` the moment the daemon first goes "reachable but not ready" (warmup branch + `applyDaemonInitStatus`), and clears it in `onConnected` / `onDisconnected` / warmup-complete — all in `app_network.cpp`. The pure `util::connectHasStalled(stallSince, now, threshold)` helper (new `util/connect_stall.h`, default 45 s from `ui.toml`) drives a draw-list "Taking longer than expected" notice in `renderLoadingOverlay()` (title + elapsed-seconds body + full-node hint), guarded off while the daemon is in `State::Error`. Background retry continues, so the notice self-clears on connect. New `testConnectHasStalled` unit test (7 assertions). Clean build; `ctest` 1/1 passing. (Draw-list text, not buttons — see as-built note above.)
|
||||
- **F6** — ☑ landed: `verifySaplingParams()` now hash-verifies each Sapling param against its pinned canonical SHA-256 (from `build-lite-backend-artifact.sh`), replacing the existence-only check, so a truncated/corrupt-but-present param is rejected instead of failing later on a shielded op. A `<params_dir>/.sapling_verified` marker keyed on `size:mtime` skips re-hashing ~48 MB on every startup. Logic extracted to the injectable `verifySaplingParamsIn(dir, digests)`; new `testVerifySaplingParams` unit test (valid / marker fast-path / wrong-hash / truncated / missing). Clean build; `ctest` 1/1 passing.
|
||||
- **F5** — ☑ landed: `startEmbeddedDaemon()` now checks `extractEmbeddedResources()`'s return (abort with `sb_daemon_extract_failed` on failure) and the previously-dropped `copy_file` `error_code` in the daemon-binary fallback loop (abort with `sb_daemon_files_failed` incl. the dir), so a disk-full / truncated `dragonxd` write is surfaced up front instead of failing opaquely at spawn. An absent source file stays non-fatal. Two i18n keys added to `i18n.cpp`. Clean build; `ctest` 1/1 passing.
|
||||
- **F7** — ☑ landed: new non-throwing `Platform::ensureDirectory(dir, outError)` in `util/platform.{h,cpp}` with one consistent message. Replaces the unchecked/throwing directory-create sites at `main.cpp:730` (pre-init: now logs + `MessageBoxA` on Windows + `return 1`), `connection.cpp:216` (autoDetectConfig now uses the ec overload — **no more uncaught `filesystem_error`** — and sets the new `ConnectionConfig::dir_error`), and both `app.cpp` daemon-dir sites (surface via `daemon_status_` + `return false`). Primary connect path (`app_network.cpp:243`) checks `dir_error` and bails to the status line instead of mislabelling it "waiting for config". `embedded_resources.cpp:270` left as-is (already correct). New `testPlatformEnsureDirectory` unit test (existing-dir / fresh-nested / empty / parent-is-file). Clean build; `ctest` 1/1 passing.
|
||||
- **F4** — ☑ landed: `start()` now gates on a lingering datadir lock after the port bail. When `!skip_port_check_ && override_datadir_.empty()`, it polls `isDaemonProcessRunning()` with a bounded ~300 ms wait (3 × 100 ms, breaks early), then a pure header-inline `evaluateDatadirLockGate()` decides: if a sibling `dragonxd` is still alive it bails with a distinct **non-crash** `State::Error` ("…holding the data directory lock. Retrying shortly…") that never touches `crash_count_`, so the 3-strike cap can't trip; the connect loop's retry resumes once the lock clears. Isolated migrate-to-seed starts are exempt. New `testDatadirLockGate` unit test (5 assertions, proceed/bail/2× exempt) added to `test_phase4.cpp`. Clean build; `ctest` 1/1 passing.
|
||||
@@ -1,61 +0,0 @@
|
||||
**DragonX (DRGX) Lite Desktop Wallet — v1.0.0**
|
||||
|
||||
ObsidianDragonLite is a lightweight companion to the ObsidianDragon full-node wallet. It skips the embedded full node entirely — **no multi-gigabyte blockchain download and near-instant startup** — by connecting to a DragonX lite-wallet (lightwalletd-style) server, while keeping the same native ImGui interface and shielded-first workflow.
|
||||
|
||||
This is the **first release** of the Lite variant.
|
||||
|
||||
---
|
||||
|
||||
## Why Lite?
|
||||
|
||||
- **No blockchain to download.** Sync in seconds instead of hours; a few MB of state instead of many GB.
|
||||
- **Same wallet, lighter footprint.** The familiar ObsidianDragon UI, shielded and transparent addresses, and address book — without running a node or miner.
|
||||
- **Portable.** A single self-contained binary that stores its data in its own `ObsidianDragonLite` folder, so it coexists cleanly alongside the full-node wallet.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast, node-free operation** — connect to a DragonX lite-wallet server and sync in seconds; minimal disk and RAM.
|
||||
- **Wallet lifecycle** — create a new wallet, restore from a seed phrase, or open an existing one; wallets auto-open on startup, with a first-run welcome prompt and **guided seed backup** on creation.
|
||||
- **Shielded + transparent** — send, shield, and receive DRGX; per-address balances computed from unspent notes and UTXOs.
|
||||
- **Keys & seed** — export your seed phrase and keys, and import keys, from Settings.
|
||||
- **Passphrase encryption** — encrypt, unlock, lock, and decrypt the wallet; you're prompted to unlock at send time and on startup when the wallet is locked.
|
||||
- **Encrypted messaging (HushChat)** — built-in **Contacts** and **Chat** with a seed-derived, **SilentDragonXLite-compatible** identity and a seed-encrypted local message store, so you can message other DragonX users end-to-end.
|
||||
- **Server management** — a built-in **server browser with automatic failover**; switch servers with live reconnect/recovery, and a **"Redownload blocks"** action to rescan from the server.
|
||||
- **Status & diagnostics** — a **Network tab** showing connection and sync status, plus an **interactive Console** for running backend commands and copyable error output.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (DMG + .app), x86-64.
|
||||
- QR code generation for receiving addresses
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## How it differs from the full-node wallet
|
||||
|
||||
- **No embedded `dragonxd`, no mining, no local blockchain explorer.** Chain data comes from the lite-wallet server rather than being verified locally by a full node — a lighter footprint in exchange for trusting the server for chain state.
|
||||
- Everything key-related stays **on your device**: seed, keys, and (when enabled) the encryption passphrase never leave the machine.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragonLite-1.0.0.AppImage | `a1459d6081124a6b8df47aa898b60c0237875e8cbb132e4fd8ae637673055ed4` |
|
||||
| ObsidianDragonLite-1.0.0-Linux-x64.zip | `573af502a6372334572e4dc88bd26e0cf36ed543b9df1e835fd8e4abd295108d` |
|
||||
| ObsidianDragonLite-1.0.0.exe | `28659efb770fa573bd1b1d0fcab1c1a3f9c7e46574185c3916cc9f669684852c` |
|
||||
| ObsidianDragonLite-1.0.0-Windows-x64.zip | `f6434a931e873e5936ebe249b044d6c7a413694767c667374c91ee438259b50c` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.dmg | `10f976f1453e6b1978cb7a85026fd033c85a10c1e66f436475aa628379a298c7` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.app.zip | `3e7259c363f7be2e9027c5282c94b742f4be280c998203752dc0a0551f6ab68b` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** x86-64 (Intel; runs on Apple Silicon via Rosetta 2), macOS 10.15 Catalina or later
|
||||
- Network access to a DragonX lite-wallet server.
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -1,86 +0,0 @@
|
||||
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.0**
|
||||
|
||||
This is ObsidianDragon, a native ImGui-based wallet for the DragonX network. It ships with an embedded full-node daemon (`dragonxd`) and an integrated CPU miner (DRG-XMRig), giving users a complete, self-contained experience on Linux, Windows, and macOS.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v2.0.0
|
||||
|
||||
v2.0.0 is a major release. It adds **end-to-end encrypted HushChat messaging** and **BIP39 seed-phrase wallets** (with migrate-to-seed for legacy wallets), lets the wallet **update its own node and miner** with each install cryptographically verified, and ships a **security-hardening pass** across the updater, RPC, and secret-storage surfaces — plus full support for the **new multi-threaded `dragonxd`** and a large stability and UX overhaul.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Encrypted messaging (HushChat)** — new **Contacts** and **Chat** tabs bring DRGX-native, end-to-end encrypted messaging. Your chat identity is derived from your wallet seed, messages are encrypted with libsodium (XChaCha20-Poly1305 / secretstream) and stored in a **seed-encrypted local database**, and the wire format is interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **BIP39 seed-phrase wallets & migrate-to-seed** — new wallets are backed by a mnemonic **seed phrase** you can back up from Settings. Existing **legacy wallets can be migrated into a seed wallet** — the wallet mints a new mnemonic wallet, sweeps your funds to it, and adopts it only once the sweep is mined, keeping a timestamped `wallet.dat` backup throughout. (The bundled `dragonxd` supports the required mnemonic RPCs; older nodes degrade gracefully.)
|
||||
- **In-app daemon updater** — Settings → **Node & Security → Daemon binary → "Check for updates…"** downloads the latest `dragonxd` from the project Gitea, verifies its **SHA-256 and a detached ed25519 signature** before installing, and replaces the binary **atomically while the node keeps running** (the new build takes effect on the next daemon start). A two-pane version picker lets you pin, downgrade, or install any published/pre-release build.
|
||||
- **In-app miner updater** — an **"Update miner…"** action in the Mining tab fetches, verifies (SHA-256 + ed25519), and installs the latest DRG-XMRig, with the same version picker and a display of current vs. latest.
|
||||
- **Daemon binary management** — the wallet no longer auto-overwrites a node you've dropped in; a dedicated Settings panel shows the managed binary and gathers all node actions (restart, rescan, repair) onto one toolbar.
|
||||
- **Repair Wallet** — a one-click `-zapwallettxes=2` recovery in Settings, plus automatic wallet reconciliation after a bootstrap and runtime rescan support for pruned nodes.
|
||||
- **Explorer search** — fuzzy, live (debounced) filtering of the block list by partial hash or height.
|
||||
- **History sorting & fast load** — sort transactions by date or amount, a "Loading older history (N%)" indicator during the initial bulk load, and persisted history that surfaces pending sends immediately.
|
||||
- **Smarter mining** — the thread benchmark now measures **sustained (thermally-throttled) hashrate** instead of an inflated initial burst, with GPU-aware idle mining and corrected idle thread scaling.
|
||||
|
||||
### Security & Integrity
|
||||
|
||||
- **Mandatory signature verification** — both the daemon and miner updaters **require** a valid ed25519 signature (checked against a key pinned in the wallet) in addition to the SHA-256; an install is refused if the signature is missing or invalid.
|
||||
- **Hardened secret handling** — RPC credentials are wiped from memory after use, RPC responses are size-capped and scrubbed of secrets in logs, generated node config is written **owner-only (0600)**, and secrets copied to the clipboard **auto-clear**.
|
||||
- **Safe on-disk writes** — settings, address book, and secret files are written **atomically with owner-only permissions**; the PIN vault fsyncs its secure-delete overwrite; SQLite cache growth is bounded.
|
||||
- **Recipient validation** — sends validate recipient address checksums (Base58Check + Bech32) before building a transaction.
|
||||
- **Path-traversal protection** — archive extraction (chain bootstrap and the updaters) rejects any entry that would escape the target directory (zip-slip), and the bootstrap fails closed on a missing checksum rather than trusting an unverified archive.
|
||||
- **Robustness** — malformed RPC error JSON is guarded and sends are single-flight, preventing duplicate/ill-formed submissions.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **New multi-threaded daemon support** — live Sapling note-witness rebuild progress, accurate sync-speed display, reliable rescan-completion detection, and RPC polling throttled during sync so block download isn't slowed.
|
||||
- **Networking** — DragonX DNS seed nodes, `-maxconnections` passed to the daemon, and a peer count that stays current on every tab.
|
||||
- **Node resilience** — non-blocking warmup so you can connect while the daemon is still initializing, a live daemon-console tail on the startup overlay, fast-failing connect probes, and mid-session disconnect detection that keeps the UI responsive (in-flight RPC calls abort cleanly on disconnect/shutdown).
|
||||
- **Address list** — modernized with drag-to-transfer, labels, and view-only handling.
|
||||
- **UI / DPI** — overlay dialogs scale correctly with the font/DPI setting, improved CJK font rendering, native language names in the language picker, and format-incompatible translations are rejected.
|
||||
- **Settings** — an "Open data folder" button and confirmation modals for rescan and restart-daemon.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Extensive fixes across history (shielded-tx ordering, stuck "refreshing history" banners, unconfirmed-badge stickiness), sends (correct fee passed to `z_sendmany`, fast-lane worker restart on reconnect, note-selection fee-gap workaround), rescan (accurate completion detection, no false "complete", no per-second error flood), RPC (mid-session disconnect handling, stale-refresh invalidation), and UI layout/i18n. See the commit history for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Full-node wallet** — send, receive, and verify DRGX transactions with a bundled `dragonxd` daemon; no external setup required.
|
||||
- **Self-updating** — verify-and-install the latest node and miner from within the app (SHA-256 + ed25519 signature enforced).
|
||||
- **Encrypted messaging** — built-in HushChat with a seed-derived identity, end-to-end encryption, and a seed-encrypted local message store, interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **Seed-phrase wallets** — BIP39 mnemonic backup, and migrate-to-seed to upgrade a legacy wallet into a seed wallet.
|
||||
- **Built-in CPU mining** — start/stop DRG-XMRig from the Mining tab with real-time hashrate and pool statistics, mine-when-idle support with configurable delay, and sustained-hashrate benchmarking.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (**universal** DMG + .app); Linux/Windows x86-64, macOS Intel + Apple Silicon.
|
||||
- Shielded (z-address) and transparent (t-address) send/receive
|
||||
- QR code generation for receiving addresses
|
||||
- Transaction history with memo support, date/amount sorting, and pending-send tracking
|
||||
- Blockchain explorer with live block/transaction search
|
||||
- Blockchain rescan and Sapling witness-rebuild with status-bar progress
|
||||
- Repair Wallet and daemon-binary management in Settings
|
||||
- Built-in console
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragon-2.0.0.AppImage | `34c9ea57ec27bf415e59d2890d995ed9069bfce04d979d7d8d58aa18f4570aa1` |
|
||||
| ObsidianDragon-2.0.0-Linux-x64.zip | `563004b4e45650dbfa9411d62fa1c59a3ca2199bd43a647b1614f2683bbdb5fa` |
|
||||
| ObsidianDragon-2.0.0.exe | `56100ae59dc3c67445d015cac5412ca40465bf459ba182c5ad2477a3b95ff548` |
|
||||
| ObsidianDragon-2.0.0-Windows-x64.zip | `ce9d067f36c5296288a473d3c2cceca7fcf807672a93a2084afc53c3b6e780d9` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.dmg | `8a01b6c0c2b5bebd64a222ba4a5ad04a1b8851138f28458b5c7f767fbb65db66` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.app.zip | `1bce1e5be15d9a2f3c6f42c95b23a0e89df142a73d65895252e4d8f50d19f541` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** Intel & Apple Silicon (universal binary), macOS 10.15 Catalina or later
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -1,167 +0,0 @@
|
||||
# Wallet Loading & Management — Hardening Plan
|
||||
|
||||
Prioritized, grouped remediation for the wallet loading/management audit (33 verified findings +
|
||||
diagnosability QoL). Companion to the findings artifact. Line references are against `dev`.
|
||||
|
||||
- **Provenance:** 7 parallel subsystem finders, each finding adversarially verified against the
|
||||
code; the 3 highest-impact confirmed findings re-checked by hand. 32 confirmed, 1 refuted
|
||||
(W1-5), 1 raised (W5-3 Low→Med).
|
||||
- **Severity:** 8 High · 12 Medium · 13 Low.
|
||||
|
||||
Status legend: ☐ not started · ◐ in progress · ☑ landed & verified
|
||||
|
||||
---
|
||||
|
||||
## Roadmap (ordered by risk; shared fixes grouped)
|
||||
|
||||
| Phase | Findings | Theme | Status |
|
||||
|-------|----------|-------|--------|
|
||||
| **P0-A** | W7-1, W2-1, W4-1, W4-3, W2-3, W4-5, W5-3 ✓ | Secret hardening (console redaction + delete-export + memzero + lite encrypt-at-create) | ☑ 7/7 |
|
||||
| **P0-B** | W2-2/W4-2, W2-4 | Encryption integrity (never silently unencrypted) | ☑ |
|
||||
| **P1-A** | W3-1, W3-2, W3-4, W3-3 ✓ | Migrate-to-seed correctness (fund-adjacent) | ☑ 4/4 (W3-3 pending a live-mainnet run) |
|
||||
| **P1-B** | W1-1, W1-2, W1-3, W1-4 ✓ + startup guard | Missing/wrong wallet-file safety | ☑ |
|
||||
| **P2** | W5-1, W5-2, W6-1, W6-3, W6-2 ✓ | Stale state & lite save-failure surfacing | ☑ 5/5 |
|
||||
| **F** | W7-2, W7-3, W7-4 ✓ · QoL: copy-diag + open-log + node-error-banner + staleness-badge + alert-history ✓ | Diagnostics foundation + QoL bundle | ☑ |
|
||||
|
||||
---
|
||||
|
||||
## P0-A — Secret hardening
|
||||
|
||||
Shared fix: a `SecureString` RAII buffer (zeroes on destruction) retrofitted onto the un-scrubbed
|
||||
key/passphrase paths, plus console redaction and deleting the plaintext export.
|
||||
|
||||
- **W7-1 (High)** `console_tab.cpp:1419` — RPC console echoes/stores/clipboards raw secrets. Fix: an
|
||||
allowlist of secret-bearing first-tokens (`walletpassphrase`, `walletpassphrasechange`,
|
||||
`encryptwallet`, `importprivkey`, `importwallet`, `z_importkey`, `z_importviewingkey`,
|
||||
`signrawtransaction`, `magicrecoverkey`, lite equivalents); echo `> walletpassphrase ****` and
|
||||
keep the raw text out of `command_history_`. Extract a pure `redactConsoleCommand(cmd)` helper for
|
||||
unit testing. **← implementing first (self-contained + testable).**
|
||||
- **W2-1 (High)** `wallet_security_workflow.cpp:66` — delete the `obsidiandecryptexport<ts>` plaintext
|
||||
key dump after `z_importwallet` succeeds (overwrite-then-unlink).
|
||||
- **W4-3 (High)** `app_network.cpp:4481` — `sodium_memzero` the concatenated all-keys string in
|
||||
`exportAllKeys`; write the backup 0600. (Also unify with `ExportAllKeysDialog` — QoL.)
|
||||
- **W4-1 (High)** `app_network.cpp:3801` — zero the key copies in `importPrivateKey`/`sweepPrivateKey`
|
||||
(local + worker-lambda copies).
|
||||
- **W2-3 (Med)** `app_security.cpp:1481` — zero the passphrase threaded through the decrypt lambda chain.
|
||||
- **W4-5 (Med)** `app.cpp:3577` — the seed-backup `.txt` is a permanent predictable cleartext seed;
|
||||
at minimum warn + offer to delete, ideally discourage file save in favor of the on-screen phrase.
|
||||
- **W5-3 (Med)** `lite_wallet_lifecycle_service.cpp:322` — remove the dead `passphrase` field from the
|
||||
lite create/open/restore requests (unused; a secret copied for nothing).
|
||||
|
||||
## P0-B — Encryption integrity
|
||||
|
||||
- **W2-2 / W4-2 (High)** `wallet_security_controller.h:89` — the wizard's deferred encryption is
|
||||
in-memory only and silently lost if the daemon doesn't connect or the app quits/crashes first, so a
|
||||
wallet the user believes is encrypted stays plaintext. Fix: persist a lightweight
|
||||
`encryption_requested_but_incomplete` settings flag (NEVER the passphrase) when
|
||||
`beginDeferredEncryption` is called; surface a persistent warning banner while it's set; clear it
|
||||
only on confirmed `encryptwallet` success; on next connect, if set, re-prompt for the passphrase to
|
||||
complete it.
|
||||
- **W2-4 (Med)** `app_security.cpp:480` — `lockWallet` only sets `locked` on RPC success; log the
|
||||
failure and notify (currently a silent no-op that can leave the wallet unlocked).
|
||||
|
||||
## P1-A — Migrate-to-seed correctness (fund-adjacent; verify carefully)
|
||||
|
||||
- **W3-1 (High)** `app_network.cpp:4327` — adopt hardcodes `datadir + "/wallet.dat"`; use
|
||||
`settings_->getActiveWalletFile()` so migrating a non-default active wallet swaps the right file.
|
||||
- **W3-2 (High)** `seed_wallet_creator.cpp:57` — `remove_all(<config>/seed-migrate)` unconditionally
|
||||
at Phase-1 start; refuse to wipe if a temp `DRAGONX/wallet.dat` already exists (a prior un-adopted
|
||||
swept wallet) and surface it, so swept funds in the temp wallet can't be destroyed by re-entry.
|
||||
- **W3-4 (Med)** `app_network.cpp:1124` — block wallet switching while a migration is *pending*
|
||||
(`getSeedMigrationPending()`), not only while the dialog is open.
|
||||
- **W3-3 (Med)** `app_network.cpp:4231` — persist the sweep opid so an app-close mid-Sweeping can
|
||||
resume/re-poll it instead of silently dropping the txid.
|
||||
|
||||
## P1-B — Missing/wrong wallet-file safety
|
||||
|
||||
- **W1-1 (High)** `app_network.cpp:1109` — `fs::exists()`-check the target wallet file in
|
||||
`switchToWallet()` and before the first daemon launch at startup; if missing, block with an explicit
|
||||
"Wallet file not found — moved or deleted?" dialog (browse / create-new) instead of letting the
|
||||
daemon fabricate an empty wallet.
|
||||
- **W1-3 (Med)** `app_network.cpp:1095` — defer the `syncedHere=true` stamp to the first successful
|
||||
address/balance readback (idHash non-empty), not bare `onConnected()`.
|
||||
- **W1-2 (Med)** `app_network.cpp:198` — split `DB_CORRUPT`-specific strings from the generic "Error
|
||||
loading wallet" fallback; give `DB_TOO_NEW` its own message/action (not a salvage offer).
|
||||
- **W1-4 (Low)** `wallets_dialog.h:393` — re-`fs::exists()` the in-datadir row before switching (match
|
||||
the out-of-datadir path).
|
||||
|
||||
## P2 — State & lite persistence
|
||||
|
||||
- **W6-2 (Med)** `network_refresh_service.cpp:1183` — record a per-field last-success timestamp / a
|
||||
"refresh failed" flag so the UI can show a staleness badge instead of last-good-as-current.
|
||||
- **W5-1 / W5-2 (Med)** `lite_wallet_controller.cpp:78,603` — `liteLog()` the failed save and bubble a
|
||||
one-shot UI warning (both call sites currently discard the bool).
|
||||
- **W6-1 (Med)** `wallet_state.h:313` — reset `mining`/`pool_mining` in `clear()` (or comment why not).
|
||||
- **W6-3 (Low)** `address_book.cpp:46` — per-entry try/catch: skip + count malformed entries instead
|
||||
of discarding the whole list.
|
||||
|
||||
## F — Diagnostics foundation + QoL
|
||||
|
||||
Land W7-2 first — it unblocks the rest.
|
||||
|
||||
- **W7-2 (Med)** `logger.cpp:31` — call `Logger::instance().init(<config>/dragonx-debug.log)` early in
|
||||
`main()` on all platforms; add an "Open log folder" action.
|
||||
- **W7-3 (Med)** `main.cpp:144` — add a `sigaction`-based crash handler writing `dragonx-crash.log` on
|
||||
POSIX (mirror the Windows SEH path).
|
||||
- **W7-4 (Low)** `logger.cpp:39` — size-cap/rotate the log on `init()`.
|
||||
- **QoL** — "Copy diagnostics for support" bundle; persistent alert history; daemon/RPC error banner;
|
||||
refresh-staleness badge; multi-wallet diagnostic panel; refresh-diagnostics panel; structured
|
||||
switch/migration audit logging; restore-from-seed entry point (W4-4, effort L).
|
||||
|
||||
---
|
||||
|
||||
## Progress log
|
||||
|
||||
- **Adversarial review of the 3 diagnostics UI features** — ran a 5-dimension finder → per-finding verify workflow over the node-banner + staleness-badge + alert-history commits (the hand-laid ImGui I couldn't visually verify). 4 confirmed, 1 refuted (banner title never overlaps its button — button is absolutely positioned + title is short), and the dedicated ImGui-stack-balance finder found **no** Push/Pop imbalance. Fixes landed:
|
||||
- **(Med) Alert popup grew off the right edge** — pivot `(0,1)` pinned the panel's *left* edge at the bell (which sits near the window's right edge), so a 320px panel overflowed rightward (an explicit `SetNextWindowPos` pivot skips ImGui's on-screen clamp). Fixed to anchor the bottom-*right* corner at the bell (pivot `(1,1)`, at `bellMax.x`) so it grows left over the canvas.
|
||||
- **(Low) Staleness badge could flash red on reconnect** — `WalletState::clear()` reset everything *except* the four `last_*_update` stamps, so after a reconnect the pre-outage timestamp survived and the badge briefly showed "Updated Nm ago" (red) on the same frame the node banner cleared — the exact contradiction the design forbids. Fixed by zeroing the four stamps in `clear()` (all readers treat 0 as "never"; verified `app_network.cpp:1473` guards on `!= 0`).
|
||||
- **(Low) Banner min-height floor wasn't DPI-scaled** — `std::max(minH, baseH*vScale())` compared a raw-px floor against a scaled value; now `minH * dpiScale()`.
|
||||
- **(Low) New i18n keys weren't in `res/lang/`** — back-filled all 16 diagnostics/QoL keys (this session's node_banner_*/data_stale_*/alerts_*/settings_*/tt_*) into all 8 language files, additively (128 insertions, 0 deletions). zh/ja/ko reworded around 2 glyphs missing from the CJK subset (提醒→通知; ko tooltip avoids 닐) and hard-asserted tofu-free against the subset font.
|
||||
- **Foundation QoL / Persistent alert history** — ☑ landed. Toasts fade in 1–4s; there was no way to review what scrolled past. `Notifications` now retains every pushed alert in a capped (100) ring buffer with a wall-clock epoch (`AlertRecord`) — separate from the 5-item live-toast deque — plus a monotonic `total_pushed_` counter. A bell in the status-bar right cluster (`ICON_MD_NOTIFICATIONS`) opens an upward popup listing recent alerts newest-first with a severity icon/colour (reusing the toast palette), the message, and a relative age (`formatTimeAgoShort`), with a Clear-all action. An **unread dot** on the bell (coloured by the most-severe unseen alert) marks alerts that arrived since the panel was last opened — driven by `totalPushed()` deltas so it survives capping/clearing. Thread-safety: every push is on the UI thread (RPC results run as main-thread `MainCb`s), matching the class's existing lock-free model — documented as a no-raw-worker-thread invariant. Build-clean; `ctest` 1/1 (adds `testNotificationHistory`: retention, order, cap, monotonic counter, clear). **This closes the QoL bundle and the Foundation tier.**
|
||||
- **W6-2 / Refresh-staleness badge** — ☑ landed. The Total Balance card now shows a small pill on its status line ("Updated 2m ago", amber → red past 3 min) **only when connected but the balance stopped refreshing** — a busy daemon can fail `z_gettotalbalance` without dropping the whole connection (only *both* core RPCs failing 3× triggers a disconnect), leaving stale numbers on screen while the node-status banner stays hidden. No refresh-path changes were needed: `WalletState::last_balance_update` is already stamped only on a successful fetch (`network_refresh_service.cpp:1187`), so the badge just reads it and computes age against the same `std::time` clock (`util::formatTimeAgoShort`). Decision is a pure, unit-tested helper (`ui/staleness_badge.h::evaluateStalenessBadge`, thresholds 45s/180s) gated on `connected` so it never contradicts the banner; hover shows a "may be out of date — check your node connection" tooltip. Build-clean; `ctest` 1/1 (adds `testStalenessBadge`). **This closes P2 (5/5).**
|
||||
- **Foundation QoL / Persistent node-status banner** — ☑ landed. A persistent horizontal strip now sits at the top of the content column whenever the wallet can't reach its node — distinct from the transient toasts, so an offline wallet is never silently mistaken for a working one. The show/severity/action decision is a pure function (`ui/node_status_banner.h` → `evaluateNodeStatusBanner`, unit-tested) fed a state snapshot by `App::renderNodeStatusBanner()`. Three cases: **full-node offline** (amber, "Reconnect" → `tryConnect`), **embedded daemon crashed & auto-restart gave up** (red, "Restart node" → `restartDaemon`), **lite wallet failed to open** (red, message-only). Suppressed during the wizard / wallet-switch / daemon-restart / screenshot-sweep / shutdown, and while an expected startup phase (warmup/init/connect-in-progress) already owns the screen. Height in `res/themes/ui.toml` (`banners.node-status`); colours from the material semantic palette; detail text ellipsis-clipped so it can't shove the action button off-screen. Build-clean; `ctest` 1/1 (added `testNodeStatusBanner`). **Remaining QoL:** persistent alert history, and the W6-2 refresh-staleness badge.
|
||||
- **Foundation QoL / "Copy diagnostics" + "Open log folder"** — ☑ landed: Settings (logging section) now has two actions. **Open log folder** opens the config dir (`Platform::openFolder`) so users can actually find `dragonx-debug.log`/`dragonx-crash.log`. **Copy diagnostics** copies a plaintext support snapshot to the clipboard via the new `App::buildDiagnosticsReport()` — version, build variant, platform, connection status, active wallet path + existence + size, encryption/lock state, sync heights, daemon status/running/crash-count/lastError (full-node), and the log paths. No secrets. Build-clean; `ctest` 1/1. **Remaining QoL:** persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge.
|
||||
- **Foundation / W7-2 · W7-3 · W7-4 (diagnostics infrastructure)** — ☑ landed (answers the original "easier to diagnose" ask — the logging/crash foundation now actually works):
|
||||
- **W7-2 (Med, keystone):** the app-level `Logger` file sink was never initialized, so `LOG`/`LOGF`/`VERBOSE_LOGF` went nowhere and `dragonx-debug.log` didn't exist on Linux/macOS at all. `main()` now calls `Logger::init(<config>/dragonx-debug.log)` on all platforms. Also fixed a **latent deadlock** this exposed: `init()` wrote its banner via `write()`, which re-locks the non-recursive `mutex_` it already holds — now written directly. On Windows the raw stdout/stderr `freopen` was moved to a separate `dragonx-stdout.log` so the two writers don't contend. New `testLoggerFileSink` (also a deadlock guard — it would hang if that regressed).
|
||||
- **W7-3 (Med):** no crash handler existed on Linux/macOS. Added an **async-signal-safe** `sigaction` handler (SIGSEGV/ABRT/BUS/FPE/ILL) that writes a signal id + `backtrace_symbols_fd` backtrace to `dragonx-crash.log`, then re-raises the default disposition for a core dump — the POSIX counterpart of the Windows SEH filter.
|
||||
- **W7-4 (Low):** `Logger::init` now rotates the log to a single `.1` backup when it exceeds 10 MB, so a long/verbose session can't grow it unbounded.
|
||||
Build-clean; `ctest` 1/1. **Remaining Foundation:** the QoL bundle (mostly UI) — "copy diagnostics for support", an "open log folder" action, persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge.
|
||||
- **P2 / W5-1 · W5-2 · W6-1 · W6-3 (localized batch)** — ☑ landed:
|
||||
- **W5-1 (Med):** `persistAfterBroadcast` (lite send/shield save) returned false on a persistent save failure but both callers discarded it and it never logged — completely silent. It now `liteLog`s the failure (the note re-derives on next sync, so it's a robustness gap, not fund loss).
|
||||
- **W5-2 (Med):** the post-**sync** and post-**rescan** `save` results (in the detached scan threads) were ignored; both now `liteLog` on failure (`LiteDiagnostics::log` is mutex-guarded, safe from those threads).
|
||||
- **W6-1 (Med):** `WalletState::clear()` didn't reset `mining`/`pool_mining`, so a wallet switch could briefly show the previous wallet's hashrate/blocks. Now reset in `clear()` (the daemon restarts on switch, so mining genuinely stops).
|
||||
- **W6-3 (Low):** `AddressBook::load()` did `entries_.clear()` then threw on the first non-object element — discarding **every** contact. Now it guards `is_object()` + per-entry try/catch, skipping and counting malformed entries.
|
||||
Build-clean; `ctest` 1/1. **Remaining P2:** W6-2 (surface refresh staleness — the timestamps exist in `WalletState`; this needs the UI "updated Xs ago" badge, which overlaps the diagnostics/QoL Foundation bundle).
|
||||
- **P1-B / W1-3 + startup wallet-existence guard** — ☑ landed:
|
||||
- **W1-3 (Med):** `syncedHere` was stamped in the `markOpened` block at bare connect (idHash still empty), letting a freshly-restored wallet skip its needed rescan. It's now stamped only once the identity is verified (idHash non-empty), so it takes effect at the post-address-refresh index update (`updateWalletIndexForActiveWallet` after addresses load), while `lastOpenedEpoch` still records at open.
|
||||
- **Startup guard (the W1-1 launch counterpart):** `App::init` now `exists()`-checks the recorded active wallet before the daemon is configured; a **non-default** active wallet that was moved/deleted between sessions falls back to the default `wallet.dat` with a warning, instead of the daemon silently auto-creating an empty wallet under the missing name. Runs before the PIN-vault init so the vault is scoped to the wallet actually opened.
|
||||
Build-clean; `ctest` 1/1.
|
||||
- **P1-A / W3-3 (sweep opid persistence)** — ☑ **implemented + two rounds of adversarial review** (the "live mainnet run" the migration code mandates is the remaining gate — see below). The deferral's core fear (re-tracking a stale opid hangs forever) was **refuted by the code**: the opid poller (`app.cpp:1122`) + `parseOperationStatusPoll` classify a tracked opid absent from a *successful* `z_getoperationstatus` as stale, remove it, and fire the callback `ok=false` — a thrown RPC aborts the poll so there's never a *false* stale. So re-tracking yields at worst one clean failure, never a hang.
|
||||
- **What landed:** a persisted `seed_migration_sweep_opid` setting; the opid is adopted **atomically** with clearing any prior txid in the *same* `settings.save()` **only once the submit succeeds** (torn-write safe; txid always outranks opid on resume). Resume routing is a pure, unit-tested helper (`data/seed_migration_resume.h::decideSeedMigrationResume`): txid → Confirming; opid **and connected** → re-track (`Sweeping`); otherwise → the dismissable Sweep gate. The shared `makeSweepCompletionCallback(resumed)`: success → Confirming; resumed-stale → Sweep gate (re-fetch balance, honest "may have already completed" copy); fresh-fail → Error.
|
||||
- **Round 1 (design review, 4 skeptics)** confirmed both safety facts (no fund loss — adopt gate + never-deleted `.bak` untouched; no hang) and caught 3 real resume-UX traps, all fixed: a missing **connectivity gate** (would trap the user in the buttonless `Sweeping` spinner while offline), a **missing balance re-fetch** on the stale fallback (permanent "Checking balance…"), and honest messaging since a daemon restart makes even a *successful* sweep read "stale".
|
||||
- **Round 2 (implementation review, 3 reviewers)** caught one regression — clearing the old txid at sweep *entry* would forget an already-mined first sweep if a remainder re-sweep's submit failed; fixed by the atomic-on-success swap above. All other fixes verified present + correct.
|
||||
- **⚑ Remaining gate — live mainnet run (user):** per CLAUDE.md this fund-moving path must be exercised once on mainnet before it ships. The self-verifiable parts (build, unit test, both review rounds) are green; a real interrupted-sweep resume on mainnet is the human gate I cannot perform.
|
||||
- **P1-B / W1-1 (+ W1-4) · W1-2 (wallet-file safety)** — ☑ landed:
|
||||
- **W1-1 (High):** `switchToWallet` never checked the target wallet file exists, so a moved/deleted file "opened" as a fresh empty wallet (dragonxd auto-creates for a missing `-wallet=`), looking exactly like fund loss. It now `std::filesystem::exists`-checks `datadir + "/" + walletFile` before switching and blocks with a "not found (moved or deleted?)" warning. Placed before the daemon-stop prompt, and — since the check runs no matter how `switchToWallet` is invoked — it also **closes W1-4** (the stale-switcher-row TOCTOU).
|
||||
- **W1-2 (Med):** `walletOutputLooksCorrupt` matched the generic "Error loading wallet" string, so a `DB_TOO_NEW` (newer-version) wallet was offered a `-salvagewallet` repair that can't fix it. Now the generic match is excluded when the output also contains "newer version".
|
||||
Build-clean; `ctest` 1/1. **Remaining P1-B:** W1-3 (defer the `syncedHere` stamp to a verified readback) + the startup-path existence check (`app.cpp` hands `getActiveWalletFile()` to the daemon with no `exists()` check — same silent-empty-wallet risk as W1-1 but at launch).
|
||||
- **P1-A / W3-1 · W3-2 · W3-4 (migrate-to-seed correctness)** — ☑ landed (fund-adjacent — reviewed carefully):
|
||||
- **W3-1 (High):** `beginAdoptSeedWallet` hardcoded `datadir + "/wallet.dat"` as the file to swap. With a non-default active wallet (e.g. `wallet-2.dat`), that installed the swept seed wallet into an unloaded `wallet.dat` and left the daemon reloading the emptied legacy — funds only recoverable via the seed phrase. Now swaps `datadir + "/" + getActiveWalletFile()` (captured on the main thread; switching is blocked during migration so it can't race).
|
||||
- **W3-2 (High):** `SeedWalletCreator::create` did `remove_all(<config>/seed-migrate)` unconditionally at the start. A prior migration that swept funds into the temp wallet but was abandoned/crashed before adopting would have that fund-bearing wallet destroyed. It now refuses (with a clear message) when `DRAGONX/wallet.dat` already exists — a completed migration removes the dir on adopt, so a leftover means an unfinished one.
|
||||
- **W3-4 (Med):** `switchToWallet` only blocked switching while the migration *dialog* was open; closing it via "Later" mid-migration dropped the guard. Now also blocks while `getSeedMigrationPending()`.
|
||||
Build-clean; `ctest` 1/1. **Remaining P1-A:** W3-3 (persist the sweep opid so an app-close mid-sweep can resume/re-poll instead of silently dropping the txid).
|
||||
|
||||
- **P0-B / W2-2 (deferred encryption silently lost) + W2-4 (auto-lock silent-fail)** — ☑ landed:
|
||||
- **W2-2:** the wizard's deferred encryption was stored only in memory, so a quit/crash or a failed daemon connect before it applied left the wallet unencrypted with **no record it was ever requested** — the user believing it was encrypted. Now a persisted `encryption_pending` settings flag is set the moment encryption is requested (**never the passphrase** — only the fact). `refreshWalletEncryptionState()` reconciles it on every connect: wallet observed **encrypted** → clear the flag; wallet **not** encrypted while the flag is set and no deferred encryption is pending/in-flight → a once-per-session **"your wallet is NOT encrypted — open Settings to finish"** warning (the flag stays set, so it recurs each launch until resolved). We deliberately don't persist the passphrase to auto-complete — surfacing it is the secure choice.
|
||||
- **W2-4:** `lockWallet()`'s continuation only handled success — a failed `walletlock` silently left the wallet **unlocked** (an unfulfilled auto-lock). It now logs and warns once (reset on the next successful lock), so a failing auto-lock is visible instead of leaving the wallet exposed.
|
||||
Touches `settings.{h,cpp}`, `app_wizard.cpp`, `app_security.cpp`, `app.h`. Not unit-testable at this layer (RPC/connect-driven state machine); build-clean, `ctest` 1/1.
|
||||
|
||||
- **P0-A / W5-3 (lite create-time passphrase)** — ☑ landed (chose option **(b) wire it up**). The lite create/open/restore passphrase was collected but never consumed by the backend — a "passphrase" field that did nothing. It now has a real meaning for all three operations, in `LiteWalletController`: **create/restore** → `encryptWallet(passphrase)` (the backend encrypts + locks + saves the brand-new wallet); **open** → `unlockWallet(passphrase)`, but only when `encryptionStatus()` reports the existing wallet is actually encrypted+locked (skips a spurious unlock otherwise). Encrypt/unlock take their own copy and wipe it; a post-create encrypt failure is `liteLog`'d (the wallet still exists — the create isn't failed). Six existing lite-controller tests carried an incidental `hunter2` create passphrase from the dead-field era; removed (they test non-encryption flows and want an unencrypted wallet), and added `testLiteWalletControllerCreateEncryptsWithPassphrase` to prove the new behavior. Build-clean; `ctest` 1/1. *(Follow-up UX polish: `settings_page` could show the passphrase field's meaning per operation — "encrypt" for create/restore vs "unlock" for open.)*
|
||||
- **P0-A / W4-5 (seed-backup file)** — ☑ landed (proportionate): the seed "Save" already wrote 0600 + zeroed the in-memory buffer, but the success message was a bare "Saved to <path>". It now reads "**Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this copy**: <path>", so the plaintext-on-disk risk is called out. `i18n.cpp` (English source; `res/lang` back-fill of this changed key is deferred to the batch i18n pass). A stronger fix (pre-save confirmation, or dropping the file-save in favor of on-screen + Copy) is a follow-up UX decision.
|
||||
- **P0-A / W4-1 · W4-3 · W2-3 (memzero cluster)** — ☑ landed, using the file's established `sodium_memzero` pattern (matching the existing lambda-capture scrub at app_network.cpp:2885 and JSON scrub at :4025) rather than a new type, since this is fund-moving code:
|
||||
- **W4-1** `importPrivateKey`/`sweepPrivateKey`: the spending/viewing key is now scrubbed on all paths — the calling-frame copy (after the worker post), the worker-lambda's captured copy (lambda made `mutable`, zeroed after the request is sent), and the JSON request `params` copy.
|
||||
- **W4-3** `exportAllKeys`/`backupWallet`: the concatenated all-keys buffer is zeroed after the consumer uses it, and the backup file is now written via `Platform::writeFileAtomically(..., restrictPermissions=true)` (atomic + 0600) instead of a umask-default `ofstream`.
|
||||
- **W2-3** decrypt-wallet passphrase: `std::move`-captured into the worker lambda (so no plaintext copy is left in the calling frame) and `sodium_memzero`'d right after `unlockWallet` (its only use).
|
||||
Not unit-testable (the scrubbing has no observable RPC effect — the key value sent to the daemon is unchanged; only post-use memory zeroing is added). Build-clean; `ctest` 1/1 (no regression). **Remaining in P0-A:** W5-3 (remove the dead lite `passphrase` field), W4-5 (predictable plaintext seed-backup file).
|
||||
- **P0-A / W2-1** — ☑ landed: the decrypt-wallet flow now scrubs (best-effort in-place zero-overwrite) and removes the plaintext key export (`obsidiandecryptexport…`) as soon as the `z_importwallet` attempt resolves — success or failure — so a full cleartext dump of every private key is no longer left on disk forever. Recovery remains the encrypted backup (`wallet.dat.encrypted.bak`). `app_security.cpp` (after the import call). Not unit-testable (fs I/O in a deep lambda); build-clean, `ctest` 1/1 (no regression).
|
||||
- **P0-A / W7-1** — ☑ landed: `RedactConsoleCommand`/`ConsoleCommandCarriesSecret` in `console_tab_helpers` redact secret-bearing commands (an allowlist of 13 first-tokens: `walletpassphrase`, `encryptwallet`, `z_importkey`, …) to `> walletpassphrase ****` before they hit the console echo AND the recall history; the real command still executes unredacted. Wired into `submitConsoleCommand` (`console_tab.cpp`). New `testConsoleSecretRedaction` (11 assertions). Clean build; `ctest` 1/1. (Output-secret commands like `z_exportkey` — result redaction — remain a follow-up.)
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "Hilfe & FAQ",
|
||||
"faq_search_hint": "FAQ durchsuchen…",
|
||||
"faq_title": "Hilfe & FAQ",
|
||||
"faq_w_addr_1_a": "Transparente Adressen (sie beginnen mit R oder t) funktionieren wie bei den meisten Coins: Beträge und Adressen sind öffentlich auf der Blockchain einsehbar.\n\nGeschützte Adressen (z-Adressen, beginnend mit zs) nutzen Zero-Knowledge-Kryptografie — Betrag, Absender und Empfänger sind on-chain verschlüsselt. Guthaben auf geschützten Adressen sind privat.",
|
||||
"faq_w_addr_1_a": "Abgeschirmte Adressen (z-Adressen, beginnend mit zs) sind die Adressen, die Sie bei DragonX verwenden. Sie nutzen Zero-Knowledge-Kryptografie: Betrag, Absender und Empfänger werden on-chain verschlüsselt, und Ihr Guthaben bleibt privat.\n\nTransparente Adressen (beginnend mit R) sind Mining-Adressen. DragonX ist eine vollständig private Chain, daher können sie nur Mining-Belohnungen empfangen – das Netzwerk lehnt jede an sie gesendete Zahlung ab. Dort eingehende Belohnungen sind öffentlich, bis sie abgeschirmt werden, was die Wallet automatisch erledigt.",
|
||||
"faq_w_addr_1_q": "Was ist der Unterschied zwischen transparenten und geschützten Adressen?",
|
||||
"faq_w_addr_2_a": "Bevorzugen Sie wann immer möglich geschützte (z-)Adressen — sie halten Ihr Guthaben und Ihren Zahlungsverlauf privat. DragonX ist eine Privacy-Coin, und geschützt ist die Standardeinstellung für empfangene Gelder.\n\nVerwenden Sie eine transparente Adresse nur, wenn ein Dienst, mit dem Sie interagieren, keine geschützten Adressen verarbeiten kann.",
|
||||
"faq_w_addr_2_a": "Verwenden Sie für alles, was Sie senden und empfangen, eine abgeschirmte (zs-)Adresse – bei DragonX ist sie die einzige Art, an die gezahlt werden kann.\n\nTransparente (Mining-)Adressen sind nur für Solo-Mining-Belohnungen da. Geben Sie nie eine für eine Zahlung heraus: Das Netzwerk würde sie ablehnen.",
|
||||
"faq_w_addr_2_q": "Welchen Adresstyp sollte ich verwenden?",
|
||||
"faq_w_addr_3_a": "Bei geschützten Adressen ist die Wiederverwendung unbedenklich und gibt keinen Verlauf preis. Bei transparenten Adressen verbessert eine neue Adresse pro Zahlung die Privatsphäre. Neue Adressen erstellen Sie im Tab Empfangen.",
|
||||
"faq_w_addr_3_a": "Eine abgeschirmte Adresse wiederzuverwenden ist unbedenklich und verrät nichts über Ihren Verlauf. Sie können trotzdem im Tab „Empfangen“ neue anlegen, zum Beispiel eine pro Kontakt, um zu sehen, wer Sie bezahlt hat. Mining-Adressen sind nur für Mining-Belohnungen da; eine neue brauchen Sie selten.",
|
||||
"faq_w_addr_3_q": "Sollte ich eine Adresse wiederverwenden oder eine neue erstellen?",
|
||||
"faq_w_addr_4_a": "Beim Schützen werden Coins von einer transparenten Adresse in eine geschützte verschoben und dadurch privat gemacht. Neu geschürfte Coins kommen transparent an und werden automatisch für Sie geschützt.\n\nSie können auch manuell aus der Wallet heraus schützen; ein Schutzvorgang ist einfach eine spezielle Transaktion und benötigt daher die normale Bestätigungszeit.",
|
||||
"faq_w_addr_4_a": "Beim Abschirmen werden Coins von einer transparenten (Mining-)Adresse auf eine abgeschirmte verschoben und damit privat. Neu geminte Coins kommen auf einer Mining-Adresse an und werden automatisch abgeschirmt, sobald sie gereift sind.\n\nSie können auch manuell in der Wallet abschirmen; eine Abschirmung ist nur eine besondere Transaktion und braucht daher die übliche Bestätigungszeit.",
|
||||
"faq_w_addr_4_q": "Was bedeutet „Schützen“?",
|
||||
"faq_w_addr_title": "Adressen & Privatsphäre",
|
||||
"faq_w_bal_1_a": "Solange Ihre Wallet noch synchronisiert, ist Ihr Guthaben unvollständig und kann 0 anzeigen — es wurden noch nicht alle Transaktionen gescannt. Es füllt sich, sobald die Synchronisierung abgeschlossen ist.\n\nBeobachten Sie die Statusleiste unten: Sie zeigt den Verbindungsstatus und die Blockhöhe an. Sobald die Synchronisierung abgeschlossen ist, ist Ihr Guthaben korrekt.",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "Ihre Seed-Phrase ist die primäre Sicherung und reicht aus, um alles wiederherzustellen. Eine Kopie Ihrer Wallet-Datei ist eine praktische zusätzliche Sicherung, die außerdem Bezeichnungen und Einstellungen erhält. Bewahren Sie jede Sicherung offline und privat auf.",
|
||||
"faq_w_seed_4_q": "Sollte ich auch wallet.dat sichern?",
|
||||
"faq_w_seed_title": "Seed-Phrase & Sicherung",
|
||||
"faq_w_send_1_a": "Öffnen Sie den Tab Senden, fügen Sie die Adresse des Empfängers ein, geben Sie einen Betrag ein und bestätigen Sie. Wenn die Wallet verschlüsselt ist, werden Sie gebeten, sie für die Transaktion zu entsperren.\n\nSendungen an geschützte Adressen sind privat; Sendungen an transparente Adressen sind öffentlich.",
|
||||
"faq_w_send_1_a": "Öffnen Sie den Tab Senden, fügen Sie die Adresse des Empfängers ein, geben Sie einen Betrag ein und bestätigen Sie. Wenn die Wallet verschlüsselt ist, werden Sie gebeten, sie für die Transaktion zu entsperren.\n\nZahlungen gehen an abgeschirmte (zs-)Adressen und bleiben privat. Die Wallet sendet nicht an eine transparente (Mining-)Adresse: Das Netzwerk lehnt Zahlungen an sie ab.",
|
||||
"faq_w_send_1_q": "Wie sende ich Gelder?",
|
||||
"faq_w_send_2_a": "DragonX-Gebühren sind sehr niedrig und werden automatisch festgelegt. Die Gebühr wird angezeigt, bevor Sie bestätigen. Normalerweise müssen Sie sie nicht ändern.",
|
||||
"faq_w_send_2_q": "Welche Gebühr zahle ich?",
|
||||
"faq_w_send_3_a": "Ja — beim Senden an eine geschützte (z-)Adresse können Sie ein verschlüsseltes Memo anhängen. Nur der Empfänger kann es lesen. Transparente Adressen unterstützen keine Memos.",
|
||||
"faq_w_send_3_a": "Ja — beim Senden an eine geschützte (z-)Adresse können Sie ein verschlüsseltes Memo anhängen. Nur der Empfänger kann es lesen.",
|
||||
"faq_w_send_3_q": "Kann ich einer Zahlung eine Nachricht beifügen?",
|
||||
"faq_w_send_4_a": "Eine Transaktion ist „ausstehend“, bis sie in einen Block aufgenommen wird und Bestätigungen erhält. Das dauert normalerweise ein bis zwei Minuten. Geschützte Transaktionen erfordern außerdem, dass Ihre Wallet synchronisiert ist, um den Beweis zu erstellen.\n\nBleibt eine Sendung ungewöhnlich lange ausstehend, prüfen Sie, ob Ihre Wallet verbunden und synchronisiert ist (siehe die Statusleiste unten im Fenster).",
|
||||
"faq_w_send_4_q": "Ich habe eine Zahlung gesendet, aber sie zeigt „ausstehend“ — warum?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "Miner-Gebühr",
|
||||
"mining": "Mining",
|
||||
"mining_active": "Aktiv",
|
||||
"mining_addr_explainer": "Empfängt nur Solo-Mining-Belohnungen – das Netzwerk lehnt Zahlungen an diese Adresse ab. Belohnungen werden automatisch abgeschirmt, sobald sie gereift sind.",
|
||||
"mining_address": "Mining-Adresse",
|
||||
"mining_address_copied": "Mining-Adresse kopiert",
|
||||
"mining_all_time": "Gesamt",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "Wallet: %.1f GB",
|
||||
"ram_wallet_mb": "Wallet: %.0f MB",
|
||||
"receive": "Empfangen",
|
||||
"receive_not_for_payments": "Nicht für Zahlungen",
|
||||
"receive_use_shielded": "Abgeschirmte Adresse verwenden",
|
||||
"received": "empfangen",
|
||||
"received_filter": "Empfangen",
|
||||
"received_label": "Empfangen",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "Adresse auswählen...",
|
||||
"request_shielded_addrs": "-- Abgeschirmte Adressen --",
|
||||
"request_title": "Zahlung anfordern",
|
||||
"request_transparent_addrs": "-- Transparente Adressen --",
|
||||
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
|
||||
"rescan": "Neu scannen",
|
||||
"rescan_bootstrapped_msg": "Ihr Node wurde per Bootstrap eingerichtet, daher liegen Blöcke unterhalb des Snapshots nicht auf der Festplatte und ein Neuscan ab dem Genesis-Block würde fehlschlagen. Führen Sie den Neuscan ab einer Höhe durch, die Ihr Snapshot umfasst, um das ausgegebene Guthaben Ihrer Wallet abzugleichen. Ihre wallet.dat und Ihre Blockchain-Daten werden nicht gelöscht.",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "Geben Sie eine DragonX-Adresse ein, um zu prüfen, ob sie gültig ist und ob sie zu dieser Wallet gehört.",
|
||||
"validate_invalid": "UNGÜLTIG",
|
||||
"validate_is_mine": "Diese Wallet besitzt diese Adresse",
|
||||
"validate_mining_type": "Mining-Adresse (transparent)",
|
||||
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
||||
"validate_ownership": "Eigentum:",
|
||||
"validate_results": "Ergebnisse:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
||||
"validate_status": "Status:",
|
||||
"validate_title": "Adresse validieren",
|
||||
"validate_transparent_type": "Transparent (t-Adresse)",
|
||||
"validate_type": "Typ:",
|
||||
"validate_valid": "GÜLTIG",
|
||||
"validating": "Validiere...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "Ayuda y preguntas frecuentes",
|
||||
"faq_search_hint": "Buscar en las preguntas frecuentes…",
|
||||
"faq_title": "Ayuda y preguntas frecuentes",
|
||||
"faq_w_addr_1_a": "Las direcciones transparentes (empiezan con R o t) funcionan como la mayoría de las monedas: los montos y las direcciones son públicos en la cadena de bloques.\n\nLas direcciones blindadas (direcciones z, empiezan con zs) usan criptografía de conocimiento cero: el monto, el remitente y el destinatario quedan cifrados en la cadena. Los saldos en direcciones blindadas son privados.",
|
||||
"faq_w_addr_1_a": "Las direcciones blindadas (direcciones z, que empiezan por zs) son las que usas en DragonX. Utilizan criptografía de conocimiento cero, así que el importe, el remitente y el destinatario se cifran en la cadena y tu saldo sigue siendo privado.\n\nLas direcciones transparentes (que empiezan por R) son direcciones de minería. DragonX es una cadena totalmente privada, por lo que solo pueden recibir recompensas de minería; la red rechaza cualquier pago enviado a ellas. Las recompensas que llegan allí son públicas hasta que se blindan, algo que la cartera hace automáticamente.",
|
||||
"faq_w_addr_1_q": "¿Cuál es la diferencia entre direcciones transparentes y blindadas?",
|
||||
"faq_w_addr_2_a": "Prefiere las direcciones blindadas (z) siempre que sea posible: mantienen privados tu saldo y tu historial de pagos. DragonX es una moneda de privacidad y lo blindado es la opción predeterminada para los fondos recibidos.\n\nUsa una dirección transparente solo cuando un servicio con el que interactúas no admita direcciones blindadas.",
|
||||
"faq_w_addr_2_a": "Usa una dirección blindada (zs) para todo lo que envíes y recibas; en DragonX es el único tipo al que se puede pagar.\n\nLas direcciones transparentes (de minería) son solo para recompensas de minería en solitario. Nunca des una para recibir un pago: la red lo rechazará.",
|
||||
"faq_w_addr_2_q": "¿Qué tipo de dirección debería usar?",
|
||||
"faq_w_addr_3_a": "Para las direcciones blindadas, reutilizarlas está bien y no filtra el historial. Para las direcciones transparentes, usar una dirección nueva por cada pago mejora la privacidad. Crea nuevas direcciones desde la pestaña Recibir.",
|
||||
"faq_w_addr_3_a": "Reutilizar una dirección blindada no es un problema y no revela tu historial. Aun así puedes crear otras nuevas en la pestaña Recibir, por ejemplo una por contacto para saber quién te ha pagado. Las direcciones de minería son solo para recompensas de minería; rara vez necesitarás una nueva.",
|
||||
"faq_w_addr_3_q": "¿Debería reutilizar una dirección o crear una nueva?",
|
||||
"faq_w_addr_4_a": "Blindar mueve monedas de una dirección transparente a una blindada, haciéndolas privadas. Las monedas recién minadas llegan transparentes y se blindan automáticamente por ti.\n\nTambién puedes blindar manualmente desde la cartera; blindar es solo una transacción especial, así que tarda el tiempo de confirmación normal en completarse.",
|
||||
"faq_w_addr_4_a": "Blindar mueve monedas de una dirección transparente (de minería) a una blindada, haciéndolas privadas. Las monedas recién minadas llegan a una dirección de minería y se blindan automáticamente cuando maduran.\n\nTambién puedes blindar manualmente desde la cartera; un blindaje es solo una transacción especial, así que tarda el tiempo normal de confirmación.",
|
||||
"faq_w_addr_4_q": "¿Qué es \"blindar\"?",
|
||||
"faq_w_addr_title": "Direcciones y privacidad",
|
||||
"faq_w_bal_1_a": "Mientras tu cartera aún se está sincronizando, tu saldo está incompleto y puede mostrar 0 — todavía no ha escaneado todas tus transacciones. Se completa cuando termina la sincronización.\n\nObserva la barra de estado en la parte inferior: muestra el estado de la conexión y la altura de bloque. Una vez que la sincronización termina, tu saldo es exacto.",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "Tu frase de recuperación es el respaldo principal y basta para restaurarlo todo. Una copia de tu archivo de cartera es un respaldo secundario práctico que además conserva las etiquetas y la configuración. Mantén cualquier respaldo sin conexión y privado.",
|
||||
"faq_w_seed_4_q": "¿Debería respaldar también wallet.dat?",
|
||||
"faq_w_seed_title": "Frase de recuperación y copia de seguridad",
|
||||
"faq_w_send_1_a": "Abre la pestaña Enviar, pega la dirección del destinatario, ingresa un monto y confirma. Si la cartera está cifrada, se te pedirá desbloquearla para la transacción.\n\nLos envíos a direcciones blindadas son privados; los envíos a direcciones transparentes son públicos.",
|
||||
"faq_w_send_1_a": "Abre la pestaña Enviar, pega la dirección del destinatario, ingresa un monto y confirma. Si la cartera está cifrada, se te pedirá desbloquearla para la transacción.\n\nLos pagos van a direcciones blindadas (zs) y son privados. La cartera no envía a una dirección transparente (de minería): la red rechaza los pagos a ellas.",
|
||||
"faq_w_send_1_q": "¿Cómo envío fondos?",
|
||||
"faq_w_send_2_a": "Las comisiones de DragonX son muy bajas y se establecen automáticamente. La comisión se muestra antes de confirmar. Normalmente no necesitas cambiarla.",
|
||||
"faq_w_send_2_q": "¿Qué comisión pago?",
|
||||
"faq_w_send_3_a": "Sí — al enviar a una dirección blindada (z) puedes incluir un memo cifrado. Solo el destinatario puede leerlo. Las direcciones transparentes no admiten memos.",
|
||||
"faq_w_send_3_a": "Sí — al enviar a una dirección blindada (z) puedes incluir un memo cifrado. Solo el destinatario puede leerlo.",
|
||||
"faq_w_send_3_q": "¿Puedo adjuntar un mensaje a un pago?",
|
||||
"faq_w_send_4_a": "Una transacción está \"pendiente\" hasta que se mina en un bloque y obtiene confirmaciones. Esto suele tardar uno o dos minutos. Las transacciones blindadas también necesitan que tu cartera esté sincronizada para construir la prueba.\n\nSi un envío permanece pendiente durante un tiempo inusualmente largo, verifica que tu cartera esté conectada y sincronizada (mira la barra de estado en la parte inferior de la ventana).",
|
||||
"faq_w_send_4_q": "Envié un pago pero dice pendiente — ¿por qué?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "Tarifa de minero",
|
||||
"mining": "Minería",
|
||||
"mining_active": "Activo",
|
||||
"mining_addr_explainer": "Solo recibe recompensas de minería en solitario; la red rechaza los pagos a esta dirección. Las recompensas se blindan automáticamente cuando maduran.",
|
||||
"mining_address": "Dirección de minería",
|
||||
"mining_address_copied": "Dirección de minería copiada",
|
||||
"mining_all_time": "Todo el Tiempo",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "Billetera: %.1f GB",
|
||||
"ram_wallet_mb": "Billetera: %.0f MB",
|
||||
"receive": "Recibir",
|
||||
"receive_not_for_payments": "No para pagos",
|
||||
"receive_use_shielded": "Usar una dirección blindada",
|
||||
"received": "recibido",
|
||||
"received_filter": "Recibido",
|
||||
"received_label": "Recibido",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "Seleccionar dirección...",
|
||||
"request_shielded_addrs": "-- Direcciones Protegidas --",
|
||||
"request_title": "Solicitar Pago",
|
||||
"request_transparent_addrs": "-- Direcciones Transparentes --",
|
||||
"request_uri_copied": "URI de pago copiada al portapapeles",
|
||||
"rescan": "Re-escanear",
|
||||
"rescan_bootstrapped_msg": "Tu nodo se inicializó con un bootstrap, por lo que los bloques anteriores a la instantánea no están en disco y un reescaneo desde el génesis fallaría. Reescanea desde una altura que incluya tu instantánea para reconciliar el saldo gastado de tu cartera. Tu wallet.dat y los datos de la cadena no se eliminan.",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "Ingresa una dirección DragonX para verificar si es válida y si pertenece a esta cartera.",
|
||||
"validate_invalid": "INVÁLIDA",
|
||||
"validate_is_mine": "Esta cartera es dueña de esta dirección",
|
||||
"validate_mining_type": "Dirección de minería (transparente)",
|
||||
"validate_not_mine": "No es propiedad de esta cartera",
|
||||
"validate_ownership": "Propiedad:",
|
||||
"validate_results": "Resultados:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "Protegida (dirección z)",
|
||||
"validate_status": "Estado:",
|
||||
"validate_title": "Validar Dirección",
|
||||
"validate_transparent_type": "Transparente (dirección t)",
|
||||
"validate_type": "Tipo:",
|
||||
"validate_valid": "VÁLIDA",
|
||||
"validating": "Validando...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "Aide et FAQ",
|
||||
"faq_search_hint": "Rechercher dans la FAQ…",
|
||||
"faq_title": "Aide et FAQ",
|
||||
"faq_w_addr_1_a": "Les adresses transparentes (elles commencent par R ou t) fonctionnent comme la plupart des cryptomonnaies : les montants et adresses sont publics sur la blockchain.\n\nLes adresses blindées (adresses Z, commençant par zs) utilisent la cryptographie à divulgation nulle de connaissance — le montant, l'expéditeur et le destinataire sont chiffrés sur la chaîne. Les soldes détenus sur des adresses blindées sont privés.",
|
||||
"faq_w_addr_1_a": "Les adresses blindées (adresses z, commençant par zs) sont celles que vous utilisez sur DragonX. Elles reposent sur une cryptographie à divulgation nulle de connaissance : le montant, l'expéditeur et le destinataire sont chiffrés sur la blockchain, et votre solde reste privé.\n\nLes adresses transparentes (commençant par R) sont des adresses de minage. DragonX est une blockchain entièrement privée : elles ne peuvent recevoir que des récompenses de minage — le réseau refuse tout paiement qui leur est envoyé. Les récompenses qui y arrivent sont publiques jusqu'à ce qu'elles soient blindées, ce que le portefeuille fait automatiquement.",
|
||||
"faq_w_addr_1_q": "Quelle est la différence entre les adresses transparentes et blindées ?",
|
||||
"faq_w_addr_2_a": "Privilégiez les adresses blindées (Z) autant que possible — elles préservent la confidentialité de votre solde et de votre historique de paiements. DragonX est une cryptomonnaie axée sur la confidentialité, et le blindage est le mode par défaut pour les fonds reçus.\n\nN'utilisez une adresse transparente que lorsqu'un service avec lequel vous interagissez ne peut pas gérer les adresses blindées.",
|
||||
"faq_w_addr_2_a": "Utilisez une adresse blindée (zs) pour tout ce que vous envoyez et recevez — sur DragonX, c'est le seul type d'adresse qui puisse être payé.\n\nLes adresses transparentes (de minage) servent uniquement aux récompenses de minage en solo. Ne la communiquez jamais pour un paiement : le réseau le refuserait.",
|
||||
"faq_w_addr_2_q": "Quel type d'adresse dois-je utiliser ?",
|
||||
"faq_w_addr_3_a": "Pour les adresses blindées, la réutilisation ne pose aucun problème et ne révèle pas l'historique. Pour les adresses transparentes, utiliser une nouvelle adresse à chaque paiement améliore la confidentialité. Créez de nouvelles adresses depuis l'onglet Recevoir.",
|
||||
"faq_w_addr_3_a": "Réutiliser une adresse blindée ne pose aucun problème et ne révèle pas votre historique. Vous pouvez tout de même en créer de nouvelles depuis l'onglet Recevoir, par exemple une par contact pour savoir qui vous a payé. Les adresses de minage servent uniquement aux récompenses de minage ; vous aurez rarement besoin d'en créer une nouvelle.",
|
||||
"faq_w_addr_3_q": "Dois-je réutiliser une adresse ou en créer une nouvelle ?",
|
||||
"faq_w_addr_4_a": "Le blindage déplace des pièces d'une adresse transparente vers une adresse blindée, les rendant privées. Les pièces nouvellement minées arrivent en transparent et sont blindées automatiquement pour vous.\n\nVous pouvez aussi blinder manuellement depuis le portefeuille ; un blindage est simplement une transaction spéciale, il faut donc un temps de confirmation normal pour qu'elle se termine.",
|
||||
"faq_w_addr_4_a": "Le blindage déplace des pièces d'une adresse transparente (de minage) vers une adresse blindée, ce qui les rend privées. Les pièces fraîchement minées arrivent sur une adresse de minage et sont blindées automatiquement une fois arrivées à maturité.\n\nVous pouvez aussi blinder manuellement depuis le portefeuille ; un blindage n'est qu'une transaction particulière, il prend donc le temps de confirmation habituel.",
|
||||
"faq_w_addr_4_q": "Qu'est-ce que le « blindage » ?",
|
||||
"faq_w_addr_title": "Adresses et confidentialité",
|
||||
"faq_w_bal_1_a": "Tant que votre portefeuille est encore en cours de synchronisation, votre solde est incomplet et peut afficher 0 — il n'a pas encore analysé toutes vos transactions. Il se complète une fois la synchronisation terminée.\n\nSurveillez la barre d'état en bas : elle indique l'état de la connexion et la hauteur de bloc. Une fois la synchronisation terminée, votre solde est exact.",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "Votre phrase de récupération est la sauvegarde principale et suffit à tout restaurer. Une copie de votre fichier de portefeuille est une sauvegarde secondaire pratique qui conserve aussi les libellés et les paramètres. Conservez toute sauvegarde hors ligne et en lieu privé.",
|
||||
"faq_w_seed_4_q": "Dois-je aussi sauvegarder wallet.dat ?",
|
||||
"faq_w_seed_title": "Phrase de récupération et sauvegarde",
|
||||
"faq_w_send_1_a": "Ouvrez l'onglet Envoyer, collez l'adresse du destinataire, saisissez un montant et confirmez. Si le portefeuille est chiffré, on vous demandera de le déverrouiller pour la transaction.\n\nLes envois vers des adresses blindées sont privés ; les envois vers des adresses transparentes sont publics.",
|
||||
"faq_w_send_1_a": "Ouvrez l'onglet Envoyer, collez l'adresse du destinataire, saisissez un montant et confirmez. Si le portefeuille est chiffré, on vous demandera de le déverrouiller pour la transaction.\n\nLes paiements vont vers des adresses blindées (zs) et restent privés. Le portefeuille n'envoie pas vers une adresse transparente (de minage) : le réseau refuse les paiements vers elles.",
|
||||
"faq_w_send_1_q": "Comment envoyer des fonds ?",
|
||||
"faq_w_send_2_a": "Les frais DragonX sont très faibles et définis automatiquement. Les frais sont affichés avant que vous ne confirmiez. Vous n'avez normalement pas besoin de les modifier.",
|
||||
"faq_w_send_2_q": "Quels frais dois-je payer ?",
|
||||
"faq_w_send_3_a": "Oui — lors de l'envoi vers une adresse blindée (Z), vous pouvez inclure un mémo chiffré. Seul le destinataire peut le lire. Les adresses transparentes ne prennent pas en charge les mémos.",
|
||||
"faq_w_send_3_a": "Oui — lors de l'envoi vers une adresse blindée (Z), vous pouvez inclure un mémo chiffré. Seul le destinataire peut le lire.",
|
||||
"faq_w_send_3_q": "Puis-je joindre un message à un paiement ?",
|
||||
"faq_w_send_4_a": "Une transaction est « en attente » jusqu'à ce qu'elle soit minée dans un bloc et gagne des confirmations. Cela prend généralement une minute ou deux. Les transactions blindées nécessitent aussi que votre portefeuille soit synchronisé pour construire la preuve.\n\nSi un envoi reste en attente anormalement longtemps, vérifiez que votre portefeuille est connecté et synchronisé (voir la barre d'état en bas de la fenêtre).",
|
||||
"faq_w_send_4_q": "J'ai envoyé un paiement mais il indique « en attente » — pourquoi ?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "Frais de mineur",
|
||||
"mining": "Minage",
|
||||
"mining_active": "Actif",
|
||||
"mining_addr_explainer": "Ne reçoit que des récompenses de minage en solo — le réseau refuse les paiements vers cette adresse. Les récompenses sont blindées automatiquement une fois arrivées à maturité.",
|
||||
"mining_address": "Adresse de minage",
|
||||
"mining_address_copied": "Adresse de minage copiée",
|
||||
"mining_all_time": "Tout le temps",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "Portefeuille : %.1f Go",
|
||||
"ram_wallet_mb": "Portefeuille : %.0f Mo",
|
||||
"receive": "Recevoir",
|
||||
"receive_not_for_payments": "Pas pour les paiements",
|
||||
"receive_use_shielded": "Utiliser une adresse blindée",
|
||||
"received": "reçu",
|
||||
"received_filter": "Reçu",
|
||||
"received_label": "Reçu",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "Sélectionner une adresse...",
|
||||
"request_shielded_addrs": "-- Adresses blindées --",
|
||||
"request_title": "Demander un paiement",
|
||||
"request_transparent_addrs": "-- Adresses transparentes --",
|
||||
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
|
||||
"rescan": "Re-scanner",
|
||||
"rescan_bootstrapped_msg": "Votre nœud a été initialisé par bootstrap, les blocs situés en dessous de l'instantané ne sont donc pas sur le disque et un rescan depuis la genèse échouerait. Effectuez un rescan à partir d'une hauteur incluse dans votre instantané pour réconcilier le solde dépensé de votre portefeuille. Votre fichier wallet.dat et vos données de chaîne ne sont pas supprimés.",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "Entrez une adresse DragonX pour vérifier si elle est valide et si elle appartient à ce portefeuille.",
|
||||
"validate_invalid": "INVALIDE",
|
||||
"validate_is_mine": "Ce portefeuille possède cette adresse",
|
||||
"validate_mining_type": "Adresse de minage (transparente)",
|
||||
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
||||
"validate_ownership": "Propriété :",
|
||||
"validate_results": "Résultats :",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "Blindée (z-adresse)",
|
||||
"validate_status": "Statut :",
|
||||
"validate_title": "Valider l'adresse",
|
||||
"validate_transparent_type": "Transparente (t-adresse)",
|
||||
"validate_type": "Type :",
|
||||
"validate_valid": "VALIDE",
|
||||
"validating": "Validation...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "ヘルプ&FAQ",
|
||||
"faq_search_hint": "FAQを検索…",
|
||||
"faq_title": "ヘルプ&FAQ",
|
||||
"faq_w_addr_1_a": "透明アドレス(RまたはtまたはR始まり)はほとんどのコインと同様に動作します。金額とアドレスはブロックチェーン上で公開されます。\n\nシールドアドレス(zアドレス、zsで始まる)はゼロ知識暗号を使用し、金額・送信者・受信者はチェーン上で暗号化されます。シールドアドレスに保有された残高は非公開です。",
|
||||
"faq_w_addr_1_a": "シールドアドレス(zs で始まる z アドレス)は、DragonX で使用するアドレスです。ゼロ知識暗号を使用するため、金額・送信者・受信者はチェーン上で暗号化され、残高は非公開に保たれます。\n\n透明アドレス(R で始まる)はマイニングアドレスです。DragonX は完全にプライベートなチェーンのため、マイニング報酬しか受け取れず、送られた支払いはネットワークによって拒否されます。そこに届いた報酬はシールドされるまで公開されますが、シールドはウォレットが自動的に行います。",
|
||||
"faq_w_addr_1_q": "透明アドレスとシールドアドレスの違いは何ですか?",
|
||||
"faq_w_addr_2_a": "できる限りシールド(z)アドレスを使用してください。残高と支払い履歴を非公開に保てます。DragonXはプライバシーコインであり、受け取る資金はデフォルトでシールドされます。\n\n透明アドレスは、やり取りするサービスがシールドアドレスに対応していない場合のみ使用してください。",
|
||||
"faq_w_addr_2_a": "送受信にはすべてシールド(zs)アドレスを使用してください。DragonX で支払いを受け取れるのはこの種類だけです。\n\n透明(マイニング)アドレスはソロマイニング報酬専用です。支払いのために他人に教えないでください。ネットワークが拒否します。",
|
||||
"faq_w_addr_2_q": "どちらのアドレスタイプを使うべきですか?",
|
||||
"faq_w_addr_3_a": "シールドアドレスは再利用しても問題なく、履歴が漏れることもありません。透明アドレスは支払いごとに新しいアドレスを使うとプライバシーが向上します。新しいアドレスは受信タブから作成できます。",
|
||||
"faq_w_addr_3_a": "シールドアドレスを再利用しても問題はなく、履歴が漏れることもありません。それでも「受信」タブで新しいアドレスを作成できます。たとえば連絡先ごとに 1 つ作れば、誰が支払ったかがわかります。マイニングアドレスはマイニング報酬専用で、新しく作る必要はほとんどありません。",
|
||||
"faq_w_addr_3_q": "アドレスは使い回すべきですか、それとも新しく作るべきですか?",
|
||||
"faq_w_addr_4_a": "シールド化とは、コインを透明アドレスからシールドアドレスへ移し、非公開にすることです。新しくマイニングされたコインは透明な状態で届き、自動的にシールドされます。\n\n手動でシールドすることもできます。シールドは特別な種類のトランザクションにすぎないため、完了までに通常の確認時間がかかります。",
|
||||
"faq_w_addr_4_a": "シールドとは、透明(マイニング)アドレスのコインをシールドアドレスに移して非公開にすることです。新たにマイニングされたコインはマイニングアドレスに届き、成熟すると自動的にシールドされます。\n\nウォレットから手動でシールドすることもできます。シールドは特別なトランザクションにすぎないため、通常の承認時間がかかります。",
|
||||
"faq_w_addr_4_q": "「シールド化」とは何ですか?",
|
||||
"faq_w_addr_title": "アドレスとプライバシー",
|
||||
"faq_w_bal_1_a": "ウォレットがまだ同期中の場合、残高は不完全で0と表示されることがあります。まだすべてのトランザクションをスキャンしていないためです。同期が完了すると反映されます。\n\n画面下部のステータスバーを確認してください。接続状態とブロック高が表示されます。同期が完了すれば残高は正確になります。",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "シードフレーズは主要なバックアップであり、これだけですべてを復元できます。ウォレットファイルのコピーは便利な二次バックアップで、ラベルや設定も保持されます。どちらのバックアップもオフラインかつ非公開の場所に保管してください。",
|
||||
"faq_w_seed_4_q": "wallet.dat もバックアップすべきですか?",
|
||||
"faq_w_seed_title": "シードフレーズとバックアップ",
|
||||
"faq_w_send_1_a": "送金タブを開き、受取人のアドレスを貼り付け、金額を入力して確認してください。ウォレットが暗号化されている場合、トランザクションのためにロック解除を求められます。\n\nシールドアドレスへの送金は非公開、透明アドレスへの送金は公開されます。",
|
||||
"faq_w_send_1_a": "送金タブを開き、受取人のアドレスを貼り付け、金額を入力して確認してください。ウォレットが暗号化されている場合、トランザクションのためにロック解除を求められます。\n\n支払いはシールド(zs)アドレスに送られ、非公開に保たれます。ウォレットは透明(マイニング)アドレスには送金しません。ネットワークがそのアドレスへの支払いを拒否するためです。",
|
||||
"faq_w_send_1_q": "資金を送金するにはどうすればよいですか?",
|
||||
"faq_w_send_2_a": "DragonXの手数料は非常に低く、自動的に設定されます。手数料は確認前に表示されます。通常は変更する必要はありません。",
|
||||
"faq_w_send_2_q": "どのくらいの手数料がかかりますか?",
|
||||
"faq_w_send_3_a": "はい — シールド(z)アドレスへ送金する際は、暗号化されたメモを添付できます。読めるのは受取人だけです。透明アドレスはメモに対応していません。",
|
||||
"faq_w_send_3_a": "はい — シールド(z)アドレスへ送金する際は、暗号化されたメモを添付できます。読めるのは受取人だけです。",
|
||||
"faq_w_send_3_q": "支払いにメッセージを添付できますか?",
|
||||
"faq_w_send_4_a": "トランザクションはブロックに取り込まれ、確認数を得るまで「保留中」となります。通常1~2分ほどかかります。シールドトランザクションでは、証明を生成するためにウォレットが同期済みである必要もあります。\n\n送金が異常に長く保留中のままの場合は、ウォレットが接続され、同期済みであることを確認してください(ウィンドウ下部のステータスバーを参照)。",
|
||||
"faq_w_send_4_q": "支払いを送りましたが「保留中」のままです — なぜですか?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "マイナー手数料",
|
||||
"mining": "マイニング",
|
||||
"mining_active": "アクティブ",
|
||||
"mining_addr_explainer": "ソロマイニング報酬のみを受け取ります。ネットワークはこのアドレスへの支払いを拒否します。報酬は成熟すると自動的にシールドされます。",
|
||||
"mining_address": "マイニングアドレス",
|
||||
"mining_address_copied": "マイニングアドレスをコピーしました",
|
||||
"mining_all_time": "全期間",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "ウォレット:%.1f GB",
|
||||
"ram_wallet_mb": "ウォレット:%.0f MB",
|
||||
"receive": "受信",
|
||||
"receive_not_for_payments": "支払いには使えません",
|
||||
"receive_use_shielded": "シールドアドレスを使う",
|
||||
"received": "受信済み",
|
||||
"received_filter": "受信済み",
|
||||
"received_label": "受信済み",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "アドレスを選択...",
|
||||
"request_shielded_addrs": "-- シールドアドレス --",
|
||||
"request_title": "支払いを請求",
|
||||
"request_transparent_addrs": "-- 透明アドレス --",
|
||||
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
|
||||
"rescan": "再スキャン",
|
||||
"rescan_bootstrapped_msg": "ノードはブートストラップされているため、スナップショットより下のブロックはディスク上になく、ジェネシスからの再スキャンは失敗します。ウォレットの使用済み残高を照合するには、スナップショットに含まれる高さから再スキャンしてください。wallet.dat とチェーンデータは削除されません。",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "DragonXアドレスを入力して、有効かどうか、そしてこのウォレットに属しているかどうかを確認します。",
|
||||
"validate_invalid": "無効",
|
||||
"validate_is_mine": "このウォレットがこのアドレスを所有しています",
|
||||
"validate_mining_type": "マイニングアドレス(透明)",
|
||||
"validate_not_mine": "このウォレットに属していません",
|
||||
"validate_ownership": "所有者:",
|
||||
"validate_results": "結果:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "シールド(zアドレス)",
|
||||
"validate_status": "ステータス:",
|
||||
"validate_title": "アドレスを検証",
|
||||
"validate_transparent_type": "透明(tアドレス)",
|
||||
"validate_type": "タイプ:",
|
||||
"validate_valid": "有効",
|
||||
"validating": "検証中...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "도움말 및 FAQ",
|
||||
"faq_search_hint": "FAQ 검색…",
|
||||
"faq_title": "도움말 및 FAQ",
|
||||
"faq_w_addr_1_a": "투명 주소(R 또는 t로 시작)는 대부분의 코인처럼 작동합니다. 금액과 주소가 블록체인에 공개됩니다.\n\n보호 주소(zs로 시작하는 z-주소)는 영지식 암호화를 사용합니다 — 금액, 발신자, 수신자가 체인상에서 암호화됩니다. 보호 주소에 보관된 잔액은 비공개입니다.",
|
||||
"faq_w_addr_1_a": "차폐 주소(zs로 시작하는 z 주소)는 DragonX에서 사용하는 주소입니다. 영지식 암호를 사용하므로 금액, 보낸 사람, 받는 사람이 체인에서 암호화되고 잔액이 비공개로 유지됩니다.\n\n투명 주소(R로 시작)는 채굴 주소입니다. DragonX는 완전한 프라이빗 체인이므로 이 주소는 채굴 보상만 받을 수 있으며, 네트워크가 이 주소로 보내는 모든 결제를 거부합니다. 그곳에 들어온 보상은 차폐되기 전까지 공개되며, 차폐는 지갑이 자동으로 처리합니다.",
|
||||
"faq_w_addr_1_q": "투명 주소와 보호 주소의 차이는 무엇인가요?",
|
||||
"faq_w_addr_2_a": "가능하면 항상 보호(z) 주소를 사용하세요 — 잔액과 결제 내역이 비공개로 유지됩니다. DragonX는 프라이버시 코인이며, 수신 자금의 기본값은 보호 주소입니다.\n\n상대방 서비스가 보호 주소를 처리할 수 없는 경우에만 투명 주소를 사용하세요.",
|
||||
"faq_w_addr_2_a": "보내고 받는 모든 거래에 차폐(zs) 주소를 사용하세요. DragonX에서는 이 주소만 결제를 받을 수 있습니다.\n\n투명(채굴) 주소는 솔로 채굴 보상 전용입니다. 결제용으로 절대 알려주지 마세요. 네트워크가 거부합니다.",
|
||||
"faq_w_addr_2_q": "어떤 주소 유형을 사용해야 하나요?",
|
||||
"faq_w_addr_3_a": "보호 주소는 재사용해도 괜찮으며 내역이 노출되지 않습니다. 투명 주소는 결제마다 새 주소를 사용하는 것이 프라이버시에 좋습니다. 수신 탭에서 새 주소를 만들 수 있습니다.",
|
||||
"faq_w_addr_3_a": "차폐 주소는 재사용해도 괜찮으며 기록이 노출되지 않습니다. 그래도 받기 탭에서 새 주소를 만들 수 있습니다. 예를 들어 연락처마다 하나씩 만들면 누가 보냈는지 알 수 있습니다. 채굴 주소는 채굴 보상 전용이라 새로 만들 일은 거의 없습니다.",
|
||||
"faq_w_addr_3_q": "주소를 재사용해야 하나요, 아니면 새로 만들어야 하나요?",
|
||||
"faq_w_addr_4_a": "보호(shielding)는 코인을 투명 주소에서 보호 주소로 옮겨 비공개로 만드는 것입니다. 새로 채굴된 코인은 투명 상태로 도착하며 자동으로 보호 처리됩니다.\n\n지갑에서 수동으로 보호 처리할 수도 있습니다. 보호는 일반 트랜잭션의 일종이므로 완료까지 일반적인 확인 시간이 걸립니다.",
|
||||
"faq_w_addr_4_a": "차폐는 투명(채굴) 주소의 코인을 차폐 주소로 옮겨 비공개로 만드는 것입니다. 새로 채굴된 코인은 채굴 주소로 들어오며, 성숙하면 자동으로 차폐됩니다.\n\n지갑에서 직접 차폐할 수도 있습니다. 차폐는 특별한 트랜잭션일 뿐이라 일반적인 확인 시간이 걸립니다.",
|
||||
"faq_w_addr_4_q": "'보호(shielding)'란 무엇인가요?",
|
||||
"faq_w_addr_title": "주소 및 프라이버시",
|
||||
"faq_w_bal_1_a": "지갑이 아직 동기화 중일 때는 잔액이 불완전하며 0으로 표시될 수 있습니다 — 아직 모든 트랜잭션을 스캔하지 않았기 때문입니다. 동기화가 끝나면 채워집니다.\n\n하단 상태 표시줄을 확인하세요. 연결 상태와 블록 높이가 표시됩니다. 동기화가 완료되면 잔액이 정확해집니다.",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "시드 문구는 기본 백업이며 모든 것을 복원하기에 충분합니다. 지갑 파일 사본은 라벨과 설정까지 보존하는 편리한 보조 백업입니다. 모든 백업은 오프라인에 비공개로 보관하세요.",
|
||||
"faq_w_seed_4_q": "wallet.dat도 백업해야 하나요?",
|
||||
"faq_w_seed_title": "시드 문구 및 백업",
|
||||
"faq_w_send_1_a": "전송 탭을 열고 받는 사람의 주소를 붙여넣은 다음 금액을 입력하고 확인하세요. 지갑이 암호화되어 있다면 트랜잭션을 위해 잠금 해제를 요청받습니다.\n\n보호 주소로의 전송은 비공개이며, 투명 주소로의 전송은 공개됩니다.",
|
||||
"faq_w_send_1_a": "전송 탭을 열고 받는 사람의 주소를 붙여넣은 다음 금액을 입력하고 확인하세요. 지갑이 암호화되어 있다면 트랜잭션을 위해 잠금 해제를 요청받습니다.\n\n결제는 차폐(zs) 주소로 보내지며 비공개로 유지됩니다. 지갑은 투명(채굴) 주소로는 보내지 않습니다. 네트워크가 그런 주소로의 결제를 거부하기 때문입니다.",
|
||||
"faq_w_send_1_q": "자금은 어떻게 보내나요?",
|
||||
"faq_w_send_2_a": "DragonX 수수료는 매우 낮으며 자동으로 설정됩니다. 확인 전에 수수료가 표시되며, 보통 변경할 필요는 없습니다.",
|
||||
"faq_w_send_2_q": "어떤 수수료를 내나요?",
|
||||
"faq_w_send_3_a": "네 — 보호(z) 주소로 전송할 때 암호화된 메모를 첨부할 수 있습니다. 받는 사람만 읽을 수 있습니다. 투명 주소는 메모를 지원하지 않습니다.",
|
||||
"faq_w_send_3_a": "네 — 보호(z) 주소로 전송할 때 암호화된 메모를 첨부할 수 있습니다. 받는 사람만 읽을 수 있습니다.",
|
||||
"faq_w_send_3_q": "결제에 메시지를 첨부할 수 있나요?",
|
||||
"faq_w_send_4_a": "트랜잭션은 블록에 채굴되어 확인을 얻기 전까지 '대기 중'입니다. 보통 1~2분 정도 걸립니다. 보호 트랜잭션은 증명을 생성하기 위해 지갑이 동기화되어 있어야 합니다.\n\n전송이 유난히 오래 대기 중이라면 지갑이 연결되고 동기화되어 있는지 확인하세요(창 하단의 상태 표시줄 참고).",
|
||||
"faq_w_send_4_q": "결제를 보냈는데 대기 중이라고 나와요 — 왜 그런가요?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "채굴 수수료",
|
||||
"mining": "채굴",
|
||||
"mining_active": "활성",
|
||||
"mining_addr_explainer": "솔로 채굴 보상만 받습니다. 네트워크가 이 주소로의 결제를 거부합니다. 보상은 성숙하면 자동으로 차폐됩니다.",
|
||||
"mining_address": "채굴 주소",
|
||||
"mining_address_copied": "채굴 주소가 복사되었습니다",
|
||||
"mining_all_time": "전체 기간",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "지갑: %.1f GB",
|
||||
"ram_wallet_mb": "지갑: %.0f MB",
|
||||
"receive": "수신",
|
||||
"receive_not_for_payments": "결제용이 아님",
|
||||
"receive_use_shielded": "차폐 주소 사용",
|
||||
"received": "수신됨",
|
||||
"received_filter": "수신됨",
|
||||
"received_label": "수신됨",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "주소 선택...",
|
||||
"request_shielded_addrs": "-- 차폐 주소 --",
|
||||
"request_title": "결제 요청",
|
||||
"request_transparent_addrs": "-- 투명 주소 --",
|
||||
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
|
||||
"rescan": "재스캔",
|
||||
"rescan_bootstrapped_msg": "노드가 부트스트랩되어 스냅샷 이전 블록은 디스크에 없으므로 제네시스부터의 재스캔은 실패합니다. 지갑의 사용된 잔액을 조정하려면 스냅샷에 포함된 높이부터 재스캔하세요. wallet.dat과 체인 데이터는 삭제되지 않습니다.",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "DragonX 주소를 입력하여 유효한지 그리고 이 지갑에 속하는지 확인합니다.",
|
||||
"validate_invalid": "유효하지 않음",
|
||||
"validate_is_mine": "이 지갑이 이 주소를 소유합니다",
|
||||
"validate_mining_type": "채굴 주소(투명)",
|
||||
"validate_not_mine": "이 지갑에 속하지 않음",
|
||||
"validate_ownership": "소유자:",
|
||||
"validate_results": "결과:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "차폐 (z 주소)",
|
||||
"validate_status": "상태:",
|
||||
"validate_title": "주소 검증",
|
||||
"validate_transparent_type": "투명 (t 주소)",
|
||||
"validate_type": "유형:",
|
||||
"validate_valid": "유효함",
|
||||
"validating": "검증 중...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "Ajuda e FAQ",
|
||||
"faq_search_hint": "Buscar no FAQ…",
|
||||
"faq_title": "Ajuda e FAQ",
|
||||
"faq_w_addr_1_a": "Endereços transparentes (começam com R ou t) funcionam como a maioria das moedas: os valores e endereços são públicos na blockchain.\n\nEndereços blindados (endereços-z, começam com zs) usam criptografia de conhecimento zero — o valor, remetente e destinatário são encriptados on-chain. Saldos em endereços blindados são privados.",
|
||||
"faq_w_addr_1_a": "Os endereços blindados (endereços z, que começam com zs) são os que você usa no DragonX. Eles usam criptografia de conhecimento zero, então o valor, o remetente e o destinatário são criptografados na blockchain e seu saldo permanece privado.\n\nOs endereços transparentes (que começam com R) são endereços de mineração. O DragonX é uma blockchain totalmente privada, então eles só podem receber recompensas de mineração — a rede rejeita qualquer pagamento enviado a eles. As recompensas que chegam ali são públicas até serem blindadas, o que a carteira faz automaticamente.",
|
||||
"faq_w_addr_1_q": "Qual a diferença entre endereços transparentes e blindados?",
|
||||
"faq_w_addr_2_a": "Prefira endereços blindados (z) sempre que possível — eles mantêm seu saldo e histórico de pagamentos privados. DragonX é uma moeda de privacidade, e blindado é o padrão para fundos recebidos.\n\nUse um endereço transparente apenas quando um serviço com o qual você interage não conseguir lidar com endereços blindados.",
|
||||
"faq_w_addr_2_a": "Use um endereço blindado (zs) para tudo o que você envia e recebe — no DragonX é o único tipo que pode receber pagamentos.\n\nEndereços transparentes (de mineração) servem apenas para recompensas de mineração solo. Nunca passe um para receber um pagamento: a rede vai rejeitá-lo.",
|
||||
"faq_w_addr_2_q": "Qual tipo de endereço devo usar?",
|
||||
"faq_w_addr_3_a": "Para endereços blindados, reutilizar é tranquilo e não expõe o histórico. Para endereços transparentes, usar um endereço novo a cada pagamento melhora a privacidade. Crie novos endereços na aba Receber.",
|
||||
"faq_w_addr_3_a": "Reutilizar um endereço blindado não tem problema e não revela seu histórico. Você ainda pode criar novos na aba Receber, por exemplo um por contato para saber quem pagou você. Endereços de mineração servem apenas para recompensas de mineração; raramente você precisará de um novo.",
|
||||
"faq_w_addr_3_q": "Devo reutilizar um endereço ou criar um novo?",
|
||||
"faq_w_addr_4_a": "Blindar move moedas de um endereço transparente para um blindado, tornando-as privadas. Moedas recém-mineradas chegam transparentes e são blindadas automaticamente para você.\n\nVocê também pode blindar manualmente pela carteira; um blindamento é apenas uma transação especial, então leva o tempo normal de confirmação para ser concluído.",
|
||||
"faq_w_addr_4_a": "Blindar move moedas de um endereço transparente (de mineração) para um blindado, tornando-as privadas. Moedas recém-mineradas chegam a um endereço de mineração e são blindadas automaticamente quando amadurecem.\n\nVocê também pode blindar manualmente pela carteira; uma blindagem é apenas uma transação especial, então leva o tempo normal de confirmação.",
|
||||
"faq_w_addr_4_q": "O que é \"blindar\"?",
|
||||
"faq_w_addr_title": "Endereços e Privacidade",
|
||||
"faq_w_bal_1_a": "Enquanto sua carteira ainda está sincronizando, seu saldo está incompleto e pode aparecer como 0 — ainda não foram escaneadas todas as suas transações. Ele se completa assim que a sincronização terminar.\n\nObserve a barra de status na parte inferior: ela mostra o estado da conexão e a altura do bloco. Quando a sincronização estiver completa, seu saldo estará correto.",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "Sua frase de recuperação é o backup principal e é suficiente para restaurar tudo. Uma cópia do arquivo da sua carteira é um backup secundário conveniente que também preserva rótulos e configurações. Mantenha qualquer backup offline e privado.",
|
||||
"faq_w_seed_4_q": "Devo também fazer backup do wallet.dat?",
|
||||
"faq_w_seed_title": "Frase de Recuperação e Backup",
|
||||
"faq_w_send_1_a": "Abra a aba Enviar, cole o endereço do destinatário, informe um valor e confirme. Se a carteira estiver encriptada, você será solicitado a desbloqueá-la para a transação.\n\nEnvios para endereços blindados são privados; envios para endereços transparentes são públicos.",
|
||||
"faq_w_send_1_a": "Abra a aba Enviar, cole o endereço do destinatário, informe um valor e confirme. Se a carteira estiver encriptada, você será solicitado a desbloqueá-la para a transação.\n\nOs pagamentos vão para endereços blindados (zs) e continuam privados. A carteira não envia para um endereço transparente (de mineração): a rede rejeita pagamentos para eles.",
|
||||
"faq_w_send_1_q": "Como eu envio fundos?",
|
||||
"faq_w_send_2_a": "As taxas do DragonX são muito baixas e definidas automaticamente. A taxa é mostrada antes de você confirmar. Normalmente você não precisa alterá-la.",
|
||||
"faq_w_send_2_q": "Qual taxa eu pago?",
|
||||
"faq_w_send_3_a": "Sim — ao enviar para um endereço blindado (z) você pode incluir um memo encriptado. Só o destinatário pode lê-lo. Endereços transparentes não suportam memos.",
|
||||
"faq_w_send_3_a": "Sim — ao enviar para um endereço blindado (z) você pode incluir um memo encriptado. Só o destinatário pode lê-lo.",
|
||||
"faq_w_send_3_q": "Posso anexar uma mensagem a um pagamento?",
|
||||
"faq_w_send_4_a": "Uma transação fica \"pendente\" até ser minerada em um bloco e ganhar confirmações. Isso costuma levar um ou dois minutos. Transações blindadas também precisam que sua carteira esteja sincronizada para gerar a prova.\n\nSe um envio permanecer pendente por tempo incomumente longo, verifique se sua carteira está conectada e sincronizada (veja a barra de status na parte inferior da janela).",
|
||||
"faq_w_send_4_q": "Enviei um pagamento, mas ele diz pendente — por quê?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "Taxa de Minerador",
|
||||
"mining": "Mineração",
|
||||
"mining_active": "Ativo",
|
||||
"mining_addr_explainer": "Recebe apenas recompensas de mineração solo — a rede rejeita pagamentos para este endereço. As recompensas são blindadas automaticamente quando amadurecem.",
|
||||
"mining_address": "Endereço de mineração",
|
||||
"mining_address_copied": "Endereço de mineração copiado",
|
||||
"mining_all_time": "Todo o Tempo",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "Carteira: %.1f GB",
|
||||
"ram_wallet_mb": "Carteira: %.0f MB",
|
||||
"receive": "Receber",
|
||||
"receive_not_for_payments": "Não é para pagamentos",
|
||||
"receive_use_shielded": "Usar um endereço blindado",
|
||||
"received": "recebido",
|
||||
"received_filter": "Recebido",
|
||||
"received_label": "Recebido",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "Selecionar endereço...",
|
||||
"request_shielded_addrs": "-- Endereços Blindados --",
|
||||
"request_title": "Solicitar Pagamento",
|
||||
"request_transparent_addrs": "-- Endereços Transparentes --",
|
||||
"request_uri_copied": "URI de pagamento copiada para a área de transferência",
|
||||
"rescan": "Reescanear",
|
||||
"rescan_bootstrapped_msg": "Seu nó foi inicializado por bootstrap, então os blocos abaixo do snapshot não estão em disco e um reescaneamento a partir do gênesis falharia. Reescaneie a partir de uma altura que seu snapshot inclua para reconciliar o saldo gasto da sua carteira. Seu wallet.dat e os dados da cadeia não são excluídos.",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "Digite um endereço DragonX para verificar se é válido e se pertence a esta carteira.",
|
||||
"validate_invalid": "INVÁLIDO",
|
||||
"validate_is_mine": "Esta carteira possui este endereço",
|
||||
"validate_mining_type": "Endereço de mineração (transparente)",
|
||||
"validate_not_mine": "Não pertence a esta carteira",
|
||||
"validate_ownership": "Propriedade:",
|
||||
"validate_results": "Resultados:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "Blindado (z-endereço)",
|
||||
"validate_status": "Status:",
|
||||
"validate_title": "Validar Endereço",
|
||||
"validate_transparent_type": "Transparente (t-endereço)",
|
||||
"validate_type": "Tipo:",
|
||||
"validate_valid": "VÁLIDO",
|
||||
"validating": "Validando...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "Справка и FAQ",
|
||||
"faq_search_hint": "Поиск по FAQ…",
|
||||
"faq_title": "Справка и FAQ",
|
||||
"faq_w_addr_1_a": "Прозрачные адреса (начинаются с R или t) работают как в большинстве монет: суммы и адреса видны в блокчейне.\n\nЗащищённые адреса (Z-адреса, начинаются с zs) используют криптографию с нулевым разглашением — сумма, отправитель и получатель зашифрованы в цепочке. Балансы на защищённых адресах приватны.",
|
||||
"faq_w_addr_1_a": "Экранированные адреса (z-адреса, начинаются с zs) — это адреса, которыми вы пользуетесь в DragonX. Они используют криптографию с нулевым разглашением: сумма, отправитель и получатель шифруются в блокчейне, а ваш баланс остаётся приватным.\n\nПрозрачные адреса (начинаются с R) — это адреса для майнинга. DragonX — полностью приватная сеть, поэтому они могут получать только награды за майнинг: сеть отклоняет любой платёж на такой адрес. Поступившие туда награды остаются публичными, пока их не экранируют, а кошелёк делает это автоматически.",
|
||||
"faq_w_addr_1_q": "В чём разница между прозрачными и защищёнными адресами?",
|
||||
"faq_w_addr_2_a": "По возможности используйте защищённые (Z) адреса — они сохраняют приватность вашего баланса и истории платежей. DragonX — приватная монета, и защищённый адрес используется по умолчанию для получения средств.\n\nПрозрачный адрес стоит использовать только если сервис, с которым вы работаете, не поддерживает защищённые адреса.",
|
||||
"faq_w_addr_2_a": "Используйте экранированный (zs) адрес для всего, что отправляете и получаете, — в DragonX только на такой адрес можно заплатить.\n\nПрозрачные адреса (для майнинга) предназначены только для наград за соло-майнинг. Никогда не давайте такой адрес для платежа: сеть его отклонит.",
|
||||
"faq_w_addr_2_q": "Какой тип адреса мне использовать?",
|
||||
"faq_w_addr_3_a": "Для защищённых адресов повторное использование допустимо и не раскрывает историю. Для прозрачных адресов использование нового адреса для каждого платежа повышает приватность. Создавайте новые адреса на вкладке «Получить».",
|
||||
"faq_w_addr_3_a": "Повторно использовать экранированный адрес безопасно — это не раскрывает вашу историю. При желании можно создавать новые на вкладке «Получить», например по одному на контакт, чтобы видеть, кто вам заплатил. Адреса для майнинга нужны только для наград за майнинг; новый понадобится редко.",
|
||||
"faq_w_addr_3_q": "Использовать один адрес повторно или создавать новый?",
|
||||
"faq_w_addr_4_a": "Экранирование перемещает монеты с прозрачного адреса на защищённый, делая их приватными. Новые добытые монеты поступают как прозрачные и экранируются автоматически.\n\nВы также можете экранировать монеты вручную из кошелька — экранирование — это обычная транзакция, поэтому она подтверждается за обычное время.",
|
||||
"faq_w_addr_4_a": "Экранирование переводит монеты с прозрачного адреса (для майнинга) на экранированный, делая их приватными. Новые намайненные монеты приходят на адрес для майнинга и экранируются автоматически, когда созревают.\n\nЭкранировать можно и вручную в кошельке; экранирование — это обычная транзакция особого вида, поэтому подтверждается в обычные сроки.",
|
||||
"faq_w_addr_4_q": "Что такое «экранирование»?",
|
||||
"faq_w_addr_title": "Адреса и приватность",
|
||||
"faq_w_bal_1_a": "Пока кошелёк синхронизируется, ваш баланс неполный и может отображаться как 0 — не все транзакции ещё просканированы. Он заполнится после завершения синхронизации.\n\nСледите за строкой состояния внизу окна: она показывает состояние соединения и высоту блока. После завершения синхронизации баланс будет точным.",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "Ваша сид-фраза — это основная резервная копия, и её достаточно для полного восстановления. Копия файла кошелька — удобная дополнительная резервная копия, которая также сохраняет метки и настройки. Храните любые резервные копии офлайн и в тайне.",
|
||||
"faq_w_seed_4_q": "Нужно ли также сохранять резервную копию wallet.dat?",
|
||||
"faq_w_seed_title": "Сид-фраза и резервное копирование",
|
||||
"faq_w_send_1_a": "Откройте вкладку «Отправить», вставьте адрес получателя, укажите сумму и подтвердите. Если кошелёк зашифрован, вам предложат разблокировать его для транзакции.\n\nОтправка на защищённые адреса приватна; отправка на прозрачные адреса публична.",
|
||||
"faq_w_send_1_a": "Откройте вкладку «Отправить», вставьте адрес получателя, укажите сумму и подтвердите. Если кошелёк зашифрован, вам предложат разблокировать его для транзакции.\n\nПлатежи отправляются на экранированные (zs) адреса и остаются приватными. Кошелёк не отправляет на прозрачный адрес (для майнинга): сеть отклоняет платежи на такие адреса.",
|
||||
"faq_w_send_1_q": "Как отправить средства?",
|
||||
"faq_w_send_2_a": "Комиссии DragonX очень низкие и устанавливаются автоматически. Комиссия показывается перед подтверждением. Обычно менять её не нужно.",
|
||||
"faq_w_send_2_q": "Какую комиссию я плачу?",
|
||||
"faq_w_send_3_a": "Да — при отправке на защищённый (Z) адрес можно приложить зашифрованную заметку. Прочитать её сможет только получатель. Прозрачные адреса не поддерживают заметки.",
|
||||
"faq_w_send_3_a": "Да — при отправке на защищённый (Z) адрес можно приложить зашифрованную заметку. Прочитать её сможет только получатель.",
|
||||
"faq_w_send_3_q": "Можно ли прикрепить сообщение к платежу?",
|
||||
"faq_w_send_4_a": "Транзакция остаётся «в ожидании», пока не будет добыта в блок и не получит подтверждения. Обычно это занимает минуту-две. Защищённым транзакциям также нужно, чтобы кошелёк был синхронизирован, для построения доказательства.\n\nЕсли отправка необычно долго остаётся в ожидании, проверьте, что кошелёк подключён и синхронизирован (см. строку состояния внизу окна).",
|
||||
"faq_w_send_4_q": "Я отправил платёж, но он в статусе «в ожидании» — почему?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "Комиссия майнера",
|
||||
"mining": "Майнинг",
|
||||
"mining_active": "Активен",
|
||||
"mining_addr_explainer": "Принимает только награды за соло-майнинг — сеть отклоняет платежи на этот адрес. Награды экранируются автоматически, когда созревают.",
|
||||
"mining_address": "Адрес для майнинга",
|
||||
"mining_address_copied": "Адрес майнинга скопирован",
|
||||
"mining_all_time": "За всё время",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "Кошелёк: %.1f ГБ",
|
||||
"ram_wallet_mb": "Кошелёк: %.0f МБ",
|
||||
"receive": "Получить",
|
||||
"receive_not_for_payments": "Не для платежей",
|
||||
"receive_use_shielded": "Использовать экранированный адрес",
|
||||
"received": "получено",
|
||||
"received_filter": "Получено",
|
||||
"received_label": "Получено",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "Выбрать адрес...",
|
||||
"request_shielded_addrs": "-- Экранированные адреса --",
|
||||
"request_title": "Запрос оплаты",
|
||||
"request_transparent_addrs": "-- Прозрачные адреса --",
|
||||
"request_uri_copied": "URI платежа скопирован в буфер обмена",
|
||||
"rescan": "Пересканировать",
|
||||
"rescan_bootstrapped_msg": "Ваш узел был инициализирован из бутстрапа, поэтому блоки ниже снимка отсутствуют на диске, и пересканирование с генезиса не удастся. Пересканируйте с высоты, входящей в ваш снимок, чтобы согласовать потраченный баланс кошелька. Ваш wallet.dat и данные блокчейна не удаляются.",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "Введите адрес DragonX, чтобы проверить его действительность и принадлежность к этому кошельку.",
|
||||
"validate_invalid": "НЕДЕЙСТВИТЕЛЕН",
|
||||
"validate_is_mine": "Этот кошелёк владеет этим адресом",
|
||||
"validate_mining_type": "Адрес для майнинга (прозрачный)",
|
||||
"validate_not_mine": "Не принадлежит этому кошельку",
|
||||
"validate_ownership": "Принадлежность:",
|
||||
"validate_results": "Результаты:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "Экранированный (z-адрес)",
|
||||
"validate_status": "Статус:",
|
||||
"validate_title": "Проверить адрес",
|
||||
"validate_transparent_type": "Прозрачный (t-адрес)",
|
||||
"validate_type": "Тип:",
|
||||
"validate_valid": "ДЕЙСТВИТЕЛЕН",
|
||||
"validating": "Проверка...",
|
||||
|
||||
+10
-8
@@ -786,13 +786,13 @@
|
||||
"faq_open_tooltip": "帮助与常见问题",
|
||||
"faq_search_hint": "搜索常见问题…",
|
||||
"faq_title": "帮助与常见问题",
|
||||
"faq_w_addr_1_a": "透明地址(以 R 或 t 开头)的工作方式与大多数币种相同:金额和地址在区块链上是公开的。\n\n隐私地址(z 地址,以 zs 开头)使用零知识加密技术——金额、发送方和接收方都在链上加密。存放在隐私地址中的余额是私密的。",
|
||||
"faq_w_addr_1_a": "屏蔽地址(z 地址,以 zs 开头)是您在 DragonX 上使用的地址。它们采用零知识密码学,金额、发送方和接收方在链上都是加密的,您的余额保持私密。\n\n透明地址(以 R 开头)是挖矿地址。DragonX 是完全私密的链,因此它们只能接收挖矿奖励——网络会拒绝任何发送给它们的付款。到达那里的奖励在被屏蔽之前是公开的,而钱包会自动完成屏蔽。",
|
||||
"faq_w_addr_1_q": "透明地址和隐私地址有什么区别?",
|
||||
"faq_w_addr_2_a": "请尽量使用隐私(z)地址——它们能保护您的余额和支付记录不被公开。DragonX 是一种隐私币,收款默认使用隐私地址。\n\n只有在您使用的服务无法处理隐私地址时,才使用透明地址。",
|
||||
"faq_w_addr_2_a": "收发任何款项都请使用屏蔽(zs)地址——在 DragonX 上只有这种地址可以接收付款。\n\n透明(挖矿)地址仅用于单人挖矿奖励。切勿把它提供给他人用于付款:网络会拒绝该付款。",
|
||||
"faq_w_addr_2_q": "应该使用哪种地址类型?",
|
||||
"faq_w_addr_3_a": "对于隐私地址,重复使用没有问题,也不会泄露历史记录。对于透明地址,每次付款使用新地址可以提升隐私性。可以在“接收”标签页创建新地址。",
|
||||
"faq_w_addr_3_a": "重复使用屏蔽地址没有问题,也不会泄露您的历史记录。您仍可以在“接收”页创建新地址,例如为每个联系人各建一个,以便知道是谁付的款。挖矿地址仅用于挖矿奖励,您很少需要新建。",
|
||||
"faq_w_addr_3_q": "应该重复使用地址,还是新建一个?",
|
||||
"faq_w_addr_4_a": "屏蔽会将币从透明地址转移到隐私地址,使其变为私密。新挖出的币到账时是透明的,会自动为您屏蔽。\n\n您也可以手动屏蔽;屏蔽只是一种特殊交易,因此需要正常的确认时间才能完成。",
|
||||
"faq_w_addr_4_a": "屏蔽是把币从透明(挖矿)地址转移到屏蔽地址,使其变为私密。新挖出的币会到达挖矿地址,并在成熟后自动屏蔽。\n\n您也可以在钱包中手动屏蔽;屏蔽只是一种特殊交易,因此需要正常的确认时间。",
|
||||
"faq_w_addr_4_q": "什么是“屏蔽”?",
|
||||
"faq_w_addr_title": "地址与隐私",
|
||||
"faq_w_bal_1_a": "在钱包同步完成之前,您的余额是不完整的,可能显示为 0——因为它还没有扫描完您的所有交易。同步完成后余额会自动补齐。\n\n请留意窗口底部的状态栏:它显示连接状态和区块高度。同步完成后,余额即为准确值。",
|
||||
@@ -836,11 +836,11 @@
|
||||
"faq_w_seed_4_a": "助记词是主要备份,足以恢复一切。钱包文件的副本是一个便捷的次要备份,还能保留标签和设置。请将所有备份妥善离线私密保管。",
|
||||
"faq_w_seed_4_q": "我还需要备份 wallet.dat 吗?",
|
||||
"faq_w_seed_title": "助记词与备份",
|
||||
"faq_w_send_1_a": "打开“发送”标签页,粘贴收款方地址,输入金额并确认。如果钱包已加密,系统会要求您解锁以完成交易。\n\n发送到隐私地址是私密的;发送到透明地址是公开的。",
|
||||
"faq_w_send_1_a": "打开“发送”标签页,粘贴收款方地址,输入金额并确认。如果钱包已加密,系统会要求您解锁以完成交易。\n\n付款发送到屏蔽(zs)地址,并保持私密。钱包不会向透明(挖矿)地址发送:网络会拒绝向此类地址付款。",
|
||||
"faq_w_send_1_q": "如何发送资金?",
|
||||
"faq_w_send_2_a": "DragonX 的手续费非常低,并且是自动设置的。手续费会在您确认前显示。通常无需手动更改。",
|
||||
"faq_w_send_2_q": "我需要支付多少手续费?",
|
||||
"faq_w_send_3_a": "可以——向隐私(z)地址发送时,您可以附加一条加密备注。只有收款方能够读取它。透明地址不支持备注。",
|
||||
"faq_w_send_3_a": "可以——向隐私(z)地址发送时,您可以附加一条加密备注。只有收款方能够读取它。",
|
||||
"faq_w_send_3_q": "我可以在付款时附加消息吗?",
|
||||
"faq_w_send_4_a": "交易在被打包进区块并获得确认之前处于「待处理」状态。这通常需要一两分钟。屏蔽交易还需要您的钱包已完成同步才能生成证明。\n\n如果一笔发款长时间处于待处理状态,请检查您的钱包是否已连接并同步(见窗口底部的状态栏)。",
|
||||
"faq_w_send_4_q": "我发送了一笔付款,但显示待处理——为什么?",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"miner_fee": "矿工费",
|
||||
"mining": "挖矿",
|
||||
"mining_active": "活跃",
|
||||
"mining_addr_explainer": "仅接收单人挖矿奖励——网络会拒绝向此地址付款。奖励成熟后会自动屏蔽。",
|
||||
"mining_address": "挖矿地址",
|
||||
"mining_address_copied": "挖矿地址已复制",
|
||||
"mining_all_time": "所有时间",
|
||||
@@ -1522,6 +1523,8 @@
|
||||
"ram_wallet_gb": "钱包:%.1f GB",
|
||||
"ram_wallet_mb": "钱包:%.0f MB",
|
||||
"receive": "接收",
|
||||
"receive_not_for_payments": "不用于付款",
|
||||
"receive_use_shielded": "使用屏蔽地址",
|
||||
"received": "已接收",
|
||||
"received_filter": "已接收",
|
||||
"received_label": "已接收",
|
||||
@@ -1550,7 +1553,6 @@
|
||||
"request_select_address": "选择地址...",
|
||||
"request_shielded_addrs": "-- 屏蔽地址 --",
|
||||
"request_title": "请求付款",
|
||||
"request_transparent_addrs": "-- 透明地址 --",
|
||||
"request_uri_copied": "付款 URI 已复制到剪贴板",
|
||||
"rescan": "重新扫描",
|
||||
"rescan_bootstrapped_msg": "您的节点使用了引导程序,因此快照以下的区块不在磁盘上,从创世区块重新扫描将失败。请从快照所包含的高度开始重新扫描,以核对钱包的已花费余额。您的 wallet.dat 和区块链数据不会被删除。",
|
||||
@@ -2134,6 +2136,7 @@
|
||||
"validate_description": "输入一个 DragonX 地址来检查它是否有效以及是否属于此钱包。",
|
||||
"validate_invalid": "无效",
|
||||
"validate_is_mine": "此钱包拥有该地址",
|
||||
"validate_mining_type": "挖矿地址(透明)",
|
||||
"validate_not_mine": "不属于此钱包",
|
||||
"validate_ownership": "所有权:",
|
||||
"validate_results": "结果:",
|
||||
@@ -2141,7 +2144,6 @@
|
||||
"validate_shielded_type": "屏蔽(z 地址)",
|
||||
"validate_status": "状态:",
|
||||
"validate_title": "验证地址",
|
||||
"validate_transparent_type": "透明(t 地址)",
|
||||
"validate_type": "类型:",
|
||||
"validate_valid": "有效",
|
||||
"validating": "验证中...",
|
||||
|
||||
@@ -25,6 +25,46 @@ def load_english_keys():
|
||||
|
||||
# All translations for the 158 missing keys
|
||||
TRANSLATIONS = {
|
||||
"mining_addr_explainer": {
|
||||
"de": "Empfängt nur Solo-Mining-Belohnungen – das Netzwerk lehnt Zahlungen an diese Adresse ab. Belohnungen werden automatisch abgeschirmt, sobald sie gereift sind.",
|
||||
"es": "Solo recibe recompensas de minería en solitario; la red rechaza los pagos a esta dirección. Las recompensas se blindan automáticamente cuando maduran.",
|
||||
"fr": "Ne reçoit que des récompenses de minage en solo — le réseau refuse les paiements vers cette adresse. Les récompenses sont blindées automatiquement une fois arrivées à maturité.",
|
||||
"pt": "Recebe apenas recompensas de mineração solo — a rede rejeita pagamentos para este endereço. As recompensas são blindadas automaticamente quando amadurecem.",
|
||||
"ru": "Принимает только награды за соло-майнинг — сеть отклоняет платежи на этот адрес. Награды экранируются автоматически, когда созревают.",
|
||||
"zh": "仅接收单人挖矿奖励——网络会拒绝向此地址付款。奖励成熟后会自动屏蔽。",
|
||||
"ja": "ソロマイニング報酬のみを受け取ります。ネットワークはこのアドレスへの支払いを拒否します。報酬は成熟すると自動的にシールドされます。",
|
||||
"ko": "솔로 채굴 보상만 받습니다. 네트워크가 이 주소로의 결제를 거부합니다. 보상은 성숙하면 자동으로 차폐됩니다.",
|
||||
},
|
||||
"receive_use_shielded": {
|
||||
"de": "Abgeschirmte Adresse verwenden",
|
||||
"es": "Usar una dirección blindada",
|
||||
"fr": "Utiliser une adresse blindée",
|
||||
"pt": "Usar um endereço blindado",
|
||||
"ru": "Использовать экранированный адрес",
|
||||
"zh": "使用屏蔽地址",
|
||||
"ja": "シールドアドレスを使う",
|
||||
"ko": "차폐 주소 사용",
|
||||
},
|
||||
"receive_not_for_payments": {
|
||||
"de": "Nicht für Zahlungen",
|
||||
"es": "No para pagos",
|
||||
"fr": "Pas pour les paiements",
|
||||
"pt": "Não é para pagamentos",
|
||||
"ru": "Не для платежей",
|
||||
"zh": "不用于付款",
|
||||
"ja": "支払いには使えません",
|
||||
"ko": "결제용이 아님",
|
||||
},
|
||||
"validate_mining_type": {
|
||||
"de": "Mining-Adresse (transparent)",
|
||||
"es": "Dirección de minería (transparente)",
|
||||
"fr": "Adresse de minage (transparente)",
|
||||
"pt": "Endereço de mineração (transparente)",
|
||||
"ru": "Адрес для майнинга (прозрачный)",
|
||||
"zh": "挖矿地址(透明)",
|
||||
"ja": "マイニングアドレス(透明)",
|
||||
"ko": "채굴 주소(투명)",
|
||||
},
|
||||
"send_mining_addr_short": {
|
||||
"de": "Mining-Adresse – kann keine Zahlungen empfangen",
|
||||
"es": "Dirección de minería: no puede recibir pagos",
|
||||
|
||||
+9
-1
@@ -19,6 +19,7 @@
|
||||
#include "ui/notifications.h"
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/send_tab.h"
|
||||
#include "ui/windows/receive_tab.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/export_transactions_dialog.h"
|
||||
#include "ui/windows/export_all_keys_dialog.h"
|
||||
@@ -102,7 +103,7 @@ void seedSweepContacts(App& a)
|
||||
s_contactsSweepBackup = a.addressBook().entries();
|
||||
data::AddressBookEntry e1("drgx pool payout address", kDemoZAddr, "mining pool payouts"); // Z, global
|
||||
e1.avatar = "icon:account_balance"; // icon avatar (verifies the icon-badge render path)
|
||||
data::AddressBookEntry e2("exchange deposit", "t1DemoTransparentAddressForUiSweep00000", ""); // T
|
||||
data::AddressBookEntry e2("mining rig", "t1DemoTransparentAddressForUiSweep00000", ""); // T (mining only)
|
||||
// Scope to the active wallet so it's visible (the tab hides contacts not in the active wallet);
|
||||
// non-empty hash also keeps it non-global -> no globe badge, so the list shows a global/non-global mix.
|
||||
e2.scope = a.activeWalletIdentityHash();
|
||||
@@ -370,6 +371,13 @@ void App::buildSweepCatalog()
|
||||
add("modal-validate-address", ui::NavPage::Receive,
|
||||
[](App&) { ui::ValidateAddressDialog::show(); },
|
||||
[](App&) { ui::ValidateAddressDialog::hide(); });
|
||||
add("modal-validate-mining", ui::NavPage::Receive,
|
||||
[](App&) { ui::ValidateAddressDialog::sweepShowMiningResult("t1DemoTransparentAddressForUiSweep00000"); },
|
||||
[](App&) { ui::ValidateAddressDialog::hide(); });
|
||||
// A mining (transparent) address selected on Receive: no payment request, no QR — the explainer instead.
|
||||
add("receive-mining-address", ui::NavPage::Receive,
|
||||
[](App& a) { ui::SweepSelectReceiveAddress(&a, "t1DemoTransparentAddressForUiSweep00000"); },
|
||||
[](App& a) { ui::SweepSelectReceiveAddress(&a, nullptr); });
|
||||
add("modal-address-label", ui::NavPage::Overview,
|
||||
[](App& a) { ui::AddressLabelDialog::show(&a, kDemoZAddr, true); },
|
||||
[](App&) { ui::AddressLabelDialog::hide(); });
|
||||
|
||||
@@ -546,7 +546,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("addr_type_mining");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
// A transparent row is already typed "Mining"; the tag is for shielded payout addresses.
|
||||
const char* miningTag = (row.mining && row.isZ) ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
@@ -655,7 +656,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
|
||||
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
||||
if (ImGui::IsItemHovered() && !s_dragActive) {
|
||||
material::Tooltip("%s", addr.address.c_str());
|
||||
if (row.isZ) material::Tooltip("%s", addr.address.c_str());
|
||||
else material::Tooltip("%s\n\n%s", addr.address.c_str(), TR("mining_addr_explainer"));
|
||||
}
|
||||
|
||||
// Context menu
|
||||
|
||||
+299
-207
@@ -130,13 +130,17 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
// Header row: label + address type toggle buttons
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("address_upper"));
|
||||
|
||||
// All / Shielded / Mining: each segment as wide as the widest label (they are words, not Z/T).
|
||||
const char* filterLabels[] = { TR("all_filter"), TR("shielded"), TR("addr_type_mining") };
|
||||
float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
|
||||
for (const char* lbl : filterLabels)
|
||||
toggleBtnW = std::max(toggleBtnW, Type().caption()->CalcTextSizeA(Type().caption()->LegacySize, FLT_MAX, 0, lbl).x +
|
||||
2.0f * Layout::spacingMd());
|
||||
float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
|
||||
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
|
||||
float segH = ImGui::GetFrameHeight();
|
||||
ImGui::SameLine(width - toggleTotalW);
|
||||
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
|
||||
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, toggleTotalW, segH,
|
||||
filterLabels, 3, s_addr_type_filter, Type().caption(),
|
||||
"##addrFilter", Layout::dpiScale());
|
||||
@@ -147,14 +151,18 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
|
||||
TrackNewAddresses(state);
|
||||
|
||||
// Auto-select address with the largest balance on first load
|
||||
// Auto-select the shielded address with the largest balance on first load. A mining (transparent)
|
||||
// address can't be paid, so it is picked only when the wallet has no shielded address yet.
|
||||
if (!s_auto_selected && app->isConnected() && !state.addresses.empty()) {
|
||||
int bestIdx = -1;
|
||||
double bestBal = -1.0;
|
||||
bool bestIsZ = false;
|
||||
for (size_t i = 0; i < state.addresses.size(); i++) {
|
||||
if (state.addresses[i].balance > bestBal) {
|
||||
const bool z = state.addresses[i].type == "shielded";
|
||||
if ((z && !bestIsZ) || (z == bestIsZ && state.addresses[i].balance > bestBal)) {
|
||||
bestBal = state.addresses[i].balance;
|
||||
bestIdx = static_cast<int>(i);
|
||||
bestIsZ = z;
|
||||
}
|
||||
}
|
||||
if (bestIdx >= 0) {
|
||||
@@ -222,6 +230,12 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
bool isZ = state.addresses[i].type == "shielded";
|
||||
if (s_addr_type_filter == 1 && !isZ) continue;
|
||||
if (s_addr_type_filter == 2 && isZ) continue;
|
||||
// "All" lists a mining address only when it matters: it holds rewards, it is marked as a
|
||||
// mining address, we are solo mining, or it is the one selected. The Mining filter lists all.
|
||||
if (s_addr_type_filter == 0 && !isZ && state.addresses[i].balance < 1e-9 &&
|
||||
!app->isMiningAddress(state.addresses[i].address) && !state.mining.generate &&
|
||||
s_selected_address_idx != static_cast<int>(i))
|
||||
continue;
|
||||
sortedIdx.push_back(i);
|
||||
}
|
||||
std::sort(sortedIdx.begin(), sortedIdx.end(),
|
||||
@@ -272,9 +286,12 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
s_cached_qr_data.clear(); // Force QR regeneration
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip(TR("grpb_tooltip_address_balance"),
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isCurrent ? TR("grpb_selected_suffix") : "");
|
||||
char tip[512];
|
||||
snprintf(tip, sizeof(tip), TR("grpb_tooltip_address_balance"),
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isCurrent ? TR("grpb_selected_suffix") : "");
|
||||
if (isZ) material::Tooltip("%s", tip);
|
||||
else material::Tooltip("%s\n\n%s", tip, TR("mining_addr_explainer"));
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -337,6 +354,60 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
void SweepSelectReceiveAddress(App* app, const char* address) {
|
||||
(void)app;
|
||||
if (address) {
|
||||
s_pending_select_address = address;
|
||||
s_addr_type_filter = 0;
|
||||
} else {
|
||||
s_auto_selected = false; // re-run the default (shielded-first) choice
|
||||
}
|
||||
s_cached_qr_data.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mining address notice (instead of a payment request)
|
||||
// ============================================================================
|
||||
// A mining (transparent) address only ever receives mining rewards: the network rejects payments to it.
|
||||
// So it gets no payment request and no QR code — just what it is for, and a way to a shielded address.
|
||||
static void RenderMiningAddressNotice(App* app, const WalletState& state, float width) {
|
||||
Type().textColored(TypeStyle::Overline, Warning(), TR("mining_address"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width);
|
||||
Type().textWrapped(TypeStyle::Body2, TR("mining_addr_explainer"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
int bestZ = -1;
|
||||
double bestBal = -1.0;
|
||||
for (size_t i = 0; i < state.addresses.size(); i++) {
|
||||
if (state.addresses[i].type == "shielded" && state.addresses[i].balance > bestBal) {
|
||||
bestBal = state.addresses[i].balance;
|
||||
bestZ = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
ImGui::BeginDisabled(bestZ < 0 && (!app->isConnected() || s_generating_address));
|
||||
if (TactileButton(TrId("receive_use_shielded", "mining").c_str(), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
if (bestZ >= 0) {
|
||||
s_selected_address_idx = bestZ;
|
||||
s_cached_qr_data.clear();
|
||||
} else {
|
||||
s_generating_address = true; // no shielded address yet: make one, as "New" does
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_shielded"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success(TR("new_shielded_created"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(width, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers: DrawRecvIcon (local copy — original is static in send_tab).
|
||||
// Relative time uses the shared util::formatTimeAgoShort ("14d ago"), matching Overview/Send.
|
||||
@@ -580,9 +651,9 @@ void RenderReceiveTab(App* app)
|
||||
const AddressInfo& selected = state.addresses[s_selected_address_idx];
|
||||
bool isZ = selected.type == "shielded";
|
||||
|
||||
// Generate QR data
|
||||
std::string qr_data = selected.address;
|
||||
if (s_request_amount > 0) {
|
||||
// Generate QR data — shielded only: a mining (transparent) address can't be paid, so it gets no QR.
|
||||
std::string qr_data = isZ ? selected.address : std::string();
|
||||
if (isZ && s_request_amount > 0) {
|
||||
// Canonical, URL-encoded "drgx:" URI via the shared builder. The old inline code emitted an
|
||||
// unparseable "dragonx:" scheme and appended the memo raw (spaces/&/= corrupted the QR).
|
||||
qr_data = util::buildPaymentUri(selected.address, s_request_amount, "",
|
||||
@@ -593,8 +664,10 @@ void RenderReceiveTab(App* app)
|
||||
FreeQRTexture(s_qr_texture);
|
||||
s_qr_texture = 0;
|
||||
}
|
||||
int w, h;
|
||||
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
|
||||
if (!qr_data.empty()) {
|
||||
int w, h;
|
||||
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
|
||||
}
|
||||
s_cached_qr_data = qr_data;
|
||||
}
|
||||
|
||||
@@ -644,205 +717,210 @@ void RenderReceiveTab(App* app)
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// ---- PAYMENT REQUEST ----
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Amount input with currency toggle
|
||||
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
|
||||
float amtInputW = addrColW - toggleW - Layout::spacingMd();
|
||||
if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
|
||||
double usd_price = state.market.price_usd;
|
||||
|
||||
ImGui::PushFont(body2);
|
||||
if (s_request_usd_mode && usd_price > 0) {
|
||||
ImGui::PushItemWidth(amtInputW);
|
||||
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
|
||||
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
|
||||
s_request_amount = s_request_usd_amount / usd_price;
|
||||
}
|
||||
{
|
||||
ImVec2 iMin = ImGui::GetItemRectMin();
|
||||
ImVec2 iMax = ImGui::GetItemRectMax();
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.4f %s", s_request_amount, DRAGONX_TICKER);
|
||||
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
|
||||
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
} else {
|
||||
ImGui::PushItemWidth(amtInputW);
|
||||
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
|
||||
if (s_request_amount < 0) s_request_amount = 0;
|
||||
if (usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
if (usd_price > 0 && s_request_amount > 0) {
|
||||
ImVec2 iMin = ImGui::GetItemRectMin();
|
||||
ImVec2 iMax = ImGui::GetItemRectMax();
|
||||
double usd_value = s_request_amount * usd_price;
|
||||
if (usd_value >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd_value);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.6f", usd_value);
|
||||
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
|
||||
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
|
||||
// Toggle button
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
{
|
||||
const char* currLabel = s_request_usd_mode ? "DRGX" : "USD";
|
||||
bool canToggle = (usd_price > 0);
|
||||
ImGui::BeginDisabled(!canToggle);
|
||||
if (TactileButton("##ToggleCurrencyRecv", ImVec2(toggleW, 0), S.resolveFont("button"))) {
|
||||
s_request_usd_mode = !s_request_usd_mode;
|
||||
if (s_request_usd_mode && usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
{
|
||||
ImVec2 bMin = ImGui::GetItemRectMin();
|
||||
ImVec2 bMax = ImGui::GetItemRectMax();
|
||||
float bH = bMax.y - bMin.y;
|
||||
ImFont* font = ImGui::GetFont();
|
||||
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
|
||||
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
|
||||
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
|
||||
float totalW2 = iconW + iconGap2 + textSz.x;
|
||||
float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
|
||||
float cy = bMin.y + bH * 0.5f;
|
||||
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
float ss = iconW * 0.5f;
|
||||
float cx = startX + ss;
|
||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
|
||||
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
|
||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
|
||||
float ay1 = cy - ag;
|
||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx + al, ay1),
|
||||
ImVec2(cx + al - hss, ay1 - hss),
|
||||
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
||||
float ay2 = cy + ag;
|
||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx - al, ay2),
|
||||
ImVec2(cx - al + hss, ay2 - hss),
|
||||
ImVec2(cx - al + hss, ay2 + hss), iconCol);
|
||||
float tx = startX + iconW + iconGap2;
|
||||
float ty = cy - textSz.y * 0.5f;
|
||||
dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
// Preset amount chips
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
|
||||
|
||||
struct Preset { const char* label; double amount; };
|
||||
Preset presets[] = {
|
||||
{ "0.1", 0.1 },
|
||||
{ "1", 1.0 },
|
||||
{ "10", 10.0 },
|
||||
{ "100", 100.0 },
|
||||
{ "1K", 1000.0 },
|
||||
{ "10K", 10000.0 },
|
||||
{ "50K", 50000.0 },
|
||||
{ "100K", 100000.0 },
|
||||
};
|
||||
constexpr int presetCount = 8;
|
||||
|
||||
// Calculate equal chip widths to span full addrColW
|
||||
float totalGap = chipGap * (presetCount - 1);
|
||||
float chipW = (addrColW - totalGap) / presetCount;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, chipRound);
|
||||
|
||||
ImFont* chipFont = S.resolveFont("button");
|
||||
|
||||
for (int ci = 0; ci < presetCount; ci++) {
|
||||
if (ci > 0) ImGui::SameLine(0, chipGap);
|
||||
|
||||
bool active = false;
|
||||
if (s_request_usd_mode) {
|
||||
active = (usd_price > 0 && std::abs(s_request_usd_amount - presets[ci].amount) < 0.001);
|
||||
} else {
|
||||
active = (std::abs(s_request_amount - presets[ci].amount) < 1e-8);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "chip-active-bg-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
}
|
||||
|
||||
char chipId[32];
|
||||
snprintf(chipId, sizeof(chipId), "%s##presetAmt%d", presets[ci].label, ci);
|
||||
if (TactileButton(chipId, ImVec2(chipW, chipH), chipFont)) {
|
||||
if (s_request_usd_mode) {
|
||||
s_request_usd_amount = presets[ci].amount;
|
||||
if (usd_price > 0)
|
||||
s_request_amount = s_request_usd_amount / usd_price;
|
||||
} else {
|
||||
s_request_amount = presets[ci].amount;
|
||||
if (usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
}
|
||||
|
||||
if (active) ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Memo (z-addresses only)
|
||||
if (isZ) {
|
||||
// ---- PAYMENT REQUEST ----
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
|
||||
|
||||
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
|
||||
ImGui::PushItemWidth(addrColW);
|
||||
ImGui::InputTextMultiline("##RequestMemo", s_request_memo, sizeof(s_request_memo),
|
||||
ImVec2(addrColW, memoInputH));
|
||||
ImGui::PopItemWidth();
|
||||
// Amount input with currency toggle
|
||||
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
|
||||
float amtInputW = addrColW - toggleW - Layout::spacingMd();
|
||||
if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
|
||||
double usd_price = state.market.price_usd;
|
||||
|
||||
// The Sapling memo limit is in BYTES and matches the enforced buffer size
|
||||
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
|
||||
// old display-only 256 value, and warn in colour once the field is at its cap.
|
||||
size_t memo_len = strlen(s_request_memo);
|
||||
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
|
||||
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
|
||||
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||
}
|
||||
ImGui::PushFont(body2);
|
||||
if (s_request_usd_mode && usd_price > 0) {
|
||||
ImGui::PushItemWidth(amtInputW);
|
||||
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
|
||||
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
|
||||
s_request_amount = s_request_usd_amount / usd_price;
|
||||
}
|
||||
{
|
||||
ImVec2 iMin = ImGui::GetItemRectMin();
|
||||
ImVec2 iMax = ImGui::GetItemRectMax();
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.4f %s", s_request_amount, DRAGONX_TICKER);
|
||||
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
|
||||
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
} else {
|
||||
ImGui::PushItemWidth(amtInputW);
|
||||
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
|
||||
if (s_request_amount < 0) s_request_amount = 0;
|
||||
if (usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
if (usd_price > 0 && s_request_amount > 0) {
|
||||
ImVec2 iMin = ImGui::GetItemRectMin();
|
||||
ImVec2 iMax = ImGui::GetItemRectMax();
|
||||
double usd_value = s_request_amount * usd_price;
|
||||
if (usd_value >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd_value);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.6f", usd_value);
|
||||
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
|
||||
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
|
||||
// URI preview
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
|
||||
float uriWrapW = addrColW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImGui::GetCursorScreenPos(),
|
||||
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
|
||||
ImVec2 uriSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
|
||||
uriWrapW, s_cached_qr_data.c_str());
|
||||
ImGui::Dummy(ImVec2(addrColW, uriSz.y + Layout::spacingSm()));
|
||||
// Toggle button
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
{
|
||||
const char* currLabel = s_request_usd_mode ? "DRGX" : "USD";
|
||||
bool canToggle = (usd_price > 0);
|
||||
ImGui::BeginDisabled(!canToggle);
|
||||
if (TactileButton("##ToggleCurrencyRecv", ImVec2(toggleW, 0), S.resolveFont("button"))) {
|
||||
s_request_usd_mode = !s_request_usd_mode;
|
||||
if (s_request_usd_mode && usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
{
|
||||
ImVec2 bMin = ImGui::GetItemRectMin();
|
||||
ImVec2 bMax = ImGui::GetItemRectMax();
|
||||
float bH = bMax.y - bMin.y;
|
||||
ImFont* font = ImGui::GetFont();
|
||||
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
|
||||
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
|
||||
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
|
||||
float totalW2 = iconW + iconGap2 + textSz.x;
|
||||
float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
|
||||
float cy = bMin.y + bH * 0.5f;
|
||||
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
float ss = iconW * 0.5f;
|
||||
float cx = startX + ss;
|
||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
|
||||
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
|
||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
|
||||
float ay1 = cy - ag;
|
||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx + al, ay1),
|
||||
ImVec2(cx + al - hss, ay1 - hss),
|
||||
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
||||
float ay2 = cy + ag;
|
||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx - al, ay2),
|
||||
ImVec2(cx - al + hss, ay2 - hss),
|
||||
ImVec2(cx - al + hss, ay2 + hss), iconCol);
|
||||
float tx = startX + iconW + iconGap2;
|
||||
float ty = cy - textSz.y * 0.5f;
|
||||
dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
// Preset amount chips
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
|
||||
|
||||
struct Preset { const char* label; double amount; };
|
||||
Preset presets[] = {
|
||||
{ "0.1", 0.1 },
|
||||
{ "1", 1.0 },
|
||||
{ "10", 10.0 },
|
||||
{ "100", 100.0 },
|
||||
{ "1K", 1000.0 },
|
||||
{ "10K", 10000.0 },
|
||||
{ "50K", 50000.0 },
|
||||
{ "100K", 100000.0 },
|
||||
};
|
||||
constexpr int presetCount = 8;
|
||||
|
||||
// Calculate equal chip widths to span full addrColW
|
||||
float totalGap = chipGap * (presetCount - 1);
|
||||
float chipW = (addrColW - totalGap) / presetCount;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, chipRound);
|
||||
|
||||
ImFont* chipFont = S.resolveFont("button");
|
||||
|
||||
for (int ci = 0; ci < presetCount; ci++) {
|
||||
if (ci > 0) ImGui::SameLine(0, chipGap);
|
||||
|
||||
bool active = false;
|
||||
if (s_request_usd_mode) {
|
||||
active = (usd_price > 0 && std::abs(s_request_usd_amount - presets[ci].amount) < 0.001);
|
||||
} else {
|
||||
active = (std::abs(s_request_amount - presets[ci].amount) < 1e-8);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "chip-active-bg-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
}
|
||||
|
||||
char chipId[32];
|
||||
snprintf(chipId, sizeof(chipId), "%s##presetAmt%d", presets[ci].label, ci);
|
||||
if (TactileButton(chipId, ImVec2(chipW, chipH), chipFont)) {
|
||||
if (s_request_usd_mode) {
|
||||
s_request_usd_amount = presets[ci].amount;
|
||||
if (usd_price > 0)
|
||||
s_request_amount = s_request_usd_amount / usd_price;
|
||||
} else {
|
||||
s_request_amount = presets[ci].amount;
|
||||
if (usd_price > 0)
|
||||
s_request_usd_amount = s_request_amount * usd_price;
|
||||
}
|
||||
}
|
||||
|
||||
if (active) ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Memo (z-addresses only)
|
||||
if (isZ) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
|
||||
|
||||
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
|
||||
ImGui::PushItemWidth(addrColW);
|
||||
ImGui::InputTextMultiline("##RequestMemo", s_request_memo, sizeof(s_request_memo),
|
||||
ImVec2(addrColW, memoInputH));
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// The Sapling memo limit is in BYTES and matches the enforced buffer size
|
||||
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
|
||||
// old display-only 256 value, and warn in colour once the field is at its cap.
|
||||
size_t memo_len = strlen(s_request_memo);
|
||||
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
|
||||
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
|
||||
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// URI preview
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
|
||||
float uriWrapW = addrColW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImGui::GetCursorScreenPos(),
|
||||
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
|
||||
ImVec2 uriSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
|
||||
uriWrapW, s_cached_qr_data.c_str());
|
||||
ImGui::Dummy(ImVec2(addrColW, uriSz.y + Layout::spacingSm()));
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
|
||||
}
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
|
||||
// A mining (transparent) address can't be paid: no payment request and no QR code.
|
||||
RenderMiningAddressNotice(app, state, addrColW);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -874,7 +952,21 @@ void RenderReceiveTab(App* app)
|
||||
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
|
||||
if (s_qr_texture) {
|
||||
if (!isZ) {
|
||||
// A mining address: nothing to scan. Mark the panel so the empty square isn't a puzzle.
|
||||
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||
ImFont* iconF = Type().iconXL();
|
||||
const char* noPay = TR("receive_not_for_payments");
|
||||
ImVec2 iSz = iconF->CalcTextSizeA(iconF->LegacySize, FLT_MAX, 0, ICON_MD_CONSTRUCTION);
|
||||
ImVec2 tSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, qrSize, noPay);
|
||||
const float blockH = iSz.y + Layout::spacingSm() + tSz.y;
|
||||
const float cx = qrPanelMin.x + totalQrSize * 0.5f;
|
||||
const float top = qrPanelMin.y + (totalQrSize - blockH) * 0.5f;
|
||||
dl->AddText(iconF, iconF->LegacySize, ImVec2(cx - iSz.x * 0.5f, top), WithAlpha(Warning(), 200),
|
||||
ICON_MD_CONSTRUCTION);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx - tSz.x * 0.5f, top + iSz.y + Layout::spacingSm()),
|
||||
OnSurfaceDisabled(), noPay, nullptr, qrSize);
|
||||
} else if (s_qr_texture) {
|
||||
RenderQRCode(s_qr_texture, qrSize);
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||
@@ -886,12 +978,12 @@ void RenderReceiveTab(App* app)
|
||||
|
||||
ImGui::SetCursorScreenPos(qrPanelMin);
|
||||
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
if (isZ && ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s",
|
||||
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (isZ && ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(qr_data.c_str());
|
||||
Notifications::instance().info(s_request_amount > 0
|
||||
? TR("payment_uri_copied") : TR("address_copied"));
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace ui {
|
||||
* Shows addresses for receiving funds with QR codes
|
||||
*/
|
||||
void RenderReceiveTab(App* app);
|
||||
// UI sweep: select `address` on the Receive tab (nullptr: back to the default choice).
|
||||
void SweepSelectReceiveAddress(App* app, const char* address);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/payment_uri.h"
|
||||
#include "../../util/address_validation.h"
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../widgets/qr_code.h"
|
||||
@@ -47,8 +48,10 @@ void RequestPaymentDialog::show(const std::string& address)
|
||||
s_label[0] = '\0';
|
||||
s_uri_dirty = true;
|
||||
|
||||
if (!address.empty()) {
|
||||
// Only a shielded address can be paid (DragonX is a private chain): never prefill a mining address.
|
||||
if (!address.empty() && !util::isTransparentAddress(address)) {
|
||||
strncpy(s_address, address.c_str(), sizeof(s_address) - 1);
|
||||
s_address[sizeof(s_address) - 1] = '\0';
|
||||
} else {
|
||||
s_address[0] = '\0';
|
||||
}
|
||||
@@ -78,9 +81,6 @@ void RequestPaymentDialog::render(App* app)
|
||||
auto zAddrLbl = S.label("dialogs.request-payment", "z-addr-label");
|
||||
auto zAddrFrontLbl = S.label("dialogs.request-payment", "z-addr-front-label");
|
||||
auto zAddrBackLbl = S.label("dialogs.request-payment", "z-addr-back-label");
|
||||
auto tAddrLbl = S.label("dialogs.request-payment", "t-addr-label");
|
||||
auto tAddrFrontLbl = S.label("dialogs.request-payment", "t-addr-front-label");
|
||||
auto tAddrBackLbl = S.label("dialogs.request-payment", "t-addr-back-label");
|
||||
auto amountInput = S.input("dialogs.request-payment", "amount-input");
|
||||
auto memoInput = S.input("dialogs.request-payment", "memo-input");
|
||||
auto qr = S.drawElement("dialogs.request-payment", "qr-code");
|
||||
@@ -124,22 +124,8 @@ void RequestPaymentDialog::render(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// T-addresses
|
||||
if (!state.t_addresses.empty()) {
|
||||
ImGui::TextDisabled("%s", TR("request_transparent_addrs"));
|
||||
for (size_t i = 0; i < state.t_addresses.size(); i++) {
|
||||
const auto& addr = state.t_addresses[i];
|
||||
std::string label = addr.address;
|
||||
if (label.length() > static_cast<size_t>(tAddrLbl.truncate)) {
|
||||
label = label.substr(0, tAddrFrontLbl.truncate) + "..." + label.substr(label.length() - tAddrBackLbl.truncate);
|
||||
}
|
||||
|
||||
if (ImGui::Selectable(label.c_str(), addr.address == s_address)) {
|
||||
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
||||
s_uri_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// No mining (transparent) addresses: they only receive mining rewards, so a request to one
|
||||
// could never be paid.
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ bool ValidateAddressDialog::s_validating = false;
|
||||
char ValidateAddressDialog::s_address_input[512] = "";
|
||||
bool ValidateAddressDialog::s_is_valid = false;
|
||||
bool ValidateAddressDialog::s_is_mine = false;
|
||||
bool ValidateAddressDialog::s_is_mining = false;
|
||||
std::string ValidateAddressDialog::s_address_type;
|
||||
std::string ValidateAddressDialog::s_error_message;
|
||||
|
||||
@@ -33,10 +34,22 @@ void ValidateAddressDialog::show()
|
||||
s_address_input[0] = '\0';
|
||||
s_is_valid = false;
|
||||
s_is_mine = false;
|
||||
s_is_mining = false;
|
||||
s_address_type.clear();
|
||||
s_error_message.clear();
|
||||
}
|
||||
|
||||
void ValidateAddressDialog::sweepShowMiningResult(const char* address)
|
||||
{
|
||||
show();
|
||||
snprintf(s_address_input, sizeof(s_address_input), "%s", address);
|
||||
s_validated = true;
|
||||
s_is_valid = true;
|
||||
s_is_mine = true;
|
||||
s_is_mining = true;
|
||||
s_address_type = TR("validate_mining_type");
|
||||
}
|
||||
|
||||
bool ValidateAddressDialog::isOpen()
|
||||
{
|
||||
return s_open;
|
||||
@@ -112,6 +125,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
s_is_valid = valid;
|
||||
s_is_mine = mine;
|
||||
s_address_type = TR("validate_shielded_type");
|
||||
s_is_mining = false;
|
||||
} else {
|
||||
s_error_message = error;
|
||||
s_is_valid = false;
|
||||
@@ -138,7 +152,8 @@ void ValidateAddressDialog::render(App* app)
|
||||
if (error.empty()) {
|
||||
s_is_valid = valid;
|
||||
s_is_mine = mine;
|
||||
s_address_type = TR("validate_transparent_type");
|
||||
s_address_type = TR("validate_mining_type");
|
||||
s_is_mining = true;
|
||||
} else {
|
||||
s_error_message = error;
|
||||
s_is_valid = false;
|
||||
@@ -204,6 +219,12 @@ void ValidateAddressDialog::render(App* app)
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", TR("validate_not_mine"));
|
||||
}
|
||||
if (s_is_mining) { // valid, but not for payments
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||
ImGui::TextWrapped("%s", TR("mining_addr_explainer"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!app->isConnected()) {
|
||||
|
||||
@@ -38,6 +38,9 @@ public:
|
||||
*/
|
||||
static void hide();
|
||||
|
||||
/** UI sweep: show the result for a valid mining (transparent) address of ours. */
|
||||
static void sweepShowMiningResult(const char* address);
|
||||
|
||||
private:
|
||||
static bool s_open;
|
||||
static bool s_validated;
|
||||
@@ -47,6 +50,7 @@ private:
|
||||
// Validation results
|
||||
static bool s_is_valid;
|
||||
static bool s_is_mine;
|
||||
static bool s_is_mining; // a valid transparent (mining) address: explain it can't be paid
|
||||
static std::string s_address_type;
|
||||
static std::string s_error_message;
|
||||
};
|
||||
|
||||
+10
-8
@@ -1632,21 +1632,21 @@ void I18n::loadBuiltinEnglish()
|
||||
|
||||
// Wallet > Addresses & Privacy
|
||||
strings_["faq_w_addr_1_q"] = "What's the difference between transparent and shielded addresses?";
|
||||
strings_["faq_w_addr_1_a"] = "Transparent addresses (they start with R or t) work like most coins: the amounts and addresses are public on the blockchain.\n\nShielded addresses (z-addresses, starting with zs) use zero-knowledge cryptography \xE2\x80\x94 the amount, sender, and receiver are encrypted on-chain. Balances held in shielded addresses are private.";
|
||||
strings_["faq_w_addr_1_a"] = "Shielded addresses (z-addresses, starting with zs) are the ones you use on DragonX. They use zero-knowledge cryptography, so the amount, sender and receiver are encrypted on-chain and your balance stays private.\n\nTransparent addresses (starting with R) are mining addresses. DragonX is a fully private chain, so they can only receive mining rewards — the network rejects any payment sent to one. Rewards that arrive there are public until they are shielded, which the wallet does automatically.";
|
||||
strings_["faq_w_addr_2_q"] = "Which address type should I use?";
|
||||
strings_["faq_w_addr_2_a"] = "Prefer shielded (z) addresses whenever possible \xE2\x80\x94 they keep your balance and payment history private. DragonX is a privacy coin and shielded is the default for received funds.\n\nUse a transparent address only when a service you interact with cannot handle shielded addresses.";
|
||||
strings_["faq_w_addr_2_a"] = "Use a shielded (zs) address for everything you send and receive — on DragonX it is the only kind that can be paid.\n\nTransparent (mining) addresses are only for solo-mining rewards. Never give one out for a payment: the network will reject it.";
|
||||
strings_["faq_w_addr_3_q"] = "Should I reuse an address or make a new one?";
|
||||
strings_["faq_w_addr_3_a"] = "For shielded addresses, reuse is fine and does not leak history. For transparent addresses, using a fresh address per payment improves privacy. Create new addresses from the Receive tab.";
|
||||
strings_["faq_w_addr_3_a"] = "Reusing a shielded address is fine and does not leak your history. You can still create new ones from the Receive tab, for example one per contact to see who paid you. Mining addresses are only for mining rewards; you rarely need a new one.";
|
||||
strings_["faq_w_addr_4_q"] = "What is 'shielding'?";
|
||||
strings_["faq_w_addr_4_a"] = "Shielding moves coins from a transparent address into a shielded one, making them private. Newly mined coins arrive transparent and are shielded for you automatically.\n\nYou can also shield manually from the wallet; a shield is just a special transaction, so it takes a normal confirmation time to complete.";
|
||||
strings_["faq_w_addr_4_a"] = "Shielding moves coins from a transparent (mining) address into a shielded one, making them private. Newly mined coins arrive at a mining address and are shielded for you automatically once they mature.\n\nYou can also shield manually from the wallet; a shield is just a special transaction, so it takes a normal confirmation time to complete.";
|
||||
|
||||
// Wallet > Sending & Receiving
|
||||
strings_["faq_w_send_1_q"] = "How do I send funds?";
|
||||
strings_["faq_w_send_1_a"] = "Open the Send tab, paste the recipient's address, enter an amount, and confirm. If the wallet is encrypted you'll be asked to unlock it for the transaction.\n\nSends to shielded addresses are private; sends to transparent addresses are public.";
|
||||
strings_["faq_w_send_1_a"] = "Open the Send tab, paste the recipient's address, enter an amount, and confirm. If the wallet is encrypted you'll be asked to unlock it for the transaction.\n\nPayments go to shielded (zs) addresses and stay private. The wallet won't send to a transparent (mining) address: the network rejects payments to one.";
|
||||
strings_["faq_w_send_2_q"] = "What fee do I pay?";
|
||||
strings_["faq_w_send_2_a"] = "DragonX fees are very low and are set automatically. The fee is shown before you confirm. You do not normally need to change it.";
|
||||
strings_["faq_w_send_3_q"] = "Can I attach a message to a payment?";
|
||||
strings_["faq_w_send_3_a"] = "Yes \xE2\x80\x94 when sending to a shielded (z) address you can include an encrypted memo. Only the recipient can read it. Transparent addresses do not support memos.";
|
||||
strings_["faq_w_send_3_a"] = "Yes — when sending to a shielded (z) address you can include an encrypted memo. Only the recipient can read it.";
|
||||
strings_["faq_w_send_4_q"] = "I sent a payment but it says pending \xE2\x80\x94 why?";
|
||||
strings_["faq_w_send_4_a"] = "A transaction is 'pending' until it is mined into a block and gains confirmations. This usually takes a minute or two. Shielded transactions also need your wallet to be synced to build the proof.\n\nIf a send stays pending unusually long, check that your wallet is connected and synced (see the status bar at the bottom of the window).";
|
||||
|
||||
@@ -2181,6 +2181,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["contact_chip_mining"] = "Mining only";
|
||||
strings_["failed_create_mining_address"] = "Failed to create mining address";
|
||||
strings_["new_mining_address_created"] = "New mining address created";
|
||||
strings_["mining_addr_explainer"] = "Receives solo-mining rewards only — the network rejects payments to it. Rewards are shielded automatically once they mature.";
|
||||
strings_["receive_use_shielded"] = "Use a shielded address";
|
||||
strings_["receive_not_for_payments"] = "Not for payments";
|
||||
strings_["validate_mining_type"] = "Mining address (transparent)";
|
||||
strings_["mining_est_daily_pool_sub"] = "rough solo-equivalent, before pool fee";
|
||||
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
|
||||
strings_["mining_pool_needs_payout_tooltip"] = "Enter a payout address first (generate a Z address)";
|
||||
@@ -2414,7 +2418,6 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["request_select_address"] = "Select address...";
|
||||
strings_["request_shielded_addrs"] = "-- Shielded Addresses --";
|
||||
strings_["request_title"] = "Request Payment";
|
||||
strings_["request_transparent_addrs"] = "-- Transparent Addresses --";
|
||||
strings_["request_uri_copied"] = "Payment URI copied to clipboard";
|
||||
|
||||
// --- Send Tab ---
|
||||
@@ -2542,7 +2545,6 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["validate_shielded_type"] = "Shielded (z-address)";
|
||||
strings_["validate_status"] = "Status:";
|
||||
strings_["validate_title"] = "Validate Address";
|
||||
strings_["validate_transparent_type"] = "Transparent (t-address)";
|
||||
strings_["validate_type"] = "Type:";
|
||||
strings_["validate_valid"] = "VALID";
|
||||
|
||||
|
||||
@@ -4464,7 +4464,8 @@ void testMiningAddressStringsTranslated()
|
||||
"tt_new_mining_address", "mining_address", "addr_type_mining", "mining_rewards", "mining_rewards_unshielded",
|
||||
"baltab_mining_addresses_count", "baltab_mining_amount", "balance_mining_fmt", "mining_card_tooltip",
|
||||
"mining_card_click_to_shield", "contact_chip_mining", "failed_create_mining_address",
|
||||
"new_mining_address_created",
|
||||
"new_mining_address_created", "mining_addr_explainer", "receive_use_shielded", "receive_not_for_payments",
|
||||
"validate_mining_type", "faq_w_addr_1_a", "faq_w_addr_2_a", "faq_w_addr_3_a", "faq_w_addr_4_a", "faq_w_send_1_a", "faq_w_send_3_a",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user