test(fake-daemon): add slow rescan and key-import methods
fake_dragonxd now answers rescan, importprivkey, z_importkey and z_importviewingkey, so the GUI's long wallet RPCs can be reproduced without a real node. - Each one holds cs_main for FAKE_RESCAN_SECS (default 0). The lock-free getrescaninfo reports the scan while it runs, and, like v1.3.0, the scan stops early when a shutdown is requested. - Argument checks, error codes and return values follow dragonxd (z_importkey returns null, importprivkey the t-address, the default "whenkeyisnew" skips a known key, z_importviewingkey needs its zaddr). Keys are only prefix-checked and never stored or logged; an imported spending key gets a made-up address that the address lists then show. - FAKE_ENCRYPTED=1 / FAKE_WALLET_PASSPHRASE: an encrypted wallet that starts locked, with walletpassphrase (-14 on a wrong passphrase), walletpassphrasechange, walletlock and getwalletinfo unlocked_until; the imports answer -13 while it is locked. As in dragonxd, the right passphrase with a non-integer timeout unlocks first and only then fails (-1): the wallet stays unlocked with no relock timer of its own. A timer pending from an earlier unlock keeps running; otherwise only walletlock locks it again. Without FAKE_ENCRYPTED, those three methods answer -15 like dragonxd. - FAKE_METHODS_ALL call events show keys and passphrases as <redacted>. A plain run is unchanged: the same 63 GUI calls before and after, and identical replies to every one of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,22 @@ What it copies from the real daemon (DragonX v1.3.0):
|
|||||||
Its replies are canned: `getinfo` reports version `1030050`, the balance is fixed, and there are two
|
Its replies are canned: `getinfo` reports version `1030050`, the balance is fixed, and there are two
|
||||||
peers on TEST-NET addresses. Unknown methods get the real `-32601 Method not found` (HTTP 404).
|
peers on TEST-NET addresses. Unknown methods get the real `-32601 Method not found` (HTTP 404).
|
||||||
|
|
||||||
|
Rescan, key imports and wallet encryption follow dragonxd's argument checks, error codes and return values:
|
||||||
|
|
||||||
|
- `rescan`, `importprivkey`, `z_importkey` and `z_importviewingkey` run a wallet scan that holds `cs_main`
|
||||||
|
for `FAKE_RESCAN_SECS`. While it runs, `getrescaninfo` (lock-free) reports `rescanning` with its progress,
|
||||||
|
and a `Still rescanning` line is logged once a minute. Like v1.3.0, the scan stops early when a shutdown
|
||||||
|
is requested. `z_importviewingkey` needs its 4th argument (the zaddr), as in dragonxd.
|
||||||
|
- Keys are only prefix-checked: a 51/52-character base58 WIF, `secret-extended-key-main1…` or `zivks1…`.
|
||||||
|
Only a fingerprint is kept; the keys are never stored or logged. An imported spending key gets a
|
||||||
|
made-up address, which `z_listaddresses`/`getaddressesbyaccount` list from then on. A second import of
|
||||||
|
the same key with the default `whenkeyisnew` returns at once, with no scan.
|
||||||
|
- With `FAKE_ENCRYPTED=1` the wallet is encrypted and starts locked. `walletpassphrase` (a wrong passphrase
|
||||||
|
gives `-14`), `walletpassphrasechange` and `walletlock` work, `getwalletinfo` carries `unlocked_until`,
|
||||||
|
and the imports answer `-13` while the wallet is locked. As in dragonxd, the right passphrase with a
|
||||||
|
non-integer timeout unlocks first and then fails (`-1`), so the wallet stays unlocked with no relock.
|
||||||
|
Without it those three methods answer `-15` ("running with an unencrypted wallet"), like dragonxd.
|
||||||
|
|
||||||
## Events file
|
## Events file
|
||||||
|
|
||||||
Everything that matters for a test is appended to `<datadir>/fake_dragonxd.events`, one
|
Everything that matters for a test is appended to `<datadir>/fake_dragonxd.events`, one
|
||||||
@@ -54,6 +70,10 @@ Everything that matters for a test is appended to `<datadir>/fake_dragonxd.event
|
|||||||
| `http-interrupted`, `shutdown-begin` | shutdown progress |
|
| `http-interrupted`, `shutdown-begin` | shutdown progress |
|
||||||
| `stats ...` | counters, every 30 s when they change |
|
| `stats ...` | counters, every 30 s when they change |
|
||||||
| `methods ...` | the 10 most-requested JSON-RPC methods with counts (with `stats`, and at exit): what the GUI is polling |
|
| `methods ...` | the 10 most-requested JSON-RPC methods with counts (with `stats`, and at exit): what the GUI is polling |
|
||||||
|
| `rescan-begin m= from=`, `rescan-end m= ms=` | a wallet scan (`rescan` or an import) took and released `cs_main` |
|
||||||
|
| `rescan-interrupted m= at= ms=` | a shutdown request ended the scan early |
|
||||||
|
| `import m= addr= rescan=` | a key was imported (`known=1`: it was already in the wallet) |
|
||||||
|
| `walletpassphrase result=`, `walletpassphrasechange result=`, `walletlock` | `FAKE_ENCRYPTED` wallet calls (`ok` or `-14`) |
|
||||||
| `clean-exit ...` | **graceful exit**, with final counters. If this is missing, the process was killed. |
|
| `clean-exit ...` | **graceful exit**, with final counters. If this is missing, the process was killed. |
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
@@ -124,11 +144,13 @@ inherits the GUI's environment.
|
|||||||
| `FAKE_MERGE_SECS` | 6 | how long a `z_mergetoaddress` operation runs before it succeeds |
|
| `FAKE_MERGE_SECS` | 6 | how long a `z_mergetoaddress` operation runs before it succeeds |
|
||||||
| `FAKE_MERGE_SERIAL` | 0 | `1` = ops run one at a time, like dragonxd's single async worker: a new op is `queued` until the previous one finishes |
|
| `FAKE_MERGE_SERIAL` | 0 | `1` = ops run one at a time, like dragonxd's single async worker: a new op is `queued` until the previous one finishes |
|
||||||
| `FAKE_MERGE_FAIL` | (off) | `syncing:N` (the Nth and later merges fail with the THROW_IF_SYNCING text), `lockedkey:N` (-8 "Could not find spending key…" after the scan), `opfail:N` (the Nth and later ops end `failed`, their notes unlocked) |
|
| `FAKE_MERGE_FAIL` | (off) | `syncing:N` (the Nth and later merges fail with the THROW_IF_SYNCING text), `lockedkey:N` (-8 "Could not find spending key…" after the scan), `opfail:N` (the Nth and later ops end `failed`, their notes unlocked) |
|
||||||
|
| `FAKE_RESCAN_SECS` | 0 | `rescan` and the key imports (`importprivkey`, `z_importkey`, `z_importviewingkey`) hold cs_main this long; `rescan-*` events |
|
||||||
|
| `FAKE_ENCRYPTED` / `FAKE_WALLET_PASSPHRASE` | 0 / `fake passphrase one` | `1` = an encrypted wallet, locked at start, with this passphrase |
|
||||||
| `FAKE_SCAN_SECS` | 0 | the GUI's wallet scans (`z_listunspent`, `listunspent`, `z_gettotalbalance`, `z_listreceivedbyaddress`, `z_mergetoaddress`) hold cs_main this long; `scan-begin`/`scan-end` events |
|
| `FAKE_SCAN_SECS` | 0 | the GUI's wallet scans (`z_listunspent`, `listunspent`, `z_gettotalbalance`, `z_listreceivedbyaddress`, `z_mergetoaddress`) hold cs_main this long; `scan-begin`/`scan-end` events |
|
||||||
| `FAKE_TIP_UNKNOWN_SECS` | 0 | `longestchain` reads 0 this long after `Done Loading` (a node that hasn't seen an UpdateTip yet) |
|
| `FAKE_TIP_UNKNOWN_SECS` | 0 | `longestchain` reads 0 this long after `Done Loading` (a node that hasn't seen an UpdateTip yet) |
|
||||||
| `FAKE_LONGEST_AHEAD` / `FAKE_CATCHUP_SECS` | 0 / 0 | the network tip (`longestchain`, `headers`) is N blocks ahead, for that long after `Done Loading` (0 = for good) |
|
| `FAKE_LONGEST_AHEAD` / `FAKE_CATCHUP_SECS` | 0 / 0 | the network tip (`longestchain`, `headers`) is N blocks ahead, for that long after `Done Loading` (0 = for good) |
|
||||||
| `FAKE_TXCOUNT` | 42 | `getwalletinfo` `txcount` (the GUI's connect gate waits for the tip on wallets of 500+) |
|
| `FAKE_TXCOUNT` | 42 | `getwalletinfo` `txcount` (the GUI's connect gate waits for the tip on wallets of 500+) |
|
||||||
| `FAKE_METHODS_ALL` | 0 | `1` = one `call m=<method> p=<params>` event per request (the GUI's RPC cadence; the `methods` lines keep only the top 10 counts) |
|
| `FAKE_METHODS_ALL` | 0 | `1` = one `call m=<method> p=<params>` event per request (the GUI's RPC cadence; the `methods` lines keep only the top 10 counts). Keys and passphrases show as `<redacted>` |
|
||||||
| `FAKE_HEIGHT`, `FAKE_REBUILD_TOTAL`, `FAKE_NOTES`, `FAKE_VERSION` | 3327145, 62583, 13632, 1030050 | cosmetic numbers |
|
| `FAKE_HEIGHT`, `FAKE_REBUILD_TOTAL`, `FAKE_NOTES`, `FAKE_VERSION` | 3327145, 62583, 13632, 1030050 | cosmetic numbers |
|
||||||
|
|
||||||
## Test cases
|
## Test cases
|
||||||
|
|||||||
@@ -644,6 +644,281 @@ Canned noteWalletReply(const std::string& m, const json& params)
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Rescan, key imports and wallet encryption (dragonxd's rpcdump.cpp / rpcwallet.cpp). walletReply runs
|
||||||
|
// under g_csMain (execOne); getrescaninfo and getwalletinfo read only the atomics.
|
||||||
|
// * rescan, importprivkey, z_importkey and z_importviewingkey: dragonxd's argument checks, then a
|
||||||
|
// ScanForWalletTransactions that holds cs_main for FAKE_RESCAN_SECS (0 = instant). The lock-free
|
||||||
|
// getrescaninfo reports it, it logs "Still rescanning" once a minute and, like v1.3.0, it stops
|
||||||
|
// between blocks once a shutdown is requested. Keys are only prefix-checked, never decoded, stored
|
||||||
|
// or logged; an imported key gets a derived fake address, and a key already imported isn't added again.
|
||||||
|
// * FAKE_ENCRYPTED=1: an encrypted wallet, locked at start, with the passphrase FAKE_WALLET_PASSPHRASE.
|
||||||
|
// walletpassphrase (-14 when wrong), walletpassphrasechange and walletlock work, getwalletinfo carries
|
||||||
|
// unlocked_until and the imports need the wallet unlocked (-13). Unencrypted, those three answer -15.
|
||||||
|
// Like dragonxd, the right passphrase with a non-integer timeout unlocks first and only then fails
|
||||||
|
// (-1), leaving the wallet unlocked with no relock timer of its own.
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
double g_rescanSecs = 0.0; // FAKE_RESCAN_SECS
|
||||||
|
std::atomic<bool> g_rescanning{false};
|
||||||
|
std::atomic<long> g_rescanStart{0}, g_rescanHeight{0};
|
||||||
|
std::vector<std::uint64_t> g_importedKeys; // fingerprints of the imported keys
|
||||||
|
std::vector<std::string> g_importedZ, g_importedT; // their addresses, listed with the wallet's own
|
||||||
|
bool g_walletEncrypted = false; // FAKE_ENCRYPTED=1 (set before any thread starts)
|
||||||
|
std::string g_walletPassphrase; // FAKE_WALLET_PASSPHRASE
|
||||||
|
std::atomic<long long> g_unlockedUntil{0}; // unix seconds; 0 = locked
|
||||||
|
std::atomic<bool> g_unlockedNoTimer{false}; // unlocked by a walletpassphrase whose timeout didn't parse
|
||||||
|
|
||||||
|
const char* const kNotString = "JSON value is not a string as expected";
|
||||||
|
const char* const kNotInteger = "JSON value is not an integer as expected";
|
||||||
|
const char* const kBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
bool isWalletMethod(const std::string& m)
|
||||||
|
{
|
||||||
|
return m == "rescan" || m == "importprivkey" || m == "z_importkey" || m == "z_importviewingkey" ||
|
||||||
|
m == "walletpassphrase" || m == "walletpassphrasechange" || m == "walletlock";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wallet relocks when walletpassphrase's timeout runs out (dragonxd's RPCRunLater("lockwallet")).
|
||||||
|
long long unlockedUntilNow()
|
||||||
|
{
|
||||||
|
const long long until = g_unlockedUntil.load();
|
||||||
|
return (until != 0 && until <= unixMs() / 1000) ? 0 : until;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CWallet::IsLocked() == false. Separate from unlocked_until (nWalletUnlockTime), which an unlock with an
|
||||||
|
// unparsable timeout leaves as it was.
|
||||||
|
bool walletUnlockedNow()
|
||||||
|
{
|
||||||
|
return unlockedUntilNow() != 0 || g_unlockedNoTimer.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t fingerprint(const std::string& s) // FNV-1a
|
||||||
|
{
|
||||||
|
std::uint64_t h = 1469598103934665603ULL;
|
||||||
|
for (unsigned char ch : s) h = (h ^ ch) * 1099511628211ULL;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string derivedAddress(const std::string& prefix, size_t length, std::uint64_t fp, const char* alphabet)
|
||||||
|
{
|
||||||
|
std::string out = prefix;
|
||||||
|
const size_t n = std::strlen(alphabet);
|
||||||
|
for (std::uint64_t i = 0; out.size() < length; ++i) out.push_back(alphabet[splitmix64(fp + i) % n]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanForWalletTransactions from height `from`, under the caller's cs_main.
|
||||||
|
void walletScan(const std::string& m, long from)
|
||||||
|
{
|
||||||
|
const long tip = g_height.load();
|
||||||
|
g_rescanStart = from;
|
||||||
|
g_rescanHeight = from;
|
||||||
|
g_rescanning = true;
|
||||||
|
const auto started = Clock::now();
|
||||||
|
event("rescan-begin", "m=" + m + " from=" + std::to_string(from));
|
||||||
|
auto lastLog = started;
|
||||||
|
bool interrupted = false;
|
||||||
|
for (;;) {
|
||||||
|
const double secs = std::chrono::duration<double>(Clock::now() - started).count();
|
||||||
|
const double frac = g_rescanSecs > 0.0 ? std::min(1.0, secs / g_rescanSecs) : 1.0;
|
||||||
|
g_rescanHeight = from + static_cast<long>(static_cast<double>(tip - from) * frac);
|
||||||
|
if (frac >= 1.0) break;
|
||||||
|
if (shutdownRequested()) {
|
||||||
|
interrupted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Clock::now() - lastLog >= 60s) {
|
||||||
|
lastLog = Clock::now();
|
||||||
|
logf("Still rescanning. At block %ld. Progress=%f", g_rescanHeight.load(),
|
||||||
|
tip > 0 ? static_cast<double>(g_rescanHeight.load()) / static_cast<double>(tip) : 1.0);
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(50ms);
|
||||||
|
}
|
||||||
|
const long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - started).count();
|
||||||
|
if (interrupted) {
|
||||||
|
logf("ScanForWalletTransactions: Rescan interrupted by shutdown request at block %ld", g_rescanHeight.load());
|
||||||
|
event("rescan-interrupted", "m=" + m + " at=" + std::to_string(g_rescanHeight.load()) + " ms=" + std::to_string(ms));
|
||||||
|
} else {
|
||||||
|
event("rescan-end", "m=" + m + " ms=" + std::to_string(ms));
|
||||||
|
}
|
||||||
|
g_rescanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The answer for isWalletMethod(m) (the checks in dragonxd's order), or c.known = false.
|
||||||
|
Canned walletReply(const std::string& m, const json& params)
|
||||||
|
{
|
||||||
|
Canned c;
|
||||||
|
if (!isWalletMethod(m)) {
|
||||||
|
c.known = false;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
auto fail = [&c](int code, const std::string& msg) {
|
||||||
|
c.errCode = code;
|
||||||
|
c.errMsg = msg;
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
auto str = [&](size_t i) { return params[i].get<std::string>(); };
|
||||||
|
const long tip = g_height.load();
|
||||||
|
const bool locked = g_walletEncrypted && !walletUnlockedNow();
|
||||||
|
const char* const kUnlockNeeded = "Error: Please enter the wallet passphrase with walletpassphrase first.";
|
||||||
|
c.result = nullptr;
|
||||||
|
|
||||||
|
if (m == "walletpassphrase" || m == "walletpassphrasechange" || m == "walletlock") {
|
||||||
|
const size_t argc = m == "walletlock" ? 0 : 2;
|
||||||
|
const std::string usage = m == "walletlock" ? "walletlock" : m == "walletpassphrase"
|
||||||
|
? "walletpassphrase \"passphrase\" timeout"
|
||||||
|
: "walletpassphrasechange \"oldpassphrase\" \"newpassphrase\"";
|
||||||
|
if (g_walletEncrypted && params.size() != argc) return fail(-1, usage);
|
||||||
|
if (!g_walletEncrypted) return fail(-15, "Error: running with an unencrypted wallet, but " + m + " was called.");
|
||||||
|
if (m == "walletlock") {
|
||||||
|
g_unlockedUntil = 0;
|
||||||
|
g_unlockedNoTimer = false;
|
||||||
|
event("walletlock", "result=ok");
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
if (!params[0].is_string()) return fail(-1, kNotString);
|
||||||
|
if (m == "walletpassphrase") {
|
||||||
|
if (str(0).empty()) return fail(-1, usage);
|
||||||
|
if (str(0) != g_walletPassphrase) {
|
||||||
|
event("walletpassphrase", "result=-14");
|
||||||
|
return fail(-14, "Error: The wallet passphrase entered was incorrect.");
|
||||||
|
}
|
||||||
|
// dragonxd unlocks before it reads the timeout (get_int64), so a bad one leaves the wallet
|
||||||
|
// unlocked with no new relock timer; a timer still pending from an earlier unlock keeps running.
|
||||||
|
if (!params[1].is_number_integer()) {
|
||||||
|
if (unlockedUntilNow() == 0) g_unlockedNoTimer = true;
|
||||||
|
return fail(-1, kNotInteger);
|
||||||
|
}
|
||||||
|
const long long timeout = params[1].get<long long>();
|
||||||
|
g_unlockedUntil = unixMs() / 1000 + timeout;
|
||||||
|
g_unlockedNoTimer = false; // the new timer relocks it
|
||||||
|
event("walletpassphrase", "result=ok timeout=" + std::to_string(timeout));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
if (!params[1].is_string()) return fail(-1, kNotString);
|
||||||
|
if (str(0).empty() || str(1).empty()) return fail(-1, usage);
|
||||||
|
if (str(0) != g_walletPassphrase) {
|
||||||
|
event("walletpassphrasechange", "result=-14");
|
||||||
|
return fail(-14, "Error: The wallet passphrase entered was incorrect.");
|
||||||
|
}
|
||||||
|
g_walletPassphrase = str(1);
|
||||||
|
event("walletpassphrasechange", "result=ok");
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m == "rescan") {
|
||||||
|
if (params.size() > 1) return fail(-1, "rescan \"height\"");
|
||||||
|
long from = 0;
|
||||||
|
if (!params.empty()) {
|
||||||
|
if (!params[0].is_number_integer()) return fail(-1, kNotInteger);
|
||||||
|
from = params[0].get<long>();
|
||||||
|
}
|
||||||
|
if (from < 0 || from > tip) return fail(-8, "Block height out of range");
|
||||||
|
logf("Rescanning from height=%ld", from);
|
||||||
|
walletScan(m, from);
|
||||||
|
logf("Done rescanning from height=%ld", from);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m == "importprivkey") {
|
||||||
|
if (params.empty() || params.size() > 5)
|
||||||
|
return fail(-1, "importprivkey \"dragonxprivkey\" ( \"label\" rescan height secret_key)");
|
||||||
|
if (locked) return fail(-13, kUnlockNeeded);
|
||||||
|
if (!params[0].is_string() || (params.size() > 1 && !params[1].is_string())) return fail(-1, kNotString);
|
||||||
|
bool rescan = true;
|
||||||
|
if (params.size() > 2) {
|
||||||
|
if (!params[2].is_boolean()) return fail(-1, "JSON value is not a boolean as expected");
|
||||||
|
rescan = params[2].get<bool>();
|
||||||
|
}
|
||||||
|
long from = 0;
|
||||||
|
if (rescan && params.size() == 4) {
|
||||||
|
if (!params[3].is_number_integer()) return fail(-1, kNotInteger);
|
||||||
|
from = params[3].get<long>();
|
||||||
|
}
|
||||||
|
if (from < 0 || from > tip) return fail(-4, "Rescan height is out of range.");
|
||||||
|
const std::string& key = params[0].get_ref<const std::string&>();
|
||||||
|
if ((key.size() != 51 && key.size() != 52) || key.find_first_not_of(kBase58) != std::string::npos)
|
||||||
|
return fail(-5, "Invalid private key encoding");
|
||||||
|
const std::uint64_t fp = fingerprint("t:" + key);
|
||||||
|
const std::string addr = derivedAddress("RFakeImport", 34, fp, kBase58);
|
||||||
|
c.result = addr;
|
||||||
|
if (std::find(g_importedKeys.begin(), g_importedKeys.end(), fp) != g_importedKeys.end()) {
|
||||||
|
event("import", "m=" + m + " known=1 addr=" + addr);
|
||||||
|
return c; // "Don't throw error in case a key is already there": no rescan either
|
||||||
|
}
|
||||||
|
g_importedKeys.push_back(fp);
|
||||||
|
g_importedT.push_back(addr);
|
||||||
|
event("import", "m=" + m + " addr=" + addr + " rescan=" + (rescan ? "1" : "0"));
|
||||||
|
if (rescan) walletScan(m, from);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// z_importkey "zkey" ( rescan startHeight ) / z_importviewingkey "vkey" ( rescan startHeight zaddr )
|
||||||
|
const bool viewing = m == "z_importviewingkey";
|
||||||
|
if (params.empty() || params.size() > (viewing ? 4u : 3u))
|
||||||
|
return fail(-1, m + (viewing ? " \"vkey\" ( rescan startHeight zaddr )" : " \"zkey\" ( rescan startHeight )"));
|
||||||
|
if (locked) return fail(-13, kUnlockNeeded);
|
||||||
|
bool rescan = true;
|
||||||
|
bool ignoreExisting = true; // the default "whenkeyisnew"
|
||||||
|
if (params.size() > 1) {
|
||||||
|
if (!params[1].is_string()) return fail(-1, kNotString);
|
||||||
|
const std::string r = str(1);
|
||||||
|
if (r != "whenkeyisnew") {
|
||||||
|
ignoreExisting = false;
|
||||||
|
if (r == "yes" || r == "no") rescan = r == "yes";
|
||||||
|
else if (!viewing && (r == "true" || r == "false")) rescan = r == "true"; // z_importkey's older API
|
||||||
|
else return fail(-8, "rescan must be \"yes\", \"no\" or \"whenkeyisnew\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long from = 0;
|
||||||
|
if (params.size() > 2) {
|
||||||
|
if (!params[2].is_number_integer()) return fail(-1, kNotInteger);
|
||||||
|
from = params[2].get<long>();
|
||||||
|
}
|
||||||
|
if (from < 0 || from > tip) return fail(-8, "Block height out of range");
|
||||||
|
if (!params[0].is_string()) return fail(-1, kNotString);
|
||||||
|
const std::string& key = params[0].get_ref<const std::string&>();
|
||||||
|
std::uint64_t fp = 0;
|
||||||
|
std::string addr;
|
||||||
|
if (viewing) {
|
||||||
|
if (key.rfind("zivks1", 0) != 0) return fail(-5, "Invalid viewing key");
|
||||||
|
if (params.size() < 4) return fail(-5, "Missing zaddr for Sapling viewing key.");
|
||||||
|
if (!params[3].is_string()) return fail(-1, kNotString);
|
||||||
|
addr = str(3);
|
||||||
|
if (addr.rfind("zs1", 0) != 0) return fail(-5, "Invalid zaddr");
|
||||||
|
fp = fingerprint("v:" + addr); // dragonxd keeps one incoming viewing key per address
|
||||||
|
} else {
|
||||||
|
if (key.rfind("secret-extended-key-main1", 0) != 0) return fail(-5, "Invalid spending key");
|
||||||
|
fp = fingerprint("z:" + key);
|
||||||
|
addr = derivedAddress("zs1fakeimport", std::strlen(kFakeZAddr), fp, "0123456789abcdef");
|
||||||
|
}
|
||||||
|
const bool known = std::find(g_importedKeys.begin(), g_importedKeys.end(), fp) != g_importedKeys.end();
|
||||||
|
if (known && ignoreExisting) {
|
||||||
|
event("import", "m=" + m + " known=1 addr=" + addr);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
if (!known) {
|
||||||
|
g_importedKeys.push_back(fp);
|
||||||
|
if (!viewing) g_importedZ.push_back(addr); // watch-only addresses aren't in z_listaddresses
|
||||||
|
}
|
||||||
|
event("import", "m=" + m + (known ? " known=1" : "") + " addr=" + addr + " rescan=" + (rescan ? "1" : "0"));
|
||||||
|
if (rescan) walletScan(m, from);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys and passphrases never reach the events file (FAKE_METHODS_ALL).
|
||||||
|
json redactedParams(const std::string& m, json params)
|
||||||
|
{
|
||||||
|
size_t secrets = 0;
|
||||||
|
if (m == "importprivkey" || m == "z_importkey" || m == "z_importviewingkey" || m == "walletpassphrase" ||
|
||||||
|
m == "encryptwallet")
|
||||||
|
secrets = 1;
|
||||||
|
else if (m == "walletpassphrasechange")
|
||||||
|
secrets = 2;
|
||||||
|
for (size_t i = 0; i < secrets && i < params.size(); ++i) params[i] = "<redacted>";
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
Canned cannedReply(const std::string& m, const json& params)
|
Canned cannedReply(const std::string& m, const json& params)
|
||||||
{
|
{
|
||||||
Canned c;
|
Canned c;
|
||||||
@@ -696,6 +971,7 @@ Canned cannedReply(const std::string& m, const json& params)
|
|||||||
c.result = {{"walletversion", 60000}, {"balance", 0.0}, {"unconfirmed_balance", 0.0},
|
c.result = {{"walletversion", 60000}, {"balance", 0.0}, {"unconfirmed_balance", 0.0},
|
||||||
{"immature_balance", 0.0}, {"txcount", g_cfg.txcount}, {"keypoololdest", now - 86400},
|
{"immature_balance", 0.0}, {"txcount", g_cfg.txcount}, {"keypoololdest", now - 86400},
|
||||||
{"keypoolsize", 101}, {"paytxfee", 0.0}};
|
{"keypoolsize", 101}, {"paytxfee", 0.0}};
|
||||||
|
if (g_walletEncrypted) c.result["unlocked_until"] = unlockedUntilNow();
|
||||||
} else if (m == "z_gettotalbalance") {
|
} else if (m == "z_gettotalbalance") {
|
||||||
c.result = {{"transparent", "0.00"}, {"private", "1234.5678"}, {"total", "1234.5678"}};
|
c.result = {{"transparent", "0.00"}, {"private", "1234.5678"}, {"total", "1234.5678"}};
|
||||||
} else if (m == "z_getbalance") {
|
} else if (m == "z_getbalance") {
|
||||||
@@ -752,7 +1028,19 @@ Canned cannedReply(const std::string& m, const json& params)
|
|||||||
} else if (m == "getaddressesbyaccount" || m == "listaddresses") {
|
} else if (m == "getaddressesbyaccount" || m == "listaddresses") {
|
||||||
c.result = json::array({kFakeTAddr});
|
c.result = json::array({kFakeTAddr});
|
||||||
} else if (m == "getrescaninfo") {
|
} else if (m == "getrescaninfo") {
|
||||||
c.result = {{"rescanning", false}};
|
c.result = {{"rescanning", g_rescanning.load()}};
|
||||||
|
if (g_rescanning.load()) {
|
||||||
|
if (h != 0) {
|
||||||
|
char progress[16];
|
||||||
|
std::snprintf(progress, sizeof(progress), "%.4f",
|
||||||
|
static_cast<double>(g_rescanHeight.load()) / static_cast<double>(h));
|
||||||
|
c.result["rescan_progress"] = progress; // a string, like dragonxd
|
||||||
|
}
|
||||||
|
c.result["rescan_start_height"] = g_rescanStart.load();
|
||||||
|
c.result["rescan_height"] = g_rescanHeight.load();
|
||||||
|
}
|
||||||
|
} else if (isWalletMethod(m)) {
|
||||||
|
c.result = nullptr; // stateful: walletReply answers it under cs_main (execOne)
|
||||||
} else if (m == "validateaddress") {
|
} else if (m == "validateaddress") {
|
||||||
c.result = {{"isvalid", true}, {"address", param0()}, {"ismine", false}};
|
c.result = {{"isvalid", true}, {"address", param0()}, {"ismine", false}};
|
||||||
} else if (m == "z_validateaddress") {
|
} else if (m == "z_validateaddress") {
|
||||||
@@ -815,7 +1103,7 @@ json execOne(const json& req, int& status)
|
|||||||
// FAKE_METHODS_ALL=1: one "call" event per request (method + params), to see the GUI's RPC cadence.
|
// FAKE_METHODS_ALL=1: one "call" event per request (method + params), to see the GUI's RPC cadence.
|
||||||
static const bool logAll = [] { const char* v = std::getenv("FAKE_METHODS_ALL"); return v && std::string(v) == "1"; }();
|
static const bool logAll = [] { const char* v = std::getenv("FAKE_METHODS_ALL"); return v && std::string(v) == "1"; }();
|
||||||
if (logAll) {
|
if (logAll) {
|
||||||
std::string p = params.dump();
|
std::string p = redactedParams(m, params).dump();
|
||||||
if (p.size() > 160) p = p.substr(0, 160) + "...";
|
if (p.size() > 160) p = p.substr(0, 160) + "...";
|
||||||
event("call", "m=" + m + " p=" + p);
|
event("call", "m=" + m + " p=" + p);
|
||||||
}
|
}
|
||||||
@@ -854,6 +1142,12 @@ json execOne(const json& req, int& status)
|
|||||||
Canned w = noteWalletReply(m, params);
|
Canned w = noteWalletReply(m, params);
|
||||||
if (w.known) c = w;
|
if (w.known) c = w;
|
||||||
}
|
}
|
||||||
|
Canned w = walletReply(m, params); // rescan, key imports, FAKE_ENCRYPTED
|
||||||
|
if (w.known) c = w;
|
||||||
|
// An imported key's address is listed with the wallet's own from then on.
|
||||||
|
const auto& imported = m == "z_listaddresses" ? g_importedZ : g_importedT;
|
||||||
|
if ((m == "z_listaddresses" || m == "getaddressesbyaccount" || m == "listaddresses") && c.result.is_array())
|
||||||
|
for (const auto& a : imported) c.result.push_back(a);
|
||||||
}
|
}
|
||||||
if (c.errCode != 0) return fail(c.errCode, c.errMsg);
|
if (c.errCode != 0) return fail(c.errCode, c.errMsg);
|
||||||
return rpcReply(c.result, nullptr, id);
|
return rpcReply(c.result, nullptr, id);
|
||||||
@@ -1422,6 +1716,10 @@ void loadConfig(int argc, char** argv)
|
|||||||
g_cfg.longestAhead = std::max(0L, envLong("FAKE_LONGEST_AHEAD", g_cfg.longestAhead));
|
g_cfg.longestAhead = std::max(0L, envLong("FAKE_LONGEST_AHEAD", g_cfg.longestAhead));
|
||||||
g_cfg.catchupSecs = envDouble("FAKE_CATCHUP_SECS", g_cfg.catchupSecs);
|
g_cfg.catchupSecs = envDouble("FAKE_CATCHUP_SECS", g_cfg.catchupSecs);
|
||||||
g_scanSecs = envDouble("FAKE_SCAN_SECS", 0.0);
|
g_scanSecs = envDouble("FAKE_SCAN_SECS", 0.0);
|
||||||
|
g_rescanSecs = std::max(0.0, envDouble("FAKE_RESCAN_SECS", 0.0));
|
||||||
|
g_walletEncrypted = envLong("FAKE_ENCRYPTED", 0) == 1;
|
||||||
|
const char* pass = std::getenv("FAKE_WALLET_PASSPHRASE");
|
||||||
|
g_walletPassphrase = (pass && *pass) ? pass : "fake passphrase one";
|
||||||
g_cfg.txcount = envLong("FAKE_TXCOUNT", g_cfg.txcount);
|
g_cfg.txcount = envLong("FAKE_TXCOUNT", g_cfg.txcount);
|
||||||
g_cfg.height = envLong("FAKE_HEIGHT", g_cfg.height);
|
g_cfg.height = envLong("FAKE_HEIGHT", g_cfg.height);
|
||||||
g_cfg.rebuildTotal = std::max(1L, envLong("FAKE_REBUILD_TOTAL", g_cfg.rebuildTotal));
|
g_cfg.rebuildTotal = std::max(1L, envLong("FAKE_REBUILD_TOTAL", g_cfg.rebuildTotal));
|
||||||
|
|||||||
Reference in New Issue
Block a user