From 88568283a6174ceac692a4eab77ee69b6c382cb2 Mon Sep 17 00:00:00 2001 From: tee8z Date: Mon, 21 Oct 2024 20:04:32 -0400 Subject: [PATCH] update coreln config and better handling missing files (#90) * fixes outdated coreln config and adds --developer flag for while it runs * rewrite coreln visualization to use the rest server that comes with the coreln node instead of c-lightning-REST * better handles missing folders/files --- Cargo.lock | 2 +- doppler/.env | 2 + doppler/.gitignore | 6 + doppler/Cargo.toml | 2 +- doppler/config/regtest/cln.conf | 11 +- doppler/config/signet/cln.conf | 9 +- doppler/src/cln.rs | 48 ++- doppler/src/conf_handler.rs | 1 + doppler/src/docker.rs | 16 +- doppler/src/lnd_actions/lnd_cli.rs | 1 + doppler/src/main.rs | 2 +- doppler/src/visualizer.rs | 8 +- doppler_ui/.gitignore | 1 + doppler_ui/README.md | 17 +- doppler_ui/package-lock.json | 4 +- doppler_ui/package.json | 2 +- doppler_ui/src/components/Visualizer.svelte | 201 +------------ doppler_ui/src/lib/connections.ts | 35 +-- doppler_ui/src/lib/coreln_requests.ts | 189 +++++++++--- doppler_ui/src/lib/node_mapper.ts | 281 ++++++++++++++++++ .../src/routes/api/connections/+server.ts | 11 +- doppler_ui/ui_config/server.conf.ini | 10 +- doppler_ui/ui_config/server.example.conf.ini | 6 - .../open_channels.doppler | 7 +- 24 files changed, 566 insertions(+), 306 deletions(-) create mode 100644 doppler/.env create mode 100644 doppler/.gitignore create mode 100644 doppler_ui/src/lib/node_mapper.ts delete mode 100644 doppler_ui/ui_config/server.example.conf.ini diff --git a/Cargo.lock b/Cargo.lock index b050d8a..8bb55bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,7 +403,7 @@ dependencies = [ [[package]] name = "doppler" -version = "0.3.1" +version = "0.3.2" dependencies = [ "anyhow", "base64", diff --git a/doppler/.env b/doppler/.env new file mode 100644 index 0000000..e474ed4 --- /dev/null +++ b/doppler/.env @@ -0,0 +1,2 @@ +USERID=1000 +GROUPID=1000 diff --git a/doppler/.gitignore b/doppler/.gitignore new file mode 100644 index 0000000..6a64700 --- /dev/null +++ b/doppler/.gitignore @@ -0,0 +1,6 @@ +/target +/data +/scripts +doppler.db +doppler-cluster.yaml +/ui_config diff --git a/doppler/Cargo.toml b/doppler/Cargo.toml index 0c31067..db01422 100644 --- a/doppler/Cargo.toml +++ b/doppler/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "doppler" -version = "0.3.1" +version = "0.3.2" repository = "https://github.com/tee8z/doppler.git" [package.metadata.dist] diff --git a/doppler/config/regtest/cln.conf b/doppler/config/regtest/cln.conf index e2c89c3..f08fef2 100755 --- a/doppler/config/regtest/cln.conf +++ b/doppler/config/regtest/cln.conf @@ -96,12 +96,9 @@ ## Load your plugins from a directory #plugin-dir=/path/to/your/.lightning/plugins - -## Load plugins individually -plugin=/opt/c-lightning-rest/plugin.js -rest-port=8080 -rest-docport=4001 -rest-protocol=https +clnrest-host=0.0.0.0 +clnrest-port=8080 +clnrest-protocol=https ## Set the network for Core Lightning to sync to, Bitcoin Mainnet for most users ## Not required if the config file is in a network directory network=regtest @@ -116,4 +113,4 @@ rpc-file-mode=0660 dev-fast-gossip experimental-dual-fund -large-channels \ No newline at end of file +large-channels diff --git a/doppler/config/signet/cln.conf b/doppler/config/signet/cln.conf index 4a31058..29ed8e2 100755 --- a/doppler/config/signet/cln.conf +++ b/doppler/config/signet/cln.conf @@ -98,10 +98,9 @@ #plugin-dir=/path/to/your/.lightning/plugins ## Load plugins individually -plugin=/opt/c-lightning-rest/plugin.js -rest-port=8080 -rest-docport=4001 -rest-protocol=https +clnrest-host=0.0.0.0 +clnrest-port=8080 +clnrest-protocol=https ## Set the network for Core Lightning to sync to, Bitcoin Mainnet for most users ## Not required if the config file is in a network directory network=signet @@ -116,4 +115,4 @@ rpc-file-mode=0660 dev-fast-gossip experimental-dual-fund -large-channels \ No newline at end of file +large-channels diff --git a/doppler/src/cln.rs b/doppler/src/cln.rs index e5a14ed..dfe5e98 100644 --- a/doppler/src/cln.rs +++ b/doppler/src/cln.rs @@ -22,12 +22,12 @@ pub struct Cln { pub container_name: String, pub name: String, pub pubkey: Option, + pub rune: Option, pub alias: String, pub grpc_port: String, pub p2p_port: String, pub rpc_server: String, pub rest_port: String, - pub macaroon_path: String, pub server_url: String, pub path_vol: String, pub bitcoind_node_container_name: String, @@ -50,6 +50,12 @@ impl Cln { ) -> Result { get_peers_short_channel_id(self, options, node_command, "source") } + + pub fn add_rune(&mut self, options: &Options) -> Result<(), Error> { + let rune = get_rune(self, options)?; + self.rune = Some(rune); + Ok(()) + } } impl L2Node for Cln { @@ -178,7 +184,7 @@ pub fn build_cln( // Passing these args on the command line is unavoidable due to how the docker image is setup let command = Command::Simple( format!( - "--network={} --lightning-dir=/home/clightning", + "--network={} --lightning-dir=/home/clightning --developer", options.network ) .to_string(), @@ -199,10 +205,7 @@ pub fn build_cln( ]), env_file: Some(EnvFile::Simple(".env".to_owned())), command: Some(command), - volumes: Volumes::Simple(vec![ - format!("{}:/home/clightning:rw", cln_conf.path_vol), - format!("{}/certs:/opt/c-lightning-rest/certs:rw", cln_conf.path_vol), - ]), + volumes: Volumes::Simple(vec![format!("{}:/home/clightning:rw", cln_conf.path_vol)]), networks: Networks::Simple(vec![NETWORK.to_owned()]), ..Default::default() }; @@ -211,11 +214,8 @@ pub fn build_cln( .insert(cln_conf.container_name.clone(), Some(cln)); cln_conf.server_url = format!("https://localhost:{}", rest_port.to_string()); info!( - "connect to {} via rest using {} with access macaroons at {} and via rpc using localhost:{} ", - cln_conf.container_name, - cln_conf.server_url, - cln_conf.macaroon_path, - grpc_port, + "connect to {} via rest using {} and via rpc using localhost:{} ", + cln_conf.container_name, cln_conf.server_url, grpc_port, ); cln_conf.grpc_port = grpc_port.to_string(); cln_conf.rest_port = rest_port.to_string(); @@ -275,7 +275,7 @@ pub fn build_and_save_config( pubkey: None, server_url: format!("http://{}:10000", container_name), path_vol: full_path.clone(), - macaroon_path: format!("{}/certs/access.macaroon", full_path), + rune: None, rpc_server: format!("{}:10000", container_name), grpc_port: "10000".to_owned(), p2p_port: "9735".to_owned(), @@ -378,13 +378,13 @@ fn load_config( alias: name.to_owned(), container_name: container_name.to_owned(), pubkey: None, + rune: None, rpc_server: format!("{}:10000", container_name), server_url: format!("https://{}:8080", container_name), path_vol: full_path.to_owned(), grpc_port: "10000".to_owned(), p2p_port: "9735".to_owned(), rest_port: "8080".to_owned(), - macaroon_path: format!("{}/certs/access.macaroon", full_path), bitcoind_node_container_name: bitcoind_service, network, }) @@ -798,3 +798,25 @@ fn get_rhash(node: &Cln, options: &Options) -> Result { } Ok(found_rhash.unwrap()) } + +fn get_rune(node: &Cln, options: &Options) -> Result { + let network: String = format!("--network={}", options.network); + let compose_path = options.compose_path.as_ref().unwrap(); + + let commands = vec![ + "-f", + compose_path, + "exec", + node.get_container_name(), + "lightning-cli", + "--lightning-dir=/home/clightning", + &network, + "createrune", + ]; + let output = run_command(options, "createrune".to_owned(), commands)?; + let found_rune: Option = node.get_property("rune", output); + if found_rune.is_none() { + error!("no rune found"); + } + Ok(found_rune.unwrap()) +} diff --git a/doppler/src/conf_handler.rs b/doppler/src/conf_handler.rs index 6ae3719..117b133 100644 --- a/doppler/src/conf_handler.rs +++ b/doppler/src/conf_handler.rs @@ -362,6 +362,7 @@ impl Options { for coreln in self.cln_nodes.iter_mut() { coreln.add_pubkey(&options_clone); + coreln.add_rune(&options_clone)?; } Ok(()) } diff --git a/doppler/src/docker.rs b/doppler/src/docker.rs index 48a5ef0..5b5a5df 100644 --- a/doppler/src/docker.rs +++ b/doppler/src/docker.rs @@ -4,9 +4,10 @@ use crate::{ }; use anyhow::{anyhow, Error}; use log::{debug, error, info}; -use std::fs::{File, OpenOptions}; +use std::fs::{self, File, OpenOptions}; use std::io::Write; use std::os::unix::prelude::PermissionsExt; +use std::path::Path; use std::process::{Command, Output}; use std::str::from_utf8; use std::thread; @@ -24,7 +25,9 @@ pub fn load_options_from_external_nodes( options.load_lnds()?; debug!("loaded lnds"); let network = options.external_nodes.clone().unwrap()[0].network.clone(); + create_ui_config_files(&options, &network)?; + Ok(()) } @@ -94,8 +97,6 @@ pub fn run_cluster(options: &mut Options, compose_path: &str) -> Result<(), Erro debug!("saved cluster config"); start_docker_compose(options)?; - create_ui_config_files(options, &options.network)?; - debug!("started cluster"); //simple wait for docker-compose to spin up thread::sleep(Duration::from_secs(6)); @@ -104,8 +105,10 @@ pub fn run_cluster(options: &mut Options, compose_path: &str) -> Result<(), Erro mine_initial_blocks(options)?; } setup_l2_nodes(options)?; + create_ui_config_files(options, &options.network) + .map_err(|e| anyhow!("error creating ui config: {}", e))?; if options.aliases && options.external_nodes.is_none() { - update_bash_alias(options)?; + update_bash_alias(options).map_err(|e| anyhow!("error creating alias: {}", e))?; } Ok(()) @@ -230,6 +233,9 @@ fn update_bash_alias(options: &Options) -> Result<(), Error> { }); let script_path = "scripts/aliases.sh"; let full_path = get_absolute_path(script_path)?; + if let Some(parent) = Path::new(&full_path).parent() { + fs::create_dir_all(parent)?; + } let mut file: File = OpenOptions::new() .read(true) .write(true) @@ -255,7 +261,7 @@ pub fn update_bash_alias_external(options: &Options) -> Result<(), Error> { {name}() {{ lncli --network={network} --macaroonpath={macaroon_path} --rpcserver={rpcserver} --tlscertpath="" "$@" }} -"#, +"#, name=lnd.node_alias, network=lnd.network, macaroon_path=lnd.macaroon_path, rpcserver=lnd.api_endpoint)); script_content.push('\n'); }); diff --git a/doppler/src/lnd_actions/lnd_cli.rs b/doppler/src/lnd_actions/lnd_cli.rs index 4ca40b4..f7b9993 100644 --- a/doppler/src/lnd_actions/lnd_cli.rs +++ b/doppler/src/lnd_actions/lnd_cli.rs @@ -411,6 +411,7 @@ impl LndCli { ]; // Used for amp payments + #[allow(unused_assignments)] let mut subcommand = String::from(""); if let Some(command) = node_command.subcommand.clone() { subcommand = command; diff --git a/doppler/src/main.rs b/doppler/src/main.rs index 302e41f..7cb49ee 100644 --- a/doppler/src/main.rs +++ b/doppler/src/main.rs @@ -40,7 +40,7 @@ pub struct Cli { app_sub_commands: Option, /// Path to ui config file, used to connect to the nodes via the browser - #[arg(short, long, default_value = "./doppler_ui/ui_config/info.conf")] + #[arg(short, long, default_value = "./ui_config/info.conf.ini")] ui_config_path: String, } diff --git a/doppler/src/visualizer.rs b/doppler/src/visualizer.rs index e7089d4..aa306ac 100644 --- a/doppler/src/visualizer.rs +++ b/doppler/src/visualizer.rs @@ -1,13 +1,17 @@ use anyhow::{Error, Result}; use std::{ - fs::File, + fs::{self, File}, io::{LineWriter, Write}, + path::Path, }; use crate::{get_absolute_path, Options}; pub fn create_ui_config_files(options: &Options, network: &str) -> Result<(), Error> { let config_absolute_path = get_absolute_path(&options.ui_config_path)?; + if let Some(parent) = Path::new(&config_absolute_path).parent() { + fs::create_dir_all(parent)?; + } let node_config_file = File::create(config_absolute_path)?; let mut node_config = LineWriter::new(node_config_file); for node in options.lnd_nodes.clone() { @@ -27,7 +31,7 @@ pub fn create_ui_config_files(options: &Options, network: &str) -> Result<(), Er for node in options.cln_nodes.clone() { let header = format!("[{}] \n", node.alias); - let macaroon = format!("ACCESS_MACAROON_PATH={} \n", node.macaroon_path); + let macaroon = format!("RUNE={} \n", node.rune.unwrap_or_default()); let api_endpoint = format!("API_ENDPOINT={} \n", node.server_url); let network = format!("NETWORK={} \n", network); let node_type = format!("TYPE={} \n", "coreln"); diff --git a/doppler_ui/.gitignore b/doppler_ui/.gitignore index 6635cf5..4d6fb15 100644 --- a/doppler_ui/.gitignore +++ b/doppler_ui/.gitignore @@ -8,3 +8,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +/scripts diff --git a/doppler_ui/README.md b/doppler_ui/README.md index a1d4b58..e9b7064 100644 --- a/doppler_ui/README.md +++ b/doppler_ui/README.md @@ -5,7 +5,20 @@ cd /.doppler/ && node ./build ``` -- Run from "./doppler/doppler_ui" in the git repo: +- Run from "./doppler/doppler_ui" in the git repo (not recommended for non-developers): ``` -UI_CONFIG_PATH=./ui_config npm run build +UI_CONFIG_PATH=./ui_config npm run dev +``` +- make sure to update `./ui_config/server.conf` to have correct path to doppler binary and full path to working directory +``` +dopplerBinaryPath = /$HOME/doppler/target/distrib/doppler-x86_64-unknown-linux-gnu/doppler +currentWorkingDirectory = /$HOME/doppler/doppler_ui +``` +- NOTE: you may need to copy the ./doppler/config into ./doppler_ui/, that's how to handle the following error if seen in the doppler logs from the UI: +``` +thread 'main' panicked at doppler/src/bitcoind.rs:116:61: + +called `Result::unwrap()` on an `Err` value: No such file or directory (os error 2) + +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` diff --git a/doppler_ui/package-lock.json b/doppler_ui/package-lock.json index da42804..359fa6d 100644 --- a/doppler_ui/package-lock.json +++ b/doppler_ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "doppler", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doppler", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "bun": "^1.0.0", "chokidar": "^4.0.1", diff --git a/doppler_ui/package.json b/doppler_ui/package.json index 468a9e8..34750f5 100644 --- a/doppler_ui/package.json +++ b/doppler_ui/package.json @@ -1,6 +1,6 @@ { "name": "doppler", - "version": "0.3.1", + "version": "0.3.2", "repository": "github:tee8z/doppler", "bin": { "doppler_ui": "build/index.js" diff --git a/doppler_ui/src/components/Visualizer.svelte b/doppler_ui/src/components/Visualizer.svelte index e06982a..992a4c1 100644 --- a/doppler_ui/src/components/Visualizer.svelte +++ b/doppler_ui/src/components/Visualizer.svelte @@ -5,12 +5,13 @@ import { edges, nodes } from './Graph.svelte'; import Info from './Info.svelte'; import Button from './Button.svelte'; - import type { Connections } from '../routes/api/connections/+server'; + import type { Connections } from '$lib/connections'; import { getConnections } from '$lib/connections'; import type { NodeRequests, Nodes } from '$lib/nodes'; import { CorelnRequests } from '$lib/coreln_requests'; import { EclairRequests } from '$lib/eclair_requests'; - import Page from '../routes/+page.svelte'; + import { ChannelMapper } from '$lib/node_mapper'; + let dataPromise: Promise | null = null; let poller: ReturnType; let isPolling = false; @@ -65,7 +66,7 @@ }; } } else if (connectionConfig.type === 'coreln') { - const requests = new CorelnRequests(connectionConfig.host, connectionConfig.macaroon); + const requests = new CorelnRequests(connectionConfig.host, connectionConfig.rune); const channels = await requests.fetchChannels(); const balance = await requests.fetchBalance(); const info = await requests.fetchInfo(); @@ -126,203 +127,15 @@ let key = Object.keys(nodeData)[0]; //Set starting node setNode(nodeData[key]); - let cur_nodes: any[] = []; - let cur_edges: any[] = []; - map_lnd_channels(cur_nodes, cur_edges, nodeData); - map_coreln_channels(cur_nodes, cur_edges, nodeData); - map_eclair_channels(cur_nodes, cur_edges, nodeData); + const channelMapper = new ChannelMapper(); + const { cur_nodes, cur_edges } = channelMapper.processNodeData(nodeData, nodeConnections); + nodes.set(cur_nodes); edges.set(cur_edges); dataPromise = Promise.resolve(nodeData); }; - function map_lnd_channels(nodes: any[], edges: any[], nodeData: Nodes) { - Object.entries(nodeData).forEach(([key, value]: [string, any]) => { - if (value.type !== 'lnd') { - return; - } - if (!value.online) { - return; - } - if (!uniqueNodes.has(value.info.identity_pubkey)) { - uniqueNodes.add(value.info.identity_pubkey); - nodes.push({ - id: value.info.identity_pubkey, - alias: key, - known: value.info.identity_pubkey, - type: value.type - }); - } - }); - Object.entries(nodeData).forEach(([key, value]: [string, any]) => { - if (value.type !== 'lnd') { - return; - } - if (!value.online) { - return; - } - let current_pubkey = value.info.identity_pubkey; - value.channels.forEach((channel: any) => { - if (!channel.remote_pubkey) { - return; - } - if (!uniqueNodes.has(channel.remote_pubkey)) { - uniqueNodes.add(channel.remote_pubkey); - let known = nodeConnections.find((node) => node.pubkey === channel.remote_pubkey); - nodes.push({ - id: channel.remote_pubkey, - alias: known && known.alias ? known.alias : channel.remote_pubkey, - known: channel.remote_pubkey - }); - } - if (!channel.initiator) { - return; - } - if (uniqueChannels.has(channel.chan_id)) { - return; - } - console.log(channel); - uniqueChannels.add(channel.chan_id); - edges.push({ - source: current_pubkey, - target: channel.remote_pubkey, - channel_id: channel.chan_id, - capacity: channel.capacity, - local_balance: channel.local_balance, - remote_balance: channel.remote_balance, - initiator: channel.initiator, - active: channel.active, - channel: channel - }); - }); - }); - } - - function map_coreln_channels(nodes: any[], edges: any[], nodeData: Nodes) { - Object.entries(nodeData).forEach(([key, value]: [string, any]) => { - if (value.type !== 'coreln') { - return; - } - if (!value.online) { - return; - } - if (!uniqueNodes.has(value.info.id)) { - uniqueNodes.add(value.info.id); - nodes.push({ - id: value.info.id, - alias: key, - known: value.info.id, - type: value.type - }); - } - }); - Object.entries(nodeData).forEach(([key, value]: [string, any]) => { - if (value.type !== 'coreln') { - return; - } - if (!value.online) { - return; - } - let current_pubkey = value.info.id; - value.channels.forEach((channel: any) => { - if (!uniqueNodes.has(channel.id)) { - uniqueNodes.add(channel.id); - let known = nodeConnections.find((node) => node.pubkey === channel.id); - nodes.push({ - id: channel.id, - alias: known && known.alias ? known.alias : channel.id, - known: current_pubkey - }); - } - if (!(channel.opener === 'local')) { - return; - } - if (uniqueChannels.has(channel.channel_id)) { - return; - } - console.log(channel); - uniqueChannels.add(channel.channel_id); - let edge = { - source: current_pubkey, - target: channel.id, - channel_id: channel.channel_id, - capacity: channel.msatoshi_total, - local_balance: Math.floor(channel.msatoshi_to_us / 1000), - remote_balance: Math.floor(channel.msatoshi_to_them / 1000), - initiator: channel.opener === 'local', - active: channel.state === 'CHANNELD_NORMAL', - channel: channel - }; - edges.push(edge); - }); - }); - } - - function map_eclair_channels(nodes: any[], edges: any[], nodeData: Nodes) { - Object.entries(nodeData).forEach(([key, value]: [string, any]) => { - if (value.type !== 'eclair') { - return; - } - if (!value.online) { - return; - } - if (!uniqueNodes.has(value.info.nodeId)) { - uniqueNodes.add(value.info.nodeId); - nodes.push({ - id: value.info.nodeId, - alias: key, - known: value.info.nodeId, - type: value.type - }); - } - }); - Object.entries(nodeData).forEach(([key, value]: [string, any]) => { - if (value.type !== 'eclair') { - return; - } - if (!value.online) { - return; - } - let current_pubkey = value.info.nodeId; - value.channels.forEach((channel: any) => { - if (!uniqueNodes.has(channel.nodeId)) { - uniqueNodes.add(channel.nodeId); - let known = nodeConnections.find((node) => node.pubkey === channel.nodeId); - nodes.push({ - id: channel.nodeId, - alias: known && known.alias ? known.alias : channel.nodeId, - known: current_pubkey - }); - } - if (!channel.data.commitments.params.localParams.isInitiator) { - return; - } - if (uniqueChannels.has(channel.channelId)) { - return; - } - uniqueChannels.add(channel.channelId); - console.log(channel); - let edge = { - source: current_pubkey, - target: channel.nodeId, - channel_id: channel.channelId, - capacity: channel.data.commitments.active[0].fundingTx.amountSatoshis, - local_balance: Math.floor( - channel.data.commitments.active[0].localCommit.spec.toLocal / 1000 - ), - remote_balance: Math.floor( - channel.data.commitments.active[0].localCommit.spec.toRemote / 1000 - ), // TODO fix these and see what happens when multiple payments are sent - initiator: channel.data.commitments.params.localParams.isInitiator, - active: channel.state === 'NORMAL', - channel: channel - }; - edges.push(edge); - }); - }); - } - onMount(async () => { if (poller) { clearInterval(poller); diff --git a/doppler_ui/src/lib/connections.ts b/doppler_ui/src/lib/connections.ts index a7bf50e..a182797 100644 --- a/doppler_ui/src/lib/connections.ts +++ b/doppler_ui/src/lib/connections.ts @@ -1,24 +1,25 @@ export interface ConnectionConfig { - macaroon: string; - password: string; - host: string; - type: string; + macaroon: string; + password: string; + rune: string; + host: string; + type: string; } export interface Connections { - [key: string]: ConnectionConfig; + [key: string]: ConnectionConfig; } export async function getConnections(): Promise { - try { - const response = await fetch('/api/connections'); - if (!response.ok) { - throw new Error('Failed to fetch connections'); - } - const connections = await response.json(); - return connections - } catch (error) { - console.error('Error fetching connections:', error); - } - return {} -} \ No newline at end of file + try { + const response = await fetch('/api/connections'); + if (!response.ok) { + throw new Error('Failed to fetch connections'); + } + const connections = await response.json(); + return connections; + } catch (error) { + console.error('Error fetching connections:', error); + } + return {}; +} diff --git a/doppler_ui/src/lib/coreln_requests.ts b/doppler_ui/src/lib/coreln_requests.ts index 6311cde..ae496d9 100644 --- a/doppler_ui/src/lib/coreln_requests.ts +++ b/doppler_ui/src/lib/coreln_requests.ts @@ -1,39 +1,160 @@ -import { BaseRequestHandler, type NodeRequests } from "./nodes"; +import { BaseRequestHandler, type NodeRequests } from './nodes'; export interface CorelnRequests { - new(base_url: string, macaroon: string): void; + new (base_url: string, macaroon: string): void; } export class CorelnRequests implements CorelnRequests, NodeRequests { - requestHandler: BaseRequestHandler - - constructor(base_url: string, macaroon: string) { - const header = { - 'macaroon': macaroon, - 'encodingtype': 'hex' - }; - const proxy = '/api/proxy'; - this.requestHandler = new BaseRequestHandler(base_url, header, proxy); - } - //API docs: https://github.com/Ride-The-Lightning/c-lightning-REST - - async fetchChannels(): Promise { - let url = `${this.requestHandler.base_url}/v1/channel/listChannels` - return await this.requestHandler.send_request(url, "GET", false); - } - - async fetchInfo(): Promise { - let url = `${this.requestHandler.base_url}/v1/getinfo` - return await this.requestHandler.send_request(url, "GET", false); - } - - async fetchBalance(): Promise { - let url = `${this.requestHandler.base_url}/v1/getBalance` - return await this.requestHandler.send_request(url, "GET", false); - } - - async fetchSpecificNodeInfo(pubkey: String): Promise { - let url = `${this.requestHandler.base_url}/v1/network/listNode/${pubkey}` - return await this.requestHandler.send_request(url, "GET", false); - } -}; \ No newline at end of file + requestHandler: BaseRequestHandler; + + constructor(base_url: string, rune: string) { + const header = { + Rune: rune + }; + const proxy = '/api/proxy'; + this.requestHandler = new BaseRequestHandler(base_url, header, proxy); + } + //API docs: https://docs.corelightning.org/reference/get_list_methods_resource + // look at the commands in "JSON-RPC API Reference" and pass them via /v1/{command} + + async fetchChannels(): Promise { + try { + const url = `${this.requestHandler.base_url}/v1/listpeers`; + const data = await this.requestHandler.send_request(url, 'POST', false); + console.log(data); + if (!data || !data.peers) { + return []; + } + const filteredPeers = data.peers.filter( + (peer: any) => peer.channels && peer.channels.length > 0 + ); + + const chanList = await Promise.all( + filteredPeers.map((peer: any) => getAliasForChannels(peer, this.fetchSpecificNodeInfo)) + ); + console.log(chanList); + return chanList.flatMap((chan) => chan); + } catch (err: any) { + console.error(err); + return { + status: 500, + error: err.message || 'An error occurred while fetching the channels' + }; + } + } + + async fetchInfo(): Promise { + let url = `${this.requestHandler.base_url}/v1/getinfo`; + return await this.requestHandler.send_request(url, 'POST', false); + } + + async fetchBalance(): Promise { + try { + const url = `${this.requestHandler.base_url}/v1/listfunds`; + const data = await this.requestHandler.send_request(url, 'POST', false); + + const opArray = data.outputs; + let confBalance = 0; + let unconfBalance = 0; + + for (const output of opArray) { + if (output.status === 'confirmed') { + confBalance += output.amount_msat; + } else if (output.status === 'unconfirmed') { + unconfBalance += output.amount_msat / 1000; + } + } + + const totalBalance = confBalance + unconfBalance; + + return { + totalBalance, + confBalance, + unconfBalance + }; + } catch (err: any) { + console.error(err); + return { + status: 500, + error: err.message || 'An error occurred while fetching the balance' + }; + } + } + async fetchSpecificNodeInfo(pubkey: String): Promise { + let url = `${this.requestHandler.base_url}/v1/listnodes?id=${pubkey}`; + return await this.requestHandler.send_request(url, 'POST', false); + } +} + +const getAliasForChannels = (peer: any, fetchSpecificNodeInfo: (id: string) => Promise) => { + return new Promise(function (resolve, reject) { + fetchSpecificNodeInfo(peer.id) + .then((data: any) => { + resolve( + peer.channels + .filter((c: any) => c.state !== 'ONCHAIN' && c.state !== 'CLOSED') + .reduce((acc: any, channel: any) => { + const TO_US_MSATS = channel.msatoshi_to_us || channel.to_us_msat; + const TOTAL_MSATS = channel.msatoshi_total || channel.total_msat; + acc.push({ + id: peer.id, + alias: data.nodes[0] ? data.nodes[0].alias : peer.id, + connected: peer.connected, + state: channel.state, + short_channel_id: channel.short_channel_id, + channel_id: channel.channel_id, + funding_txid: channel.funding_txid, + private: channel.private, + msatoshi_to_us: TO_US_MSATS, + msatoshi_total: TOTAL_MSATS, + msatoshi_to_them: TOTAL_MSATS - TO_US_MSATS, + their_channel_reserve_satoshis: + channel.their_channel_reserve_satoshis || channel.their_reserve_msat, + our_channel_reserve_satoshis: + channel.our_channel_reserve_satoshis || channel.our_reserve_msat, + spendable_msatoshi: channel.spendable_msatoshi || channel.spendable_msat, + funding_allocation_msat: channel.funding_allocation_msat, + opener: channel.opener, + direction: channel.direction, + htlcs: channel.htlcs + }); + return acc; + }, []) + ); + }) + .catch((err: any) => { + console.error(err); + resolve( + peer.channels + .filter((c: any) => c.state !== 'ONCHAIN' && c.state !== 'CLOSED') + .reduce((acc: any, channel: any) => { + const TO_US_MSATS = channel.msatoshi_to_us || channel.to_us_msat; + const TOTAL_MSATS = channel.msatoshi_total || channel.total_msat; + acc.push({ + id: peer.id, + alias: peer.id, + connected: peer.connected, + state: channel.state, + short_channel_id: channel.short_channel_id, + channel_id: channel.channel_id, + funding_txid: channel.funding_txid, + private: channel.private, + msatoshi_to_us: TO_US_MSATS, + msatoshi_total: TOTAL_MSATS, + msatoshi_to_them: TOTAL_MSATS - TO_US_MSATS, + their_channel_reserve_satoshis: + channel.their_channel_reserve_satoshis || channel.their_reserve_msat, + our_channel_reserve_satoshis: + channel.our_channel_reserve_satoshis || channel.our_reserve_msat, + spendable_msatoshi: channel.spendable_msatoshi || channel.spendable_msat, + funding_allocation_msat: channel.funding_allocation_msat, + opener: channel.opener, + direction: channel.direction, + htlcs: channel.htlcs + }); + return acc; + }, []) + ); + }); + }); +}; diff --git a/doppler_ui/src/lib/node_mapper.ts b/doppler_ui/src/lib/node_mapper.ts new file mode 100644 index 0000000..cf1134e --- /dev/null +++ b/doppler_ui/src/lib/node_mapper.ts @@ -0,0 +1,281 @@ +interface Node { + id: string; + alias: string; + known: string; + type?: string; +} + +interface Channel { + source: string; + target: string; + channel_id: string; + capacity: number; + local_balance: number; + remote_balance: number; + initiator: boolean; + active: boolean; + channel: any; + types?: string[]; +} + +interface NodeConnection { + pubkey: string; + alias: string; + connection: any; +} + +interface Nodes { + [key: string]: any; +} + +export class ChannelMapper { + private nodeMap: Map; + private channelMap: Map; + private uniqueNodes: Set; + private uniqueChannels: Set; + private nodeConnections: NodeConnection[]; + private nodes: Node[]; + private edges: Channel[]; + + constructor() { + this.nodeMap = new Map(); + this.channelMap = new Map(); + this.uniqueNodes = new Set(); + this.uniqueChannels = new Set(); + this.nodeConnections = []; + this.nodes = []; + this.edges = []; + } + + public processNodeData(nodeData: Nodes, nodeConnections: NodeConnection[]) { + this.clear(); + this.nodeConnections = nodeConnections; + + // Process each implementation type in order + this.map_lnd_channels(nodeData); + this.map_coreln_channels(nodeData); + this.map_eclair_channels(nodeData); + + return { + cur_nodes: this.nodes, + cur_edges: this.edges + }; + } + + private clear() { + this.nodeMap.clear(); + this.channelMap.clear(); + this.uniqueNodes.clear(); + this.uniqueChannels.clear(); + this.nodes = []; + this.edges = []; + } + + private map_lnd_channels(nodeData: Nodes) { + Object.entries(nodeData).forEach(([key, value]: [string, any]) => { + if (value.type !== 'lnd' || !value.online) return; + + const nodeInfo = { + id: value.info.identity_pubkey, + alias: key, + known: value.info.identity_pubkey, + type: value.type + }; + + if (!this.nodeMap.has(value.info.identity_pubkey)) { + this.nodeMap.set(value.info.identity_pubkey, nodeInfo); + this.nodes.push(nodeInfo); + } else { + const existingNode = this.nodeMap.get(value.info.identity_pubkey)!; + existingNode.type = value.type; + } + this.uniqueNodes.add(value.info.identity_pubkey); + }); + + Object.entries(nodeData).forEach(([key, value]: [string, any]) => { + if (value.type !== 'lnd' || !value.online) return; + + let current_pubkey = value.info.identity_pubkey; + value.channels.forEach((channel: any) => { + if (!channel.remote_pubkey || !channel.initiator) return; + + if (!this.nodeMap.has(channel.remote_pubkey)) { + let known = this.nodeConnections.find((node) => node.pubkey === channel.remote_pubkey); + const nodeInfo = { + id: channel.remote_pubkey, + alias: known?.alias || channel.remote_pubkey, + known: channel.remote_pubkey + }; + this.nodeMap.set(channel.remote_pubkey, nodeInfo); + this.nodes.push(nodeInfo); + } + this.uniqueNodes.add(channel.remote_pubkey); + + const channelInfo: Channel = { + source: current_pubkey, + target: channel.remote_pubkey, + channel_id: channel.chan_id, + capacity: channel.capacity, + local_balance: channel.local_balance, + remote_balance: channel.remote_balance, + initiator: channel.initiator, + active: channel.active, + channel: channel, + types: ['lnd'] + }; + + if (!this.channelMap.has(channel.chan_id)) { + this.channelMap.set(channel.chan_id, channelInfo); + this.edges.push(channelInfo); + this.uniqueChannels.add(channel.chan_id); + } else { + const existingChannel = this.channelMap.get(channel.chan_id)!; + if (!existingChannel.types?.includes('lnd')) { + existingChannel.types = [...(existingChannel.types || []), 'lnd']; + } + Object.assign(existingChannel, channelInfo); + } + }); + }); + } + + private map_coreln_channels(nodeData: Nodes) { + Object.entries(nodeData).forEach(([key, value]: [string, any]) => { + if (value.type !== 'coreln' || !value.online) return; + + const nodeInfo = { + id: value.info.id, + alias: key, + known: value.info.id, + type: value.type + }; + + if (!this.nodeMap.has(value.info.id)) { + this.nodeMap.set(value.info.id, nodeInfo); + this.nodes.push(nodeInfo); + } else { + const existingNode = this.nodeMap.get(value.info.id)!; + existingNode.type = value.type; + } + this.uniqueNodes.add(value.info.id); + }); + + Object.entries(nodeData).forEach(([key, value]: [string, any]) => { + if (value.type !== 'coreln' || !value.online) return; + + let current_pubkey = value.info.id; + value.channels.forEach((channel: any) => { + if (channel.opener !== 'local') return; + + if (!this.nodeMap.has(channel.id)) { + let known = this.nodeConnections.find((node) => node.pubkey === channel.id); + const nodeInfo = { + id: channel.id, + alias: known?.alias || channel.id, + known: current_pubkey + }; + this.nodeMap.set(channel.id, nodeInfo); + this.nodes.push(nodeInfo); + } + this.uniqueNodes.add(channel.id); + + const channelInfo: Channel = { + source: current_pubkey, + target: channel.id, + channel_id: channel.channel_id, + capacity: channel.msatoshi_total, + local_balance: Math.floor(channel.msatoshi_to_us / 1000), + remote_balance: Math.floor(channel.msatoshi_to_them / 1000), + initiator: channel.opener === 'local', + active: channel.state === 'CHANNELD_NORMAL', + channel: channel, + types: ['coreln'] + }; + + if (!this.channelMap.has(channel.channel_id)) { + this.channelMap.set(channel.channel_id, channelInfo); + this.edges.push(channelInfo); + this.uniqueChannels.add(channel.channel_id); + } else { + const existingChannel = this.channelMap.get(channel.channel_id)!; + if (!existingChannel.types?.includes('coreln')) { + existingChannel.types = [...(existingChannel.types || []), 'coreln']; + } + Object.assign(existingChannel, channelInfo); + } + }); + }); + } + + private map_eclair_channels(nodeData: Nodes) { + Object.entries(nodeData).forEach(([key, value]: [string, any]) => { + if (value.type !== 'eclair' || !value.online) return; + + const nodeInfo = { + id: value.info.nodeId, + alias: key, + known: value.info.nodeId, + type: value.type + }; + + if (!this.nodeMap.has(value.info.nodeId)) { + this.nodeMap.set(value.info.nodeId, nodeInfo); + this.nodes.push(nodeInfo); + } else { + const existingNode = this.nodeMap.get(value.info.nodeId)!; + existingNode.type = value.type; + } + this.uniqueNodes.add(value.info.nodeId); + }); + + Object.entries(nodeData).forEach(([key, value]: [string, any]) => { + if (value.type !== 'eclair' || !value.online) return; + + let current_pubkey = value.info.nodeId; + value.channels.forEach((channel: any) => { + if (!channel.data.commitments.params.localParams.isInitiator) return; + + if (!this.nodeMap.has(channel.nodeId)) { + let known = this.nodeConnections.find((node) => node.pubkey === channel.nodeId); + const nodeInfo = { + id: channel.nodeId, + alias: known?.alias || channel.nodeId, + known: current_pubkey + }; + this.nodeMap.set(channel.nodeId, nodeInfo); + this.nodes.push(nodeInfo); + } + this.uniqueNodes.add(channel.nodeId); + + const channelInfo: Channel = { + source: current_pubkey, + target: channel.nodeId, + channel_id: channel.channelId, + capacity: channel.data.commitments.active[0].fundingTx.amountSatoshis, + local_balance: Math.floor( + channel.data.commitments.active[0].localCommit.spec.toLocal / 1000 + ), + remote_balance: Math.floor( + channel.data.commitments.active[0].localCommit.spec.toRemote / 1000 + ), + initiator: channel.data.commitments.params.localParams.isInitiator, + active: channel.state === 'NORMAL', + channel: channel, + types: ['eclair'] + }; + + if (!this.channelMap.has(channel.channelId)) { + this.channelMap.set(channel.channelId, channelInfo); + this.edges.push(channelInfo); + this.uniqueChannels.add(channel.channelId); + } else { + const existingChannel = this.channelMap.get(channel.channelId)!; + if (!existingChannel.types?.includes('eclair')) { + existingChannel.types = [...(existingChannel.types || []), 'eclair']; + } + Object.assign(existingChannel, channelInfo); + } + }); + }); + } +} diff --git a/doppler_ui/src/routes/api/connections/+server.ts b/doppler_ui/src/routes/api/connections/+server.ts index 3973f85..e5e0a2e 100644 --- a/doppler_ui/src/routes/api/connections/+server.ts +++ b/doppler_ui/src/routes/api/connections/+server.ts @@ -6,6 +6,7 @@ import * as path from 'path'; export interface ConnectionConfig { macaroon: string; + rune: string; password: string; type: string; host: string; @@ -55,16 +56,13 @@ export const GET: RequestHandler = async function () { macaroon: readMacaroon, host: sectionConfig.API_ENDPOINT, type: sectionConfig.TYPE, + rune: '', password: '' }; } else if (sectionConfig.TYPE === 'coreln') { - const macaroonBuffer = safeReadFileSync(sectionConfig.ACCESS_MACAROON_PATH); - if (macaroonBuffer === null) { - continue; - } - const readMacaroon = Buffer.from(macaroonBuffer).toString('hex'); connections[section] = { - macaroon: readMacaroon, + macaroon: '', + rune: sectionConfig.RUNE, host: sectionConfig.API_ENDPOINT, type: sectionConfig.TYPE, password: '' @@ -72,6 +70,7 @@ export const GET: RequestHandler = async function () { } else if (sectionConfig.TYPE === 'eclair') { connections[section] = { macaroon: '', + rune: '', host: sectionConfig.API_ENDPOINT, password: sectionConfig.API_PASSWORD, type: sectionConfig.TYPE diff --git a/doppler_ui/ui_config/server.conf.ini b/doppler_ui/ui_config/server.conf.ini index a8524c5..ae98eb0 100644 --- a/doppler_ui/ui_config/server.conf.ini +++ b/doppler_ui/ui_config/server.conf.ini @@ -1,6 +1,6 @@ [paths] -dopplerScriptsFolder = ~/.doppler/doppler_scripts -logsFolder = ~/.doppler/doppler_logs -scriptsFolder = ~/.doppler/scripts -dopplerBinaryPath = ~/.doppler/doppler -currentWorkingDirectory = ~/.doppler +dopplerScriptsFolder =./doppler_scripts +logsFolder = ./doppler_logs +scriptsFolder = ../scripts +dopplerBinaryPath = $HOME/doppler/target/distrib/doppler-x86_64-unknown-linux-gnu/doppler +currentWorkingDirectory = $HOME/doppler/doppler_ui diff --git a/doppler_ui/ui_config/server.example.conf.ini b/doppler_ui/ui_config/server.example.conf.ini deleted file mode 100644 index 04ddf3f..0000000 --- a/doppler_ui/ui_config/server.example.conf.ini +++ /dev/null @@ -1,6 +0,0 @@ -[paths] -dopplerScriptsFolder = doppler_scripts -logsFolder = doppler_logs -scriptsFolder = ./scripts -dopplerBinaryPath = ./doppler -currentWorkingDirectory = ./target/distrib/doppler-x86_64-unknown-linux-gnu diff --git a/examples/doppler_files/all_node_implementations/open_channels.doppler b/examples/doppler_files/all_node_implementations/open_channels.doppler index 8674da2..612a685 100644 --- a/examples/doppler_files/all_node_implementations/open_channels.doppler +++ b/examples/doppler_files/all_node_implementations/open_channels.doppler @@ -1,10 +1,9 @@ SKIP_CONF -bd SEND_COINS coreln AMT 500000 -bd MINE_BLOCKS 20 +bd SEND_COINS coreln AMT 500000 +bd MINE_BLOCKS 5 lnd OPEN_CHANNEL coreln AMT 100000 bd MINE_BLOCKS 6 eclair OPEN_CHANNEL lnd AMT 100000 -eclair OPEN_CHANNEL lnd AMT 100000 bd MINE_BLOCKS 6 -coreln OPEN_CHANNEL eclair AMT 100000 +eclair OPEN_CHANNEL coreln AMT 100000 bd MINE_BLOCKS 6