diff --git a/Cargo.lock b/Cargo.lock index 9f4d377b6..9bbbf55a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,7 @@ dependencies = [ "futures", "gl-client", "hex", + "serde_json", "thiserror 2.0.18", "tokio", "vls-core", diff --git a/libs/gl-cli/Cargo.toml b/libs/gl-cli/Cargo.toml index b0bd4d40f..f1babf3bf 100644 --- a/libs/gl-cli/Cargo.toml +++ b/libs/gl-cli/Cargo.toml @@ -28,6 +28,7 @@ hex = "0.4" thiserror = "2.0.11" tokio = "1.43.0" vls-core.workspace = true +serde_json = "^1.0" [badges] maintenance = { status = "actively-developed" } diff --git a/libs/gl-cli/src/bin/glcli.rs b/libs/gl-cli/src/bin/glcli.rs index d224311c6..a89ade553 100644 --- a/libs/gl-cli/src/bin/glcli.rs +++ b/libs/gl-cli/src/bin/glcli.rs @@ -1,14 +1,21 @@ use clap::Parser; use gl_cli::{run, Cli}; +use serde_json::json; #[tokio::main] async fn main() { let cli = Cli::parse(); + let json_print = cli.json; match run(cli).await { Ok(()) => (), Err(e) => { - println!("{}", e); + if json_print { + let j = json!({"error": e.to_string()}); + println!("{}", serde_json::to_string_pretty(&j).unwrap()); + } else { + println!("{}", e); + } } } } diff --git a/libs/gl-cli/src/error.rs b/libs/gl-cli/src/error.rs index e13f4ba59..12b7162b6 100644 --- a/libs/gl-cli/src/error.rs +++ b/libs/gl-cli/src/error.rs @@ -16,6 +16,9 @@ pub enum Error { #[error(transparent)] UtilError(#[from] util::UtilsError), + + #[error("Failed to serialize response: {0}")] + JsonResponseError(String), } impl Error { @@ -31,6 +34,10 @@ impl Error { pub fn credentials_not_found(e: impl std::fmt::Display) -> Error { Error::CredentialsNotFoundError(e.to_string()) } + + pub fn failed_response_serialization(e: impl std::fmt::Display) -> Error { + Error::JsonResponseError(e.to_string()) + } } // -- Disable hints for now as it would require to get rid of thiserror. Might diff --git a/libs/gl-cli/src/json_hex.rs b/libs/gl-cli/src/json_hex.rs new file mode 100644 index 000000000..0360ca019 --- /dev/null +++ b/libs/gl-cli/src/json_hex.rs @@ -0,0 +1,296 @@ +use gl_client::pb::cln; +use serde_json::json; + +pub trait ToJsonHex { + fn to_json_hex(&self) -> serde_json::Value; +} + +impl ToJsonHex for cln::GetinfoResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "id": hex::encode(&self.id), + "color": hex::encode(&self.color), + "num_peers": self.num_peers, + "num_pending_channels": self.num_pending_channels, + "num_active_channels": self.num_active_channels, + "num_inactive_channels": self.num_inactive_channels, + "address": self.address.clone(), + "binding": self.binding.clone(), + "version": self.version.clone(), + "blockheight": self.blockheight, + "network": self.network, + "lightning_dir": self.lightning_dir.clone(), + "fees_collected_msat": self.fees_collected_msat.clone().map_or(0, |amt| amt.msat), + }); + if let Some(alias) = &self.alias { + j["alias"] = json!(alias); + } + if let Some(feat) = &self.our_features { + j["our_features"] = json!({ + "init": hex::encode(&feat.init), + "node": hex::encode(&feat.node), + "channel": hex::encode(&feat.channel), + "invoice": hex::encode(&feat.invoice), + }); + } + if let Some(warn_bsync) = &self.warning_bitcoind_sync { + j["warning_bitcoind_sync"] = json!(warn_bsync); + } + if let Some(warn_lsync) = &self.warning_lightningd_sync { + j["warning_lightningd_sync"] = json!(warn_lsync); + } + j + } +} + +impl ToJsonHex for cln::InvoiceResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "bolt11": self.bolt11.clone(), + "payment_hash": hex::encode(&self.payment_hash), + "payment_secret": hex::encode(&self.payment_secret), + "expires_at": self.expires_at, + }); + if let Some(x) = self.created_index { + j["created_index"] = json!(x); + } + if let Some(x) = &self.warning_capacity { + j["warning_capacity"] = json!(x); + } + if let Some(x) = &self.warning_offline { + j["warning_offline"] = json!(x); + } + if let Some(x) = &self.warning_deadends { + j["warning_deadends"] = json!(x); + } + if let Some(x) = &self.warning_private_unused { + j["warning_private_unused"] = json!(x); + } + if let Some(x) = &self.warning_mpp { + j["warning_mpp"] = json!(x); + } + j + } +} + +impl ToJsonHex for cln::PayResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "status": self.status, + "payment_preimage": hex::encode(&self.payment_preimage), + "payment_hash": hex::encode(&self.payment_hash), + "created_at": self.created_at, + "parts": self.parts, + "amount_msat": self.amount_msat.clone().map_or(0, |amt| amt.msat), + "amount_sent_msat": self.amount_sent_msat.clone().map_or(0, |amt| amt.msat), + }); + if let Some(x) = &self.destination { + j["destination"] = json!(hex::encode(x)); + } + if let Some(x) = &self.warning_partial_completion { + j["warning_partial_completion"] = json!(x); + } + j + } +} + +impl ToJsonHex for cln::ListpaysPays { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "payment_hash": hex::encode(&self.payment_hash), + "status": self.status, + "created_at": self.created_at, + }); + if let Some(x) = &self.destination { + j["destination"] = json!(hex::encode(x)); + } + if let Some(x) = self.completed_at { + j["completed_at"] = json!(x); + } + if let Some(x) = &self.label { + j["label"] = json!(x); + } + if let Some(x) = &self.bolt11 { + j["bolt11"] = json!(x); + } + if let Some(x) = &self.description { + j["description"] = json!(x); + } + if let Some(x) = &self.bolt12 { + j["bolt12"] = json!(x); + } + if let Some(x) = &self.amount_msat { + j["amount_msat"] = x.msat.into(); + } + if let Some(x) = &self.amount_sent_msat { + j["amount_sent_msat"] = x.msat.into(); + } + if let Some(x) = &self.preimage { + j["preimage"] = json!(hex::encode(x)); + } + if let Some(x) = self.number_of_parts { + j["number_of_parts"] = json!(x); + } + if let Some(x) = &self.erroronion { + j["erroronion"] = json!(hex::encode(x)); + } + j + } +} + +impl ToJsonHex for cln::ListpaysResponse { + fn to_json_hex(&self) -> serde_json::Value { + json!({ + "pays": json!(self.pays.iter().map(|x| x.to_json_hex()).collect::>()) + }) + } +} + +impl ToJsonHex for cln::ConnectAddress { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "item_type": self.item_type, + }); + if let Some(x) = &self.socket { + j["socket"] = json!(x); + } + if let Some(x) = &self.address { + j["address"] = json!(x); + } + if let Some(x) = &self.port { + j["port"] = json!(x); + } + j + } +} + +impl ToJsonHex for cln::ConnectResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "id": hex::encode(&self.id), + "features": hex::encode(&self.features), + "direction": self.direction, + }); + if let Some(x) = &self.address { + j["address"] = x.to_json_hex(); + } + j + } +} + +impl ToJsonHex for cln::StopResponse { + fn to_json_hex(&self) -> serde_json::Value { + json!({}) + } +} + +impl ToJsonHex for cln::CloseResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "item_type": self.item_type, + }); + if let Some(x) = &self.tx { + j["tx"] = json!(hex::encode(x)); + } + if let Some(x) = &self.txid { + j["txid"] = json!(hex::encode(x)); + } + + j + } +} + +impl ToJsonHex for cln::FundchannelResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "tx": hex::encode(&self.tx), + "txid": hex::encode(&self.txid), + "outnum": self.outnum, + "channel_id": hex::encode(&self.channel_id), + }); + if let Some(x) = &self.close_to { + j["close_to"] = json!(hex::encode(x)); + } + if let Some(x) = &self.mindepth { + j["mindepth"] = json!(x); + } + + j + } +} + +impl ToJsonHex for cln::WithdrawResponse { + fn to_json_hex(&self) -> serde_json::Value { + json!({ + "tx": hex::encode(&self.tx), + "txid": hex::encode(&self.txid), + "psbt": self.psbt.clone(), + }) + } +} + +impl ToJsonHex for cln::ListfundsOutputs { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "txid": hex::encode(&self.txid), + "scriptpubkey": hex::encode(&self.scriptpubkey), + "output": self.output, + "amount_msat": self.amount_msat.clone().map_or(0, |amt| amt.msat), + "status": self.status, + "reserved": self.reserved + }); + if let Some(x) = &self.address { + j["address"] = json!(x); + } + if let Some(x) = &self.redeemscript { + j["redeemscript"] = json!(hex::encode(x)); + } + if let Some(x) = self.blockheight { + j["blockheight"] = json!(x); + } + j + } +} + +impl ToJsonHex for cln::ListfundsChannels { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({ + "peer_id": hex::encode(&self.peer_id), + "our_amount_msat": self.our_amount_msat.clone().map_or(0, |amt| amt.msat), + "amount_msat": self.amount_msat.clone().map_or(0, |amt| amt.msat), + "funding_txid": hex::encode(&self.funding_txid), + "funding_output": self.funding_output, + "connected": self.connected, + "state": self.state, + }); + if let Some(x) = &self.channel_id { + j["channel_id"] = json!(hex::encode(x)); + } + if let Some(x) = &self.short_channel_id { + j["short_channel_id"] = json!(x); + } + j + } +} + +impl ToJsonHex for cln::ListfundsResponse { + fn to_json_hex(&self) -> serde_json::Value { + json!({ + "outputs": json!(self.outputs.iter().map(|x| x.to_json_hex()).collect::>()), + "channels": json!(self.channels.iter().map(|x| x.to_json_hex()).collect::>()) + }) + } +} + +impl ToJsonHex for cln::NewaddrResponse { + fn to_json_hex(&self) -> serde_json::Value { + let mut j = json!({}); + if let Some(x) = &self.p2tr { + j["p2tr"] = json!(x); + } + if let Some(x) = &self.bech32 { + j["bech32"] = json!(x); + } + j + } +} diff --git a/libs/gl-cli/src/lib.rs b/libs/gl-cli/src/lib.rs index 4a1c96f4a..ddbe5b01b 100644 --- a/libs/gl-cli/src/lib.rs +++ b/libs/gl-cli/src/lib.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use gl_client::bitcoin::Network; use std::{path::PathBuf, str::FromStr}; mod error; +mod json_hex; pub mod model; mod node; mod scheduler; @@ -20,6 +21,9 @@ pub struct Cli { network: Network, #[arg(long, short, global = true, help_heading = "Global options")] verbose: bool, + /// Produce json outputs. + #[arg(long, short, global = true, help_heading = "Global options")] + pub json: bool, #[command(subcommand)] cmd: Commands, } @@ -57,6 +61,7 @@ pub async fn run(cli: Cli) -> Result<()> { scheduler::Config { data_dir, network: cli.network, + print_json: cli.json, }, ) .await? @@ -68,6 +73,7 @@ pub async fn run(cli: Cli) -> Result<()> { signer::Config { data_dir, network: cli.network, + print_json: cli.json, }, ) .await? @@ -78,6 +84,7 @@ pub async fn run(cli: Cli) -> Result<()> { node::Config { data_dir, network: cli.network, + print_json: cli.json, }, ) .await? diff --git a/libs/gl-cli/src/node.rs b/libs/gl-cli/src/node.rs index 115bfc151..df71875a8 100644 --- a/libs/gl-cli/src/node.rs +++ b/libs/gl-cli/src/node.rs @@ -1,4 +1,5 @@ use crate::error::{Error, Result}; +use crate::json_hex::ToJsonHex; use crate::model; use crate::util::{self, CREDENTIALS_FILE_NAME, SEED_FILE_NAME}; use clap::Subcommand; @@ -10,6 +11,7 @@ use std::path::Path; pub struct Config> { pub data_dir: P, pub network: Network, + pub print_json: bool, } #[derive(Subcommand, Debug)] @@ -231,6 +233,21 @@ pub async fn command_handler>(cmd: Command, config: Config

) -> } } +macro_rules! print_json_or_pb { + ($print_json: expr, $obj: expr) => { + if $print_json { + let j = $obj.to_json_hex(); + println!( + "{}", + serde_json::to_string_pretty(&j) + .map_err(|e| Error::failed_response_serialization(e))? + ); + } else { + println!("{:?}", $obj); + } + }; +} + async fn init_handler>(config: Config

, mnemonic: Option) -> Result<()> { // Check if seed already exists in the configuration path let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME); @@ -324,24 +341,26 @@ async fn log>(config: Config

) -> Result<()> { } async fn newaddr_handler>(config: Config

) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .new_addr(cln::NewaddrRequest { addresstype: None }) .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } async fn listfunds_handler>(config: Config

) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .list_funds(cln::ListfundsRequest { spent: None }) .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } @@ -350,6 +369,7 @@ async fn withdraw_handler>( destination: String, amount_sat: model::AmountSatOrAll, ) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .withdraw(cln::WithdrawRequest { @@ -362,7 +382,7 @@ async fn withdraw_handler>( .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } @@ -371,6 +391,7 @@ async fn fundchannel_handler>( id: String, amount_sat: model::AmountSatOrAll, ) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let id_bytes = hex::FromHex::from_hex(&id) .map_err(|e| Error::custom(format!("Invalid hex string: {id}. {e}")))?; @@ -393,11 +414,12 @@ async fn fundchannel_handler>( .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } async fn close_handler>(config: Config

, id: String) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .close(cln::CloseRequest { @@ -412,29 +434,31 @@ async fn close_handler>(config: Config

, id: String) -> Result< .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } async fn stop>(config: Config

) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .stop(cln::StopRequest {}) .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } async fn getinfo_handler>(config: Config

) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .getinfo(cln::GetinfoRequest {}) .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } @@ -449,6 +473,7 @@ async fn invoice_handler>( cltv: Option, deschashonly: Option, ) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .invoice(cln::InvoiceRequest { @@ -465,7 +490,7 @@ async fn invoice_handler>( .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } @@ -475,13 +500,14 @@ async fn connect_handler>( host: Option, port: Option, ) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .connect_peer(cln::ConnectRequest { id, host, port }) .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } @@ -491,6 +517,7 @@ async fn listpays_handler>( payment_hash: Option>, status: Option, ) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .list_pays(cln::ListpaysRequest { @@ -504,7 +531,7 @@ async fn listpays_handler>( .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } @@ -523,6 +550,7 @@ async fn pay_handler>( maxfee: Option, description: Option, ) -> Result<()> { + let print_json = config.print_json; let mut node: gl_client::node::ClnClient = get_node(config).await?; let res = node .pay(cln::PayRequest { @@ -543,6 +571,6 @@ async fn pay_handler>( .await .map_err(|e| Error::custom(e.message()))? .into_inner(); - println!("{:?}", res); + print_json_or_pb!(print_json, res); Ok(()) } diff --git a/libs/gl-cli/src/scheduler.rs b/libs/gl-cli/src/scheduler.rs index ce11d90bc..775d7eb40 100644 --- a/libs/gl-cli/src/scheduler.rs +++ b/libs/gl-cli/src/scheduler.rs @@ -13,6 +13,7 @@ use util::{CREDENTIALS_FILE_NAME, SEED_FILE_NAME}; pub struct Config> { pub data_dir: P, pub network: Network, + pub print_json: bool, } #[derive(Subcommand, Debug)] diff --git a/libs/gl-cli/src/signer.rs b/libs/gl-cli/src/signer.rs index 767d30556..e1f8acbc2 100644 --- a/libs/gl-cli/src/signer.rs +++ b/libs/gl-cli/src/signer.rs @@ -13,6 +13,7 @@ use util::{CREDENTIALS_FILE_NAME, SEED_FILE_NAME}; pub struct Config> { pub data_dir: P, pub network: Network, + pub print_json: bool, } #[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]