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?