Merge feat/mesh-archy-command: lightning channel controls + open-race fix
Channel open fee control, channels tab in wallet settings, mempool tx
link, and the channel-open async peer-connect race fix. Conflict in
LightningChannels.vue resolved to the branch's extracted
LightningChannelsPanel component, with main's Teleport-to-body modal
fixes and glass-button-warning class (8256fde1) ported into the panel
so they aren't lost. lnd tests 16/16, LightningChannels vitest green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user