fix(lnd): retry channel opens during LND startup + calm non-red notice
LND's RPC answers before its p2p server finishes loading, so connect/open during that window failed red with the raw "server is still in the process of starting". Now: quiet ~30s retry first; if still starting, a calm amber "still finishing its startup" notice instead of an error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
97464779d4
commit
088b3e255a
@ -5,6 +5,21 @@ use tracing::info;
|
|||||||
|
|
||||||
use super::LND_REST_BASE_URL;
|
use super::LND_REST_BASE_URL;
|
||||||
|
|
||||||
|
/// LND rejects RPCs with "server is still in the process of starting" for a
|
||||||
|
/// short window after wallet unlock (p2p/graph subsystems still loading).
|
||||||
|
/// Transient by design — match it so callers retry and, if it persists,
|
||||||
|
/// surface a calm notice instead of a scary failure. The exact phrase below
|
||||||
|
/// is what the frontend keys its softer (non-red) styling on.
|
||||||
|
fn lnd_still_starting(msg: &str) -> bool {
|
||||||
|
let m = msg.to_ascii_lowercase();
|
||||||
|
m.contains("in the process of starting") || m.contains("server is still starting")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User-facing text for the still-starting state. Deliberately calm: this is
|
||||||
|
/// a "wait a moment", not an error.
|
||||||
|
const LND_STARTING_MSG: &str = "Your Lightning node is still finishing its startup — this \
|
||||||
|
usually takes a minute or two after the node comes online. Please try again shortly.";
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct ChannelInfo {
|
struct ChannelInfo {
|
||||||
chan_id: String,
|
chan_id: String,
|
||||||
@ -251,25 +266,45 @@ impl RpcHandler {
|
|||||||
"perm": false,
|
"perm": false,
|
||||||
"timeout": "30"
|
"timeout": "30"
|
||||||
});
|
});
|
||||||
let connect_resp = client
|
// Right after wallet unlock, LND's RPC answers while its p2p
|
||||||
.post(format!("{LND_REST_BASE_URL}/v1/peers"))
|
// server is still spinning up, and every connect attempt gets
|
||||||
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
// "server is still in the process of starting". That's a
|
||||||
.json(&connect_body)
|
// transient state, not a failure — it clears in seconds — so
|
||||||
.timeout(std::time::Duration::from_secs(35))
|
// retry quietly for ~30s before surfacing a calm, non-scary
|
||||||
.send()
|
// notice (the frontend styles LND_STARTING_MSG as info, not red).
|
||||||
.await
|
let mut attempt = 0u32;
|
||||||
.context("Failed to connect to peer")?;
|
loop {
|
||||||
|
let connect_resp = client
|
||||||
|
.post(format!("{LND_REST_BASE_URL}/v1/peers"))
|
||||||
|
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||||
|
.json(&connect_body)
|
||||||
|
.timeout(std::time::Duration::from_secs(35))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to peer")?;
|
||||||
|
|
||||||
if !connect_resp.status().is_success() {
|
if connect_resp.status().is_success() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let body: serde_json::Value = connect_resp.json().await.unwrap_or_default();
|
let body: serde_json::Value = connect_resp.json().await.unwrap_or_default();
|
||||||
let msg = body
|
let msg = body
|
||||||
.get("message")
|
.get("message")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("Unknown error");
|
.unwrap_or("Unknown error");
|
||||||
// LND returns an error if we already have this peer — that is fine
|
// LND returns an error if we already have this peer — that is fine
|
||||||
if !msg.contains("already connected") {
|
if msg.contains("already connected") {
|
||||||
return Err(anyhow::anyhow!("Failed to connect to peer: {}", msg));
|
break;
|
||||||
}
|
}
|
||||||
|
if lnd_still_starting(msg) && attempt < 5 {
|
||||||
|
attempt += 1;
|
||||||
|
info!(attempt, "LND still starting — retrying peer connect in 6s");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if lnd_still_starting(msg) {
|
||||||
|
return Err(anyhow::anyhow!("{LND_STARTING_MSG}"));
|
||||||
|
}
|
||||||
|
return Err(anyhow::anyhow!("Failed to connect to peer: {}", msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,6 +340,9 @@ impl RpcHandler {
|
|||||||
.get("message")
|
.get("message")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("Unknown error");
|
.unwrap_or("Unknown error");
|
||||||
|
if lnd_still_starting(msg) {
|
||||||
|
return Err(anyhow::anyhow!("{LND_STARTING_MSG}"));
|
||||||
|
}
|
||||||
return Err(anyhow::anyhow!("Failed to open channel: {}", msg));
|
return Err(anyhow::anyhow!("Failed to open channel: {}", msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -245,8 +245,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="openError" class="mt-3 alert-error">
|
<!-- "Still starting" is a wait-a-moment notice, not a failure — show
|
||||||
<p class="text-xs">{{ openError }}</p>
|
it as calm amber info instead of the red error treatment. -->
|
||||||
|
<div
|
||||||
|
v-if="openError"
|
||||||
|
class="mt-3"
|
||||||
|
:class="isStartupNotice(openError)
|
||||||
|
? 'p-3 rounded-lg border border-amber-400/25 bg-amber-500/10 text-amber-200/90'
|
||||||
|
: 'alert-error'"
|
||||||
|
>
|
||||||
|
<p class="text-xs">
|
||||||
|
<span v-if="isStartupNotice(openError)" class="mr-1">⏳</span>{{ openError }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
@ -356,6 +366,14 @@ const openForm = ref(defaultOpenForm())
|
|||||||
const openingChannel = ref(false)
|
const openingChannel = ref(false)
|
||||||
const openError = ref<string | null>(null)
|
const openError = ref<string | null>(null)
|
||||||
|
|
||||||
|
/** LND's transient post-unlock state ("still finishing its startup") is a
|
||||||
|
* wait-a-moment notice, not a failure — the template styles it amber. */
|
||||||
|
function isStartupNotice(msg: string | null): boolean {
|
||||||
|
if (!msg) return false
|
||||||
|
const m = msg.toLowerCase()
|
||||||
|
return m.includes('still finishing its startup') || m.includes('in the process of starting')
|
||||||
|
}
|
||||||
|
|
||||||
const closeTarget = ref<Channel | null>(null)
|
const closeTarget = ref<Channel | null>(null)
|
||||||
const closingChannel = ref(false)
|
const closingChannel = ref(false)
|
||||||
const closeError = ref<string | null>(null)
|
const closeError = ref<string | null>(null)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user