fix(lws): import_account uses add_account + /rescan for the modern LWS

The 1.1-alpha LWS dropped the start_height param from add_account (every account
starts scanning at the chain TIP) and moved restore to a separate /rescan admin
command. The backend's import_account still passed start_height to add_account —
a no-op there, so imported/restored wallets never scanned their history (balance
read as 0).

Rework import_account: register only if the account is new (add_account errors
on a duplicate), then lower its scan height to the wallet birthday with a
backwards /rescan when that strictly lowers it (honoring rescan's backwards-only
invariant). Drop the now-unused start_height from add_account / AddAccountParams.

Verified live against a freshly-built wownero-lws 1.1-alpha: registering with a
start_height drives the rescan and the historical outputs are found.
This commit is contained in:
jwinterm
2026-06-29 20:33:07 -04:00
parent 99e1727204
commit 1e08119c5e
2 changed files with 24 additions and 15 deletions
+24 -13
View File
@@ -209,35 +209,46 @@ impl LwsClient {
/// current height.
#[instrument(skip(self, view_key), fields(net = %self.network))]
pub async fn register_account(&self, address: &str, view_key: &str) -> Result<(), AppError> {
self.admin_add_account(address, view_key, None).await
self.admin_add_account(address, view_key).await
}
/// Register + activate an account scanning from a specific `start_height`
/// (avoids re-scanning from genesis for an older wallet).
#[instrument(skip(self, view_key), fields(net = %self.network))]
/// Register an account and scan it from `start_height` (a wallet birthday).
///
/// The LWS `add_account` starts every account at the chain TIP and ignores
/// any supplied start height, so a restore needs an explicit backwards
/// rescan. Read the post-add scan height and only rescan when strictly
/// lowering it — honoring the `rescan` backwards-only invariant (a rescan to
/// `height >= current` is undefined behavior in monero-lws).
#[instrument(skip(self, view_key), fields(net = %self.network, start_height))]
pub async fn import_account(
&self,
address: &str,
view_key: &str,
start_height: u64,
) -> Result<(), AppError> {
self.admin_add_account(address, view_key, Some(start_height))
.await
// `add_account` errors on a duplicate, so only register a NEW account;
// an existing one keeps its view key. Either way, lower its scan height
// to the birthday with a backwards rescan when that strictly lowers it.
let current = match self.account_scan_height(address).await? {
Some(h) => h,
None => {
self.admin_add_account(address, view_key).await?;
self.account_scan_height(address).await?.unwrap_or(u64::MAX)
}
};
if start_height < current {
self.rescan(vec![address.to_string()], start_height).await?;
}
Ok(())
}
async fn admin_add_account(
&self,
address: &str,
view_key: &str,
start_height: Option<u64>,
) -> Result<(), AppError> {
async fn admin_add_account(&self, address: &str, view_key: &str) -> Result<(), AppError> {
let url = format!("{}/add_account", self.admin_url);
let body = AdminAddAccountRequest {
auth: self.admin_key.expose().to_string(),
params: AdminAddAccountParams {
address: address.to_string(),
key: view_key.to_string(),
start_height,
},
};
self.post_ok(url, "add_account", &body).await
-2
View File
@@ -143,8 +143,6 @@ pub(crate) struct AdminAddAccountParams {
pub address: String,
/// The private view key.
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_height: Option<u64>,
}
#[derive(Serialize)]