diff --git a/core/archipelago/src/api/rpc/lnd/channels.rs b/core/archipelago/src/api/rpc/lnd/channels.rs index 80648fd9..d1603850 100644 --- a/core/archipelago/src/api/rpc/lnd/channels.rs +++ b/core/archipelago/src/api/rpc/lnd/channels.rs @@ -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 diff --git a/core/archipelago/src/api/rpc/middleware.rs b/core/archipelago/src/api/rpc/middleware.rs index 8cef041e..33939af1 100644 --- a/core/archipelago/src/api/rpc/middleware.rs +++ b/core/archipelago/src/api/rpc/middleware.rs @@ -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", diff --git a/neode-ui/src/views/apps/LightningChannels.vue b/neode-ui/src/views/apps/LightningChannels.vue index 6129a276..307272e1 100644 --- a/neode-ui/src/views/apps/LightningChannels.vue +++ b/neode-ui/src/views/apps/LightningChannels.vue @@ -155,6 +155,13 @@ />
Minimum 20,000 sats
+Some nodes (e.g. LSPs like Olympus) only accept unannounced channels
+