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?;
|
||||
|
||||
// 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()) {
|
||||
// Validate peer address format (host:port)
|
||||
if addr.len() > 256 || addr.contains('\0') || addr.contains(' ') {
|
||||
@ -210,19 +225,35 @@ impl RpcHandler {
|
||||
}
|
||||
let connect_body = serde_json::json!({
|
||||
"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"))
|
||||
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||
.json(&connect_body)
|
||||
.timeout(std::time::Duration::from_secs(35))
|
||||
.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!({
|
||||
"node_pubkey_string": pubkey,
|
||||
"local_funding_amount": amount.to_string(),
|
||||
"private": private,
|
||||
});
|
||||
|
||||
let resp = client
|
||||
|
||||
@ -61,6 +61,9 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
|
||||
"Session",
|
||||
"Failed to pull",
|
||||
"Failed to start",
|
||||
"Failed to open channel",
|
||||
"Failed to close channel",
|
||||
"Failed to connect to peer",
|
||||
"Container",
|
||||
"Image",
|
||||
"Bitcoin address",
|
||||
|
||||
@ -155,6 +155,13 @@
|
||||
/>
|
||||
<p class="text-white/40 text-xs mt-1">Minimum 20,000 sats</p>
|
||||
</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 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 showOpenModal = ref(false)
|
||||
const openForm = ref({ peerUri: '', amount: 100000 })
|
||||
const openForm = ref({ peerUri: '', amount: 100000, private: false })
|
||||
const openingChannel = ref(false)
|
||||
const openError = ref<string | null>(null)
|
||||
|
||||
@ -279,11 +286,12 @@ async function openChannel() {
|
||||
try {
|
||||
await rpcClient.call({
|
||||
method: 'lnd.openchannel',
|
||||
params: { pubkey, address, amount: openForm.value.amount },
|
||||
timeout: 30000,
|
||||
params: { pubkey, address, amount: openForm.value.amount, private: openForm.value.private },
|
||||
// Server may wait up to 35s for a synchronous peer connect before opening
|
||||
timeout: 60000,
|
||||
})
|
||||
showOpenModal.value = false
|
||||
openForm.value = { peerUri: '', amount: 100000 }
|
||||
openForm.value = { peerUri: '', amount: 100000, private: false }
|
||||
await loadChannels()
|
||||
} catch (err: unknown) {
|
||||
openError.value = err instanceof Error ? err.message : 'Failed to open channel'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user