feat(wallet): real-time tx push — 0-conf shows in seconds, not on poll
Backend streams LND /v1/transactions/subscribe (fires on mempool arrival and each confirmation) and nudges the /ws/db revision per event; the Home wallet card subscribes and refetches (debounced 800ms). An incoming broadcast now appears while the 30s poll is still asleep. Reconnects forever with capped backoff when LND is down/locked/absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
088b3e255a
commit
5e7e928650
@@ -56,6 +56,70 @@ pub(crate) async fn read_lnd_admin_macaroon() -> Result<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Real-time wallet push (user req 2026-07-22): the UI must reflect an
|
||||
/// incoming on-chain transaction the moment the node sees it, not on the
|
||||
/// next poll. Streams LND's `/v1/transactions/subscribe` — it fires on
|
||||
/// 0-conf mempool arrival AND again on each confirmation — and nudges the
|
||||
/// shared data-model revision on every event; /ws/db pushes that to every
|
||||
/// connected client and the frontend refetches wallet balance/transactions.
|
||||
/// Reconnects forever with capped backoff: LND restarting, wallet locked, or
|
||||
/// LND not installed yet all just mean "try again shortly".
|
||||
pub(crate) fn spawn_lnd_tx_watcher(state_manager: std::sync::Arc<crate::state::StateManager>) {
|
||||
tokio::spawn(async move {
|
||||
let mut delay = std::time::Duration::from_secs(5);
|
||||
loop {
|
||||
match stream_lnd_transactions(&state_manager).await {
|
||||
// Stream ended cleanly (LND shutdown) — resume fast.
|
||||
Ok(()) => delay = std::time::Duration::from_secs(5),
|
||||
Err(e) => {
|
||||
tracing::debug!("lnd tx watcher: {e:#} — retrying in {delay:?}");
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(delay).await;
|
||||
delay = (delay * 2).min(std::time::Duration::from_secs(120));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn stream_lnd_transactions(sm: &crate::state::StateManager) -> Result<()> {
|
||||
let macaroon_hex = hex::encode(read_lnd_admin_macaroon().await?);
|
||||
// Dedicated client: the shared lnd_client() carries a 15s total timeout,
|
||||
// which would kill this deliberately long-lived stream.
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to create streaming HTTP client")?;
|
||||
let mut resp = client
|
||||
.get(format!("{LND_REST_BASE_URL}/v1/transactions/subscribe"))
|
||||
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||
.send()
|
||||
.await
|
||||
.context("subscribe request failed")?;
|
||||
anyhow::ensure!(
|
||||
resp.status().is_success(),
|
||||
"transactions/subscribe returned {}",
|
||||
resp.status()
|
||||
);
|
||||
tracing::info!("lnd tx watcher: streaming wallet transaction events");
|
||||
while let Some(chunk) = resp.chunk().await? {
|
||||
if chunk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Any streamed event = wallet activity. Revision bump is the push
|
||||
// contract (same pattern as the mesh-peer bridge in server.rs) — the
|
||||
// clients refetch, so we don't need to parse the event body.
|
||||
let (data, _) = sm.get_snapshot().await;
|
||||
sm.update_data(data).await;
|
||||
tracing::debug!(
|
||||
bytes = chunk.len(),
|
||||
"lnd tx watcher: wallet tx event — nudged ws clients"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
/// Helper: create an authenticated LND REST client.
|
||||
/// Returns an HTTP client configured for LND's self-signed TLS and the
|
||||
|
||||
@@ -15,7 +15,7 @@ mod fips;
|
||||
mod handshake;
|
||||
mod identity;
|
||||
mod interfaces;
|
||||
pub(in crate::api) mod lnd;
|
||||
pub(crate) mod lnd;
|
||||
mod marketplace;
|
||||
mod mesh;
|
||||
mod middleware;
|
||||
|
||||
Reference in New Issue
Block a user