diff --git a/core/archipelago/src/api/rpc/lnd/channels.rs b/core/archipelago/src/api/rpc/lnd/channels.rs
index 80648fd9..c5e58ee4 100644
--- a/core/archipelago/src/api/rpc/lnd/channels.rs
+++ b/core/archipelago/src/api/rpc/lnd/channels.rs
@@ -198,11 +198,49 @@ impl RpcHandler {
));
}
- info!(peer = pubkey, amount = amount, "Opening Lightning channel");
+ let private = params
+ .get("private")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+
+ // Fee control: either a confirmation target or an explicit fee rate
+ let target_conf = params.get("target_conf").and_then(|v| v.as_i64());
+ let sat_per_vbyte = params.get("sat_per_vbyte").and_then(|v| v.as_i64());
+ if target_conf.is_some() && sat_per_vbyte.is_some() {
+ return Err(anyhow::anyhow!(
+ "Invalid fee parameters: specify either target_conf or sat_per_vbyte, not both"
+ ));
+ }
+ if let Some(tc) = target_conf {
+ if !(1..=1008).contains(&tc) {
+ return Err(anyhow::anyhow!(
+ "Invalid target_conf: must be between 1 and 1008 blocks"
+ ));
+ }
+ }
+ if let Some(rate) = sat_per_vbyte {
+ if !(1..=5000).contains(&rate) {
+ return Err(anyhow::anyhow!(
+ "Invalid sat_per_vbyte: must be between 1 and 5000"
+ ));
+ }
+ }
+
+ 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,20 +248,43 @@ 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!({
+ let mut open_body = serde_json::json!({
"node_pubkey_string": pubkey,
"local_funding_amount": amount.to_string(),
+ "private": private,
});
+ if let Some(tc) = target_conf {
+ open_body["target_conf"] = serde_json::json!(tc);
+ }
+ if let Some(rate) = sat_per_vbyte {
+ // LND REST encodes uint64 as a JSON string
+ open_body["sat_per_vbyte"] = serde_json::json!(rate.to_string());
+ }
let resp = client
.post(format!("{LND_REST_BASE_URL}/v1/channels"))
diff --git a/core/archipelago/src/api/rpc/middleware.rs b/core/archipelago/src/api/rpc/middleware.rs
index da078543..5233d8ff 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/public/assets/img/bg-web5.jpg b/neode-ui/public/assets/img/bg-web5.jpg
index 537c3a3d..190e46b4 100644
Binary files a/neode-ui/public/assets/img/bg-web5.jpg and b/neode-ui/public/assets/img/bg-web5.jpg differ
diff --git a/neode-ui/src/components/LightningChannelsPanel.vue b/neode-ui/src/components/LightningChannelsPanel.vue
new file mode 100644
index 00000000..7adb8057
--- /dev/null
+++ b/neode-ui/src/components/LightningChannelsPanel.vue
@@ -0,0 +1,433 @@
+
+ Total Outbound {{ formatSats(summary.total_outbound) }} Total Inbound {{ formatSats(summary.total_inbound) }} Channels {{ channels.length }} Loading channels... {{ error }} No channels yet Open a channel to start sending and receiving Lightning payments.
+ {{ ch.remote_pubkey }}
+
+ Capacity: {{ formatSats(ch.capacity) }}
+ Format: pubkey@host:port Minimum 20,000 sats
+ {{ feePresets.find(p => p.key === openForm.feePreset)?.hint }}
+ Set one — sats per vByte takes precedence when both are set Some nodes (e.g. LSPs like Olympus) only accept unannounced channels {{ openError }} This will cooperatively close the channel with peer {{ closeTarget.remote_pubkey.slice(0, 16) }}... {{ closeError }}Open Channel
+
+ Close Channel?
+
+ Lightning channels on this node. Open a channel to a peer to send and receive Lightning payments. +
+@@ -145,6 +156,7 @@ import { ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { rpcClient } from '@/api/rpc-client' import BaseModal from '@/components/BaseModal.vue' +import LightningChannelsPanel from '@/components/LightningChannelsPanel.vue' const { t } = useI18n() @@ -152,10 +164,11 @@ const props = defineProps<{ show: boolean }>() const emit = defineEmits<{ close: []; changed: [] }>() const tabs = [ + { key: 'channels' as const, label: 'Channels' }, { key: 'cashu' as const, label: 'Cashu Mints' }, { key: 'fedimint' as const, label: 'Fedimint Federations' }, ] -const activeTab = ref<'cashu' | 'fedimint'>('cashu') +const activeTab = ref<'channels' | 'cashu' | 'fedimint'>('channels') // Backed by wallet.fedimint-list / -join / -leave (fedimint-clientd HTTP bridge). // Join degrades gracefully with a clear error if the Fedimint client app isn't installed. diff --git a/neode-ui/src/views/apps/LightningChannels.vue b/neode-ui/src/views/apps/LightningChannels.vue index 2a55d735..2e427ccb 100644 --- a/neode-ui/src/views/apps/LightningChannels.vue +++ b/neode-ui/src/views/apps/LightningChannels.vue @@ -10,317 +10,13 @@
Total Outbound
-{{ formatSats(summary.total_outbound) }}
-Total Inbound
-{{ formatSats(summary.total_inbound) }}
-Channels
-{{ channels.length }}
-Loading channels...
-{{ error }}
- -No channels yet
-Open a channel to start sending and receiving Lightning payments.
-- {{ ch.remote_pubkey }} -
- - -- Capacity: {{ formatSats(ch.capacity) }} -
-Format: pubkey@host:port
-Minimum 20,000 sats
-{{ openError }}
-This will cooperatively close the channel with peer {{ closeTarget.remote_pubkey.slice(0, 16) }}...
-{{ closeError }}
-