fix(lws): make import_account idempotent for an existing account

Re-registering an XMR/WOW account carrying the fixed wallet birthday triggered a
backwards rescan (`if start_height < current { rescan }`), resetting the scan
cursor to the birthday. Because the wallet re-registers before every balance
read, an old-birthday account was reset on each poll: it read a 0 balance, slowly
re-backfilled, then got reset again and never caught up (users saw 0 XMR/WOW
despite real funds).

Only a NEWLY added account needs the backwards rescan (monero-lws add_account
starts at the chain tip). An account that already exists has already been
imported from its birthday, so re-registration is now a no-op here. Genuine
re-restore to an earlier birthday uses the explicit admin rescan path.
This commit is contained in:
jwinterm
2026-07-10 07:29:57 -04:00
parent 4d1d9006bc
commit 81148945c0
+18 -8
View File
@@ -226,21 +226,31 @@ impl LwsClient {
view_key: &str, view_key: &str,
start_height: u64, start_height: u64,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
// `add_account` errors on a duplicate, so only register a NEW account; // Only a NEWLY added account needs the backwards rescan to its birthday
// an existing one keeps its view key. Either way, lower its scan height // (monero-lws `add_account` starts every account at the chain tip). An
// to the birthday with a backwards rescan when that strictly lowers it. // account that ALREADY exists has already been imported from its
let current = match self.account_scan_height(address).await? { // birthday, so re-registration MUST be an idempotent no-op here.
Some(h) => h, //
// Rescanning an existing account resets its scan cursor to the birthday,
// so a client that re-registers on every balance poll (passing the fixed
// wallet birthday) would perpetually wipe scan progress: the account
// reads a 0 balance, slowly re-backfills, then gets reset again on the
// next poll and never catches up. A genuine re-restore to an EARLIER
// birthday goes through the explicit admin `rescan` path, not this one.
match self.account_scan_height(address).await? {
// Already registered: never reset an existing account from here.
Some(_) => Ok(()),
None => { None => {
self.admin_add_account(address, view_key).await?; self.admin_add_account(address, view_key).await?;
self.account_scan_height(address).await?.unwrap_or(u64::MAX) let current =
} self.account_scan_height(address).await?.unwrap_or(u64::MAX);
};
if start_height < current { if start_height < current {
self.rescan(vec![address.to_string()], start_height).await?; self.rescan(vec![address.to_string()], start_height).await?;
} }
Ok(()) Ok(())
} }
}
}
async fn admin_add_account(&self, address: &str, view_key: &str) -> Result<(), AppError> { async fn admin_add_account(&self, address: &str, view_key: &str) -> Result<(), AppError> {
let url = format!("{}/add_account", self.admin_url); let url = format!("{}/add_account", self.admin_url);