fix(lightning): channel open raced async peer connect, errors were swallowed
- Connect to peer synchronously (perm=false, 30s timeout) and check the result before opening the channel; previously the connect was fired with perm=true (queued in background, returns immediately) and its result ignored, so the open failed with 'peer is not online' - Let LND channel/peer errors through the error sanitizer so the UI shows the real cause instead of 'Check server logs' - Add private (unannounced) channel option to backend and UI — some peers (e.g. Olympus/ZEUS LSP) reject public channel opens Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e56c5af3ba
commit
c141a733b4
@ -198,11 +198,26 @@ impl RpcHandler {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(peer = pubkey, amount = amount, "Opening Lightning channel");
|
let private = params
|
||||||
|
.get("private")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
peer = pubkey,
|
||||||
|
amount = amount,
|
||||||
|
private = private,
|
||||||
|
"Opening Lightning channel"
|
||||||
|
);
|
||||||
|
|
||||||
let (client, macaroon_hex) = self.lnd_client().await?;
|
let (client, macaroon_hex) = self.lnd_client().await?;
|
||||||
|
|
||||||
// First connect to the peer if an address is provided
|
// First connect to the peer if an address is provided.
|
||||||
|
// perm=false makes LND connect synchronously, so the peer is online
|
||||||
|
// (or we get a real error) before we attempt the channel open.
|
||||||
|
// perm=true queues the connection in the background and returns
|
||||||
|
// immediately, which makes the subsequent open race and fail with
|
||||||
|
// "peer is not online".
|
||||||
if let Some(addr) = params.get("address").and_then(|v| v.as_str()) {
|
if let Some(addr) = params.get("address").and_then(|v| v.as_str()) {
|
||||||
// Validate peer address format (host:port)
|
// Validate peer address format (host:port)
|
||||||
if addr.len() > 256 || addr.contains('\0') || addr.contains(' ') {
|
if addr.len() > 256 || addr.contains('\0') || addr.contains(' ') {
|
||||||
@ -210,19 +225,35 @@ impl RpcHandler {
|
|||||||
}
|
}
|
||||||
let connect_body = serde_json::json!({
|
let connect_body = serde_json::json!({
|
||||||
"addr": { "pubkey": pubkey, "host": addr },
|
"addr": { "pubkey": pubkey, "host": addr },
|
||||||
"perm": true
|
"perm": false,
|
||||||
|
"timeout": "30"
|
||||||
});
|
});
|
||||||
let _ = client
|
let connect_resp = client
|
||||||
.post(format!("{LND_REST_BASE_URL}/v1/peers"))
|
.post(format!("{LND_REST_BASE_URL}/v1/peers"))
|
||||||
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||||
.json(&connect_body)
|
.json(&connect_body)
|
||||||
|
.timeout(std::time::Duration::from_secs(35))
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await
|
||||||
|
.context("Failed to connect to peer")?;
|
||||||
|
|
||||||
|
if !connect_resp.status().is_success() {
|
||||||
|
let body: serde_json::Value = connect_resp.json().await.unwrap_or_default();
|
||||||
|
let msg = body
|
||||||
|
.get("message")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Unknown error");
|
||||||
|
// LND returns an error if we already have this peer — that is fine
|
||||||
|
if !msg.contains("already connected") {
|
||||||
|
return Err(anyhow::anyhow!("Failed to connect to peer: {}", msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let open_body = serde_json::json!({
|
let open_body = serde_json::json!({
|
||||||
"node_pubkey_string": pubkey,
|
"node_pubkey_string": pubkey,
|
||||||
"local_funding_amount": amount.to_string(),
|
"local_funding_amount": amount.to_string(),
|
||||||
|
"private": private,
|
||||||
});
|
});
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
|
|||||||
@ -61,6 +61,9 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
|
|||||||
"Session",
|
"Session",
|
||||||
"Failed to pull",
|
"Failed to pull",
|
||||||
"Failed to start",
|
"Failed to start",
|
||||||
|
"Failed to open channel",
|
||||||
|
"Failed to close channel",
|
||||||
|
"Failed to connect to peer",
|
||||||
"Container",
|
"Container",
|
||||||
"Image",
|
"Image",
|
||||||
"Bitcoin address",
|
"Bitcoin address",
|
||||||
|
|||||||
@ -155,6 +155,13 @@
|
|||||||
/>
|
/>
|
||||||
<p class="text-white/40 text-xs mt-1">Minimum 20,000 sats</p>
|
<p class="text-white/40 text-xs mt-1">Minimum 20,000 sats</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input v-model="openForm.private" type="checkbox" class="accent-blue-500" />
|
||||||
|
<span class="text-white/60 text-sm">Private channel (unannounced)</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-white/40 text-xs mt-1">Some nodes (e.g. LSPs like Olympus) only accept unannounced channels</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="openError" class="mt-3 alert-error">
|
<div v-if="openError" class="mt-3 alert-error">
|
||||||
@ -221,7 +228,7 @@ const channels = ref<Channel[]>([])
|
|||||||
const summary = ref({ total_inbound: 0, total_outbound: 0 })
|
const summary = ref({ total_inbound: 0, total_outbound: 0 })
|
||||||
|
|
||||||
const showOpenModal = ref(false)
|
const showOpenModal = ref(false)
|
||||||
const openForm = ref({ peerUri: '', amount: 100000 })
|
const openForm = ref({ peerUri: '', amount: 100000, private: false })
|
||||||
const openingChannel = ref(false)
|
const openingChannel = ref(false)
|
||||||
const openError = ref<string | null>(null)
|
const openError = ref<string | null>(null)
|
||||||
|
|
||||||
@ -279,11 +286,12 @@ async function openChannel() {
|
|||||||
try {
|
try {
|
||||||
await rpcClient.call({
|
await rpcClient.call({
|
||||||
method: 'lnd.openchannel',
|
method: 'lnd.openchannel',
|
||||||
params: { pubkey, address, amount: openForm.value.amount },
|
params: { pubkey, address, amount: openForm.value.amount, private: openForm.value.private },
|
||||||
timeout: 30000,
|
// Server may wait up to 35s for a synchronous peer connect before opening
|
||||||
|
timeout: 60000,
|
||||||
})
|
})
|
||||||
showOpenModal.value = false
|
showOpenModal.value = false
|
||||||
openForm.value = { peerUri: '', amount: 100000 }
|
openForm.value = { peerUri: '', amount: 100000, private: false }
|
||||||
await loadChannels()
|
await loadChannels()
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
openError.value = err instanceof Error ? err.message : 'Failed to open channel'
|
openError.value = err instanceof Error ? err.message : 'Failed to open channel'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user