Postpone the Zano refresh worker and drop the tx-wide payment id - #18
Conversation
j0ntz
left a comment
There was a problem hiding this comment.
Reviewed alongside EdgeApp/edge-currency-accountbased#1092. Nothing I found blocks the merge; all three notes below are suggestions. I checked the rfc4648 swap against the old hand-rolled encoder specifically: base16.stringify uppercases and the .toLowerCase() restores the previous output, so existing wallet files still open.
Separately, all four commit subjects are over the 50-character Edge limit that Paul enforces in review (54 to 77 characters).
|
The design doc for this work is now in #19, rewritten to describe what ships here rather than the first round. Section 8's phase 8 records where this pair diverges from #17 and #1090: the checkpoint replacing the synced-only store, the mempool sweep moving after the paging loop, the HF6 payment-id handling, and the It lived only on |
Opening a wallet auto-starts its native refresh worker, and the worker holds the per-wallet recursive mutex for the entire first catch-up scan (plain_wallet_api.cpp:508 auto-runs on open; worker_func holds the locked_object proxy across refresh()). The 0.4.0 re-key migration's resetWalletPassword takes that same mutex, so on a wallet weeks behind it blocked for the whole scan while sitting on React Native's shared native-module dispatch queue -- every native call in the app queued behind it, and the app usually died before the migration finished, so the file was never re-keyed and every launch repeated the block. startWallet now configures the native library's postponed_run_wallet mode before its first open, so no probe or migration open starts a worker, and starts the worker explicitly for exactly the wallet it returns. Every throw path leaves either nothing open, or a wallet that reaches its next caller as ALREADY_EXISTS -- and under postponed mode an adopted wallet is not running, so the adopter owns issuing run_wallet for it, which edge-currency-accountbased's adoption path does. The flag is process-wide and sticky; the raw open/restore/generate docs carry the caveat, and any open made after this must be followed by run_wallet once the wallet should sync. generateSeedPhrase had the same shape: its temporary wallet opened with the worker auto-running, so the closeWallet that follows waited on the same lock. It postpones too, and a close that does not report OK now fails the call rather than deleting a file this process still holds open. Both native methods (configure, run_wallet) exist in the shipped 0.4.0 dispatch, so this is JS-only, and runWallet treats the one bare-string native failure answer as a failure report rather than a parse surprise. The fake models the postponed-run contract (runningWallets, an open-wallet table tests can clear to simulate the process dying), and every startWallet success path asserts exactly-the-returned-wallet- running -- verified by mutation: dropping any started() call now fails the suite. Ported from #17. Co-authored-by: Jonathan Tzeng <jnthntzng@gmail.com>
`prepareZanoDirectory` failures were log-and-continue: if the `wallets` directory could not be created or could not be marked excluded from device backups, the module still handed out `documentDirectory` and the SDK went on to write the seed and spend keys into a directory an unencrypted Finder backup would capture -- the exact exposure the 0.4.0 backup exclusion exists to close. A nil documents directory was worse: `[docsDir path]` fed nil into an NSDictionary literal, which throws at module init. The module now withholds `documentDirectory` when `wallets` cannot be prepared, and the `CppBridge` constructor turns that into a hard failure with a clear message, so Zano is disabled for the session rather than silently unprotected. `NativeZanoModule.documentDirectory` becomes optional to match the contract this creates, the bridge keeps a guard-validated copy for its own paths, and a test pins the refusal for both the missing and the empty value. `logs` and `app_config` carry no key material, so their preparation stays best-effort. This trades availability for key protection: a transient exclusion failure now disables Zano until the next launch instead of degrading silently. Deliberate, and worth revisiting only if such failures show up in the field. Ported from #17. Co-authored-by: Jonathan Tzeng <jnthntzng@gmail.com>
Replaces the hand-rolled hex encoder in `deriveWalletFilePassword` with `base16.stringify(...).toLowerCase()` from rfc4648, which is the encoding library the rest of the stack standardizes on. Output is byte-identical for every input -- both produce the lowercase hex of the same 16 digest bytes -- and the unchanged golden-vector test enforces that: any drift here would orphan every wallet file the 0.4.0 migration re-keyed. Strictly cosmetic; no changelog entry. The `utf8Bytes` docstring also grows a paragraph on why the encoder avoids host globals. rfc4648 joins dependencies and the lockfile is regenerated with it, so `npm ci` stays reproducible. Ported from #17. Co-authored-by: Jonathan Tzeng <jnthntzng@gmail.com>
Zano HF6 moved payment ids into the transaction outputs: each integrated destination's embedded id is attached natively, one per output, and the wallet RPC rejects any non-empty request-level `payment_id` outright (WALLET_RPC_ERROR_CODE_WRONG_PAYMENT_ID, "tx-wide payment id you provided is now deprecated"). The shipped 0.4.0 SDK enforces this whether or not the fork has activated. The one real defect was forwarding: a caller-supplied `opts.paymentId` went out on the wire and the node refused the send with the deprecation error. The per-destination validation loop next to it, meanwhile, was inert: it gated on `addressInfo.is_integrated`, a field the native `get_address_info` has never returned -- the real response carries `payment_id` only as a boolean presence flag (plain_wallet_api.cpp:484, "lazy to make struct for it") -- so the loop never resolved an id, never threw its one-id-per-transaction error, and only cost a native round-trip per recipient on every send. There is no longer anything a request-level id can express, so the option is removed rather than kept as decoration: the field is always sent empty, and a caller holding a separate payment id folds it into an integrated destination address before calling -- which is what edge-currency-accountbased now does on the user's behalf, offline via zano-utils-js. The dead loop goes with it, along with the fictional `is_integrated`/string-`payment_id` declarations in `AddressInfo` and the test fake, which now match the real native shape. The fake also models the transfer path (`asyncCall`/`tryPullResult`, `invoke`), which is the gap that let the forwarding go untested: tests pin that nothing non-empty ever reaches the wire, that multi-destination sends pass, and that the send path makes no address lookups. Replaces the `payment_id: paymentId ?? ''` change proposed in #17 -- which was itself a no-op, since the loop feeding `paymentId` never assigned it -- with the removal the HF6 contract actually calls for. References: - Zano HF6 migration guide, "What is intrinsic payment id": https://docs.zano.org/docs/build/exchange-guidelines/HF6-migration-guide/ - wallet_rpc_server.cpp on_transfer (rejection at the pinned SDK)
7a564f7 to
c9947d5
Compare
|
All four subjects reworded to <=50 characters (39-45). |
|
Pushed It is current as of today rather than a copy: section 6 gained the block-height conversion from the confirmation-count fix (pushed to EdgeApp/edge-currency-accountbased#1092 as |
a979b0f to
c9947d5
Compare
|
Retracting my previous comment and the commit it described. The design doc already has its own PR, #19, carrying a newer revision than the copy I pushed here. My phase goes there instead. |
j0ntz
left a comment
There was a problem hiding this comment.
Ensure fixups squash correctly, they're not fixup! tagged
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none — but EdgeApp/edge-currency-accountbased#1092 consumes this and should be
reviewed alongside it. That PR needs a release of this one before it can bump
its
react-native-zanopin.Description
Addresses the performance regression QA found after the HF6 merge, plus the
HF6 payment-id correctness gap.
Related: #17 — this branch carries forward the correct parts of that PR and
takes a different approach where that one was incorrect. Left open for now;
no action taken on it.
Postpone the refresh worker through
startWalletandgenerateSeedPhraseOpening a Zano wallet auto-starts the SDK refresh worker, and that worker holds
the per-wallet
epee::locked_objectrecursive mutex for the entire firstcatch-up scan — minutes for a wallet weeks behind. Because React Native runs
every TurboModule call on one shared dispatch queue, anything that then touches
the wallet blocks the whole app's native calls behind that scan.
This opens with
postponed_run_walletand starts the worker explicitly, oncethe wallet being returned is the one that should sync. That gives callers a
window to do their setup — the file re-key in particular — before the scan owns
the lock, and gives the engine an actual stop path.
Remove the
transferpayment-id option, never send the tx-wide idSince HF6 the network rejects the transaction-wide payment id outright; ids
travel inside integrated addresses and the wallet attaches each destination's
embedded id natively.
transfernow always sendspayment_id: ''and theper-destination loop is gone.
TransferParams.paymentIdis deleted rather thandeprecated, so callers get a compile error instead of a silently dropped id.
Also corrects
AddressInfoto the real native shape —payment_idis apresence boolean, not the id, and there is no
is_integratedfield.Fail closed when the iOS wallet directory cannot be protected
prepareZanoDirectorynow returns a status andconstantsToExportwithholdsdocumentDirectorywhen thewalletsdirectory could not be prepared, ratherthan handing out a path with no protection applied. The JS constructor throws on
a missing directory instead of writing wallets somewhere unintended.
Encode the derived file password with rfc4648
Cosmetic — uses the shared encoder rather than a bespoke one.
Testing
Unit tests cover the JS logic against a fake native module. Beyond that, this
was run on device in a three-stage ladder on an account with four mainnet Zano
wallets created April–May 2025, each carrying a ~760k-block catch-up window.
Each stage used the same wallet-file fixture; stages 2 and 3 are the same GUI
commit differing only in which packages are installed, so the comparison
isolates these changes.
Confirmed the mechanism on device — with the published packages, the refresh
worker sits in
wallet2::refresh→pull_blocksfor 2269 of 2269 threadsamples, and nothing is written to the wallet file across 8.5 minutes and
~72,000 blocks scanned.
With this branch (plus EdgeApp/edge-currency-accountbased#1092), all four
wallets persisted
partial progress and resumed forward across a hard kill instead of rescanning.
App responsiveness was unchanged — Zano-attributable mutex waits stayed at 0–1
frames per sample in both stages.
Not covered by that run: the startup freeze path, which needs an account whose
wallets are not already marked in
securityCheckedWallets, and thelegacy→HF6 wallet-file migration, which needs a fixture holding real scan
history. Both are being followed up separately.