From 35a8885c2adade402508694122f28e717590af72 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 5 Jul 2026 18:55:48 +0700 Subject: [PATCH] fix(platform-wallet): re-broadcast a Broadcast-status lock on resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resuming a tracked asset lock at `Broadcast` status only waited for a proof; it never re-broadcast the funding tx (only the `Built` arm does). If the tx had been evicted from every mempool (Core's default `-mempoolexpiry` is two weeks) or the original broadcast reached no peers (an SPV connectivity gap), no InstantSend/ChainLock proof could ever arrive — and since the user-facing funding flows now wait for the ChainLock indefinitely (#4006), the resume would hang forever with no recovery path. Add a defensive re-broadcast to the `Broadcast` arm of `resume_asset_lock` before the wait. Unlike the `Built` arm (whose tx was never broadcast, so a broadcast error is genuine and propagates), a `Broadcast`-status tx may still be in a mempool or already mined, where the network reports "already known" / "already in block chain". The broadcaster can't distinguish that from a real rejection (DAPI classifies every failure as `MaybeSent`), so the re-broadcast is best-effort: log the error and proceed to `wait_for_proof` regardless rather than failing a resume on a tx that is actually fine (if it was mined, the wait resolves immediately from the persisted record). This path is shared by the identity, platform-address, and shielded resume flows plus the app-launch catch-up sweep; re-broadcasting an already-known tx is idempotent, so the extra call is harmless where the tx is still live. Co-Authored-By: Claude Opus 4.8 --- .../src/wallet/asset_lock/sync/recovery.rs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs b/packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs index 64e709c0799..629d89b9eca 100644 --- a/packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs +++ b/packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs @@ -265,7 +265,36 @@ impl AssetLockManager { .await? } AssetLockStatus::Broadcast => { - // Already broadcast — just wait for proof. + // Defensive re-broadcast, then wait for proof. A lock can + // sit at `Broadcast` across app restarts long enough for + // its funding tx to be evicted from every mempool (Core's + // default `-mempoolexpiry` is two weeks), or the original + // broadcast may have reached no peers at all (SPV + // connectivity gap). Once no node holds the tx, no IS/CL + // proof can ever arrive, and the wait — now unbounded for + // the user-facing funding flows — would hang forever. A + // re-broadcast revives an evicted/undelivered tx. + // + // Best-effort: unlike the `Built` arm, this tx was already + // broadcast once (that's what `Broadcast` means), so it may + // still be in a mempool or already mined — in which case the + // network reports "already known" / "already in block + // chain". The broadcaster can't distinguish that from a real + // rejection (DAPI classifies every failure as `MaybeSent`), + // so we log and proceed to `wait_for_proof` regardless + // rather than failing the resume on a tx that is actually + // fine. If the tx really was mined, `wait_for_proof` + // resolves immediately from the SPV/persisted record. + if let Err(e) = self.broadcaster.broadcast(&tx).await { + tracing::debug!( + outpoint = %out_point, + error = %e, + "resume_asset_lock: defensive re-broadcast of a \ + Broadcast-status lock returned an error (likely \ + already in a mempool or mined); proceeding to wait \ + for proof" + ); + } let proof = self.wait_for_proof(out_point, timeout).await?; self.validate_or_upgrade_proof(proof, account_index, out_point) .await?