diff --git a/app/src/api/cln.ts b/app/src/api/cln.ts index 526d55a1..118d752c 100644 --- a/app/src/api/cln.ts +++ b/app/src/api/cln.ts @@ -86,6 +86,11 @@ export async function create_channel( return await clnCmd("AddChannel", tag, { pubkey, amount, satsperbyte }); } -export async function add_peer(tag: string, pubkey: string, host: string) { - return await clnCmd("AddPeer", tag, { pubkey, host }); +export async function add_peer( + tag: string, + pubkey: string, + host: string, + alias?: string +) { + return await clnCmd("AddPeer", tag, { pubkey, host, alias }); } diff --git a/app/src/api/cmd.ts b/app/src/api/cmd.ts index 321f164c..755e6cdf 100644 --- a/app/src/api/cmd.ts +++ b/app/src/api/cmd.ts @@ -98,7 +98,10 @@ export type Cmd = | "ChangeChildSwarmPassword" | "GetLightningBotsDetails" | "ChangeLightningBotLabel" - | "CreateInvoiceForLightningBot"; + | "CreateInvoiceForLightningBot" + | "GetLightningPeers" + | "AddLightningPeer" + | "UpdateLightningPeer"; interface CmdData { cmd: Cmd; diff --git a/app/src/api/lnd.ts b/app/src/api/lnd.ts index 224cc824..5037eb2c 100644 --- a/app/src/api/lnd.ts +++ b/app/src/api/lnd.ts @@ -59,8 +59,13 @@ export async function list_channels(tag: string) { return await lndCmd("ListChannels", tag); } -export async function add_peer(tag: string, pubkey: string, host: string) { - return await lndCmd("AddPeer", tag, { pubkey, host }); +export async function add_peer( + tag: string, + pubkey: string, + host: string, + alias?: string +) { + return await lndCmd("AddPeer", tag, { pubkey, host, alias }); } export async function list_peers(tag: string) { diff --git a/app/src/api/swarm.ts b/app/src/api/swarm.ts index 27a273b3..2afff8d8 100644 --- a/app/src/api/swarm.ts +++ b/app/src/api/swarm.ts @@ -393,6 +393,36 @@ export async function change_lightning_bot_label({ return await swarmCmd("ChangeLightningBotLabel", { id, new_label }); } -export async function create_invoice_for_lightning_bot({id, amt_msat}: {id: string, amt_msat: number}) { - return await swarmCmd("CreateInvoiceForLightningBot", {id, amt_msat}) +export async function create_invoice_for_lightning_bot({ + id, + amt_msat, +}: { + id: string; + amt_msat: number; +}) { + return await swarmCmd("CreateInvoiceForLightningBot", { id, amt_msat }); +} + +export async function get_lightning_peers() { + return await swarmCmd("GetLightningPeers"); +} + +export async function add_lightning_peer({ + pubkey, + alias, +}: { + pubkey: string; + alias: string; +}) { + return await swarmCmd("AddLightningPeer", { pubkey, alias }); +} + +export async function update_lightning_peer({ + pubkey, + alias, +}: { + pubkey: string; + alias: string; +}) { + return await swarmCmd("UpdateLightningPeer", { pubkey, alias }); } diff --git a/app/src/helpers/cln.ts b/app/src/helpers/cln.ts index da55e6ee..b149c050 100644 --- a/app/src/helpers/cln.ts +++ b/app/src/helpers/cln.ts @@ -6,6 +6,7 @@ import { } from "./"; import long from "long"; import type { LndChannel, LndPeer } from "../api/lnd"; +import type { LightningPeer } from "../nodes"; enum ClnChannelState { CHANNELD_AWAITING_LOCKIN = "CHANNELD_AWAITING_LOCKIN", @@ -238,3 +239,11 @@ export function parseClnInvoices(transactions) { return []; } } + +export function convertLightningPeersToObject(lightningPeers: LightningPeer[]) { + const peersObj = {}; + for (let i = 0; i < lightningPeers.length; i++) { + peersObj[lightningPeers[i].pubkey] = lightningPeers[i].alias; + } + return peersObj; +} diff --git a/app/src/helpers/swarm.ts b/app/src/helpers/swarm.ts index fb8a6db4..1cfbe6b1 100644 --- a/app/src/helpers/swarm.ts +++ b/app/src/helpers/swarm.ts @@ -1,7 +1,11 @@ import type { Writable } from "svelte/store"; -import { get_all_image_actual_version, get_image_tags } from "../api/swarm"; +import { + get_all_image_actual_version, + get_image_tags, + get_lightning_peers, +} from "../api/swarm"; import type { Stack, Node } from "../nodes"; -import { swarmVersion } from "../store"; +import { lightningPeers, swarmVersion } from "../store"; export async function getImageVersion( stack: Writable, @@ -87,3 +91,18 @@ export function splitPubkey(pubkey: string) { } return pubkey; } + +export async function handleGetLightningPeers() { + const res = await get_lightning_peers(); + if (Array.isArray(res)) { + lightningPeers.set(res); + } +} + +export function formatPubkey(pk: string) { + return `${pk.substring(0, 6)}...${pk.substring(pk.length - 6)}`; +} + +export function formatPubkeyAliasDisplay(pubkey: string, alias: string) { + return `${alias} (${formatPubkey(pubkey)})`; +} diff --git a/app/src/lnd/AddChannel.svelte b/app/src/lnd/AddChannel.svelte index 3dbf9d11..c657d96b 100644 --- a/app/src/lnd/AddChannel.svelte +++ b/app/src/lnd/AddChannel.svelte @@ -15,20 +15,24 @@ peers as peersStore, channels, channelCreatedForOnboarding, + lightningPeers, } from "../store"; import { formatSatsNumbers, convertSatsToMilliSats } from "../helpers"; import { + convertLightningPeersToObject, parseClnListFunds, parseClnListPeerChannelsRes, parseClnListPeerRes, } from "../helpers/cln"; import { getLndPendingAndActiveChannels } from "../helpers/lnd"; + import { formatPubkeyAliasDisplay } from "../helpers/swarm"; export let activeKey: string = null; $: pubkey = activeKey ? activeKey : ""; $: amount = 0; $: sats = 0; + $: peersObj = convertLightningPeersToObject($lightningPeers); export let tag = ""; export let type = ""; @@ -47,7 +51,9 @@ $: peerData = peers?.length ? peers.map((p) => ({ id: p.pub_key, - text: p.pub_key, + text: peersObj[p.pub_key] + ? formatPubkeyAliasDisplay(p.pub_key, peersObj[p.pub_key]) + : p.pub_key, })) : []; diff --git a/app/src/lnd/ChannelRows.svelte b/app/src/lnd/ChannelRows.svelte index c1d5ce4b..95c265c0 100644 --- a/app/src/lnd/ChannelRows.svelte +++ b/app/src/lnd/ChannelRows.svelte @@ -4,17 +4,20 @@ import ReceiveLine from "../components/ReceiveLine.svelte"; import DotWrap from "../components/DotWrap.svelte"; import Dot from "../components/Dot.svelte"; - import { channels } from "../store"; + import { channels, lightningPeers } from "../store"; import { formatSatsNumbers } from "../helpers"; import { getTransactionStatus, getBlockTip } from "../helpers/bitcoin"; import Exit from "carbon-icons-svelte/lib/Exit.svelte"; import { onDestroy, onMount } from "svelte"; + import { convertLightningPeersToObject } from "../helpers/cln"; export let tag = ""; export let onclose = (id: string, dest: string) => {}; let channel_arr = $channels[tag]; + $: peersObj = convertLightningPeersToObject($lightningPeers); + function copyText(txt: string) { navigator.clipboard.writeText(txt); } @@ -157,7 +160,9 @@ {/if}
- {chan.remote_pubkey} + {peersObj[chan.remote_pubkey] || chan.remote_pubkey}
{#if selectedChannelParter === chan.remote_pubkey} diff --git a/app/src/lnd/Lnd.svelte b/app/src/lnd/Lnd.svelte index b70c6756..59ae385c 100644 --- a/app/src/lnd/Lnd.svelte +++ b/app/src/lnd/Lnd.svelte @@ -10,9 +10,11 @@ selectedNode, hsmd, hsmdClients, + lightningPeers, } from "../store"; import { onMount } from "svelte"; import { get_clients } from "../api/hsmd"; + import { handleGetLightningPeers } from "../helpers/swarm"; export let tag = ""; export let type = ""; @@ -43,9 +45,16 @@ } onMount(async () => { - if (type === "Cln") { - const clients = await get_clients(tag); - if (clients) hsmdClients.set(clients); + try { + if (type === "Cln") { + const clients = await get_clients(tag); + if (clients) hsmdClients.set(clients); + } + + //get all lightning peers + await handleGetLightningPeers(); + } catch (error) { + console.log(error); } }); diff --git a/app/src/lnd/Peers.svelte b/app/src/lnd/Peers.svelte index a5e95fe5..6860165f 100644 --- a/app/src/lnd/Peers.svelte +++ b/app/src/lnd/Peers.svelte @@ -3,6 +3,7 @@ Button, TextInput, InlineNotification, + Modal, } from "carbon-components-svelte"; import Add from "carbon-icons-svelte/lib/Add.svelte"; import ArrowLeft from "carbon-icons-svelte/lib/ArrowLeft.svelte"; @@ -13,8 +14,18 @@ finishedOnboarding, createdPeerForOnboarding, isOnboarding, + lightningPeers, } from "../store"; - import { parseClnListPeerRes } from "../helpers/cln"; + import { + convertLightningPeersToObject, + parseClnListPeerRes, + } from "../helpers/cln"; + import { add_lightning_peer, update_lightning_peer } from "../api/swarm"; + import { + formatPubkey, + formatPubkeyAliasDisplay, + handleGetLightningPeers, + } from "../helpers/swarm"; $: pubkey = ""; $: host = ""; @@ -29,25 +40,38 @@ let show_notification = false; let message = ""; + let open_add_peer_detail = false; + let isSubmitting = false; + let alias = ""; + let peerPubkey = ""; + let error_notification = false; + let error_message = false; + let isUpdate = false; + let peerAlias = ""; async function addPeer() { + message = ""; if (type === "Cln") { - const peer = await CLN.add_peer(tag, pubkey, host); + const peer = await CLN.add_peer(tag, pubkey, host, peerAlias); show_notification = true; if (typeof peer === "string") { message = peer; + error_message = true; return; } if (typeof peer !== "object") { message = "unexpected error"; + error_message = true; console.log(peer); return; } if (peer) { pubkey = ""; host = ""; + peerAlias = ""; + await handleGetLightningPeers(); const peersData = await CLN.list_peers(tag); const thepeers = await parseClnListPeerRes(peersData); peersStore.update((peer) => { @@ -56,11 +80,12 @@ createdPeerForOnboarding.update(() => true); } } else { - if (await add_peer(tag, pubkey, host)) { + if (await add_peer(tag, pubkey, host, peerAlias)) { show_notification = true; pubkey = ""; host = ""; - + peerAlias = ""; + await handleGetLightningPeers(); setTimeout(async () => { const peersData = await list_peers(tag); peersStore.update((ps) => { @@ -84,14 +109,63 @@ } } + function handleOpenAddPeer() { + open_add_peer_detail = true; + alias = ""; + peerPubkey = ""; + } + + function handleOnCloseAddPeer() { + open_add_peer_detail = false; + alias = ""; + peerPubkey = ""; + isUpdate = false; + } + + async function handleAddPeer() { + message = ""; + isSubmitting = true; + try { + let res; + if (isUpdate) { + res = await update_lightning_peer({ pubkey: peerPubkey, alias }); + } else { + res = await add_lightning_peer({ pubkey: peerPubkey, alias }); + } + message = res.message; + if (res.success) { + show_notification = true; + await handleGetLightningPeers(); + handleOnCloseAddPeer(); + return; + } + error_notification = true; + } catch (error) { + error_notification = true; + } finally { + isSubmitting = false; + } + } + + function openEditAlias(pubkey: string) { + isUpdate = true; + peerPubkey = pubkey; + alias = peerObj[pubkey]; + open_add_peer_detail = true; + } + $: peersLength = peers && peers.length ? peers.length : "No"; $: peersLabel = peers && peers.length <= 1 ? "peer" : "peers"; $: addDisabled = !pubkey || !host; + $: peerObj = convertLightningPeersToObject($lightningPeers);
-
{}}> - +
+
{}}> + +
+
{#if peers && peers.length} @@ -99,8 +173,18 @@
{#each peers as peer}
-
{peer.pub_key}
+
+ {`${peerObj[peer.pub_key] ? formatPubkeyAliasDisplay(peer.pub_key, peerObj[peer.pub_key]) : peer.pub_key}`} +
{peer.address}
+ {#if peerObj[peer.pub_key]} + + {/if} @@ -113,8 +197,8 @@ {#if show_notification} { @@ -137,6 +221,12 @@ bind:value={host} />
+ +
+ (open_add_peer_detail = false)} + on:open + on:close={handleOnCloseAddPeer} + on:submit={handleAddPeer} + > + {#if error_notification} + { + e.preventDefault(); + error_notification = false; + }} + /> + {/if} +
+ +
+
+ +
+
diff --git a/app/src/nodes.ts b/app/src/nodes.ts index af3422ac..94f18eb0 100644 --- a/app/src/nodes.ts +++ b/app/src/nodes.ts @@ -7,6 +7,11 @@ export interface Stack { custom_2b_domain?: string; } +export interface LightningPeer { + alias: string; + pubkey: string; +} + export interface Node { name: string; type: NodeType; diff --git a/app/src/store.ts b/app/src/store.ts index 5845aa7b..10bfc579 100644 --- a/app/src/store.ts +++ b/app/src/store.ts @@ -1,5 +1,5 @@ import { writable, derived, type Readable } from "svelte/store"; -import type { Node, Stack } from "./nodes"; +import type { LightningPeer, Node, Stack } from "./nodes"; import { initialUsers } from "./relay/users"; import type { User } from "./relay/users"; import type { Tribe, Person } from "./api/tribes"; @@ -38,6 +38,8 @@ export const people = writable([]); export const channels = writable<{ [tag: string]: LndChannel[] }>({}); +export const lightningPeers = writable([]); + export const proxy = writable({ total: 0, user_count: 0, diff --git a/src/cmd.rs b/src/cmd.rs index cc19bd85..c23e4f9f 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -2,7 +2,7 @@ use anyhow::Error; use reqwest::Response; use std::collections::HashMap; -use crate::{images::Image, utils::make_reqwest_client}; +use crate::{config::LightningPeer, images::Image, utils::make_reqwest_client}; use anyhow::Context; use serde::{Deserialize, Serialize}; use sphinx_auther::secp256k1::PublicKey; @@ -161,6 +161,9 @@ pub enum SwarmCmd { GetSignedInUserDetails, GetAllImageActualVersion, ChangeUserPasswordBySuperAdmin(ChangeUserPasswordBySuperAdminInfo), + GetLightningPeers, + AddLightningPeer(LightningPeer), + UpdateLightningPeer(LightningPeer), } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -194,6 +197,7 @@ pub struct TestMine { pub struct AddPeer { pub pubkey: String, pub host: String, + pub alias: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/src/config.rs b/src/config.rs index 8f0458ef..d685718a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,7 @@ pub struct Stack { #[serde(skip_serializing_if = "Option::is_none")] pub global_mem_limit: Option, pub backup_services: Option>, + pub lightning_peers: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] @@ -88,6 +89,12 @@ pub struct User { pub role: Role, } +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct LightningPeer { + pub alias: String, + pub pubkey: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct SendSwarmDetailsBody { pub username: String, @@ -308,6 +315,7 @@ impl Stack { custom_2b_domain: self.custom_2b_domain.clone(), global_mem_limit: self.global_mem_limit, backup_services: self.backup_services.clone(), + lightning_peers: self.lightning_peers.clone(), } } } diff --git a/src/conn/swarm/mod.rs b/src/conn/swarm/mod.rs index 67cea928..e24133de 100644 --- a/src/conn/swarm/mod.rs +++ b/src/conn/swarm/mod.rs @@ -1,10 +1,11 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::time::Duration; use crate::{ cmd::{ChangeUserPasswordBySuperAdminInfo, GetDockerImageTagsDetails}, - config::State, + config::{LightningPeer, State}, }; #[derive(Serialize, Deserialize, Debug, Clone)] @@ -18,6 +19,13 @@ pub struct ChangePasswordBySuperAdminResponse { pub message: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SwarmResponse { + pub success: bool, + pub message: String, + pub data: Option, +} + pub async fn update_swarm() -> Result { let password = std::env::var("SWARM_UPDATER_PASSWORD").unwrap_or(String::new()); @@ -148,3 +156,88 @@ pub async fn change_swarm_user_password_by_user_admin( } } } + +pub fn add_new_lightning_peer( + state: &mut State, + info: LightningPeer, + must_save_stack: &mut bool, +) -> SwarmResponse { + if info.pubkey.is_empty() || info.alias.is_empty() { + return SwarmResponse { + success: false, + message: "pubkey and alias cannot be empty".to_string(), + data: None, + }; + } + + let lightning_peers_clone = state.stack.lightning_peers.clone(); + + if let Some(mut lightning_peers) = lightning_peers_clone { + let peer_exist = lightning_peers + .iter() + .position(|peer| peer.pubkey == info.pubkey); + if peer_exist.is_some() { + return SwarmResponse { + success: false, + message: "public key already exist, please update".to_string(), + data: None, + }; + } + lightning_peers.push(info); + state.stack.lightning_peers = Some(lightning_peers); + } else { + state.stack.lightning_peers = Some(vec![info]); + } + + *must_save_stack = true; + SwarmResponse { + success: true, + message: "peer added successfully".to_string(), + data: None, + } +} + +pub fn update_lightning_peer( + state: &mut State, + info: LightningPeer, + must_save_stack: &mut bool, +) -> SwarmResponse { + if info.alias.is_empty() { + return SwarmResponse { + success: false, + message: "alias cannot be empty".to_string(), + data: None, + }; + } + + if state.stack.lightning_peers.is_none() { + return SwarmResponse { + success: false, + message: "pubkey does not exist".to_string(), + data: None, + }; + }; + + if let Some(mut clone_lightning_peers) = state.stack.lightning_peers.clone() { + let pos = clone_lightning_peers + .iter() + .position(|peer| peer.pubkey == info.pubkey); + + if pos.is_none() { + return SwarmResponse { + success: false, + message: "invalid pubkey".to_string(), + data: None, + }; + } + + clone_lightning_peers[pos.unwrap()] = info; + state.stack.lightning_peers = Some(clone_lightning_peers); + } + *must_save_stack = true; + SwarmResponse { + success: true, + message: "alias updated successfully".to_string(), + data: None, + } +} diff --git a/src/defaults.rs b/src/defaults.rs index 3bb6f683..6aca868c 100644 --- a/src/defaults.rs +++ b/src/defaults.rs @@ -180,6 +180,7 @@ impl Default for Stack { custom_2b_domain: env_no_empty("NAV_BOLTWALL_SHARED_HOST"), global_mem_limit: None, backup_services: None, + lightning_peers: None, } } } @@ -336,6 +337,7 @@ pub fn llama_only(network: &str, host: Option) -> Stack { custom_2b_domain: None, global_mem_limit: None, backup_services: None, + lightning_peers: None, } } diff --git a/src/handler.rs b/src/handler.rs index 1f006e35..0e2a87af 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -5,11 +5,14 @@ use crate::auth; use crate::builder; use crate::cmd::*; use crate::config; +use crate::config::LightningPeer; use crate::config::Role; use crate::config::User; use crate::config::{Clients, Node, Stack, State, STATE}; use crate::conn::boltwall::get_api_token; use crate::conn::boltwall::update_user; +use crate::conn::swarm::add_new_lightning_peer; +use crate::conn::swarm::update_lightning_peer; use crate::conn::swarm::{change_swarm_user_password_by_user_admin, get_image_tags}; use crate::dock::*; use crate::images::DockerHubImage; @@ -449,6 +452,21 @@ pub async fn handle( .await; Some(serde_json::to_string(&res)?) } + SwarmCmd::GetLightningPeers => { + log::info!("Get all lightning peers"); + let res = &state.stack.lightning_peers; + Some(serde_json::to_string(&res)?) + } + SwarmCmd::AddLightningPeer(info) => { + log::info!("Add new lightning peer"); + let res = add_new_lightning_peer(&mut state, info, &mut must_save_stack); + Some(serde_json::to_string(&res)?) + } + SwarmCmd::UpdateLightningPeer(info) => { + log::info!("Update Lightning peer"); + let res = update_lightning_peer(&mut state, info, &mut must_save_stack); + Some(serde_json::to_string(&res)?) + } }, Cmd::Relay(c) => { let client = state.clients.relay.get(tag).context("no relay client")?; @@ -506,8 +524,22 @@ pub async fn handle( Some(serde_json::to_string(&channel_list.channels)?) } LndCmd::AddPeer(peer) => { - let result = client.add_peer(peer).await?; - Some(serde_json::to_string(&result)?) + if let Some(alias) = peer.alias.clone() { + add_new_lightning_peer( + &mut state, + LightningPeer { + pubkey: peer.pubkey.clone(), + alias, + }, + &mut must_save_stack, + ); + let client = state.clients.lnd.get_mut(tag).context("no lnd client")?; + let result = client.add_peer(peer).await?; + Some(serde_json::to_string(&result)?) + } else { + let result = client.add_peer(peer).await?; + Some(serde_json::to_string(&result)?) + } } LndCmd::ListPeers => { let result = client.list_peers().await?; @@ -587,8 +619,22 @@ pub async fn handle( } else { peer.host }; - let result = client.connect_peer(&peer.pubkey, &host, port).await?; - Some(serde_json::to_string(&result)?) + if let Some(alias) = peer.alias.clone() { + add_new_lightning_peer( + &mut state, + LightningPeer { + alias, + pubkey: peer.pubkey.clone(), + }, + &mut must_save_stack, + ); + let client = state.clients.cln.get_mut(tag).context("no cln client")?; + let result = client.connect_peer(&peer.pubkey, &host, port).await?; + Some(serde_json::to_string(&result)?) + } else { + let result = client.connect_peer(&peer.pubkey, &host, port).await?; + Some(serde_json::to_string(&result)?) + } } ClnCmd::AddChannel(channel) => { let channel = client diff --git a/src/secondbrain.rs b/src/secondbrain.rs index c7eafc0b..5582e8f2 100644 --- a/src/secondbrain.rs +++ b/src/secondbrain.rs @@ -29,6 +29,7 @@ pub fn only_second_brain(network: &str, host: Option, lightning_provider custom_2b_domain: env_no_empty("NAV_BOLTWALL_SHARED_HOST"), global_mem_limit: None, backup_services: Some(vec!["boltwall".to_string(), "neo4j".to_string()]), + lightning_peers: None, } } @@ -128,5 +129,6 @@ pub fn default_local_stack(host: Option, network: &str, nodes: Vec custom_2b_domain: None, global_mem_limit: None, backup_services: None, + lightning_peers: None, } } diff --git a/src/sphinxv2.rs b/src/sphinxv2.rs index 27c8963b..3505f86d 100644 --- a/src/sphinxv2.rs +++ b/src/sphinxv2.rs @@ -77,6 +77,7 @@ pub fn sphinxv2_only(network: &str, host: Option) -> Stack { custom_2b_domain: None, global_mem_limit: None, backup_services: None, + lightning_peers: None, } } @@ -113,6 +114,7 @@ pub fn sphinxv1_only(network: &str, host: Option) -> Stack { custom_2b_domain: None, global_mem_limit: None, backup_services: Some(vec!["mixer".to_string(), "tribes".to_string()]), + lightning_peers: None, } } @@ -131,5 +133,6 @@ pub fn config_only(host: Option) -> Stack { custom_2b_domain: None, global_mem_limit: None, backup_services: None, + lightning_peers: None, } }