refactor(cli): extract repeated gRPC client setup and metadata handling

Extracted duplicated gRPC channel creation, client authentication, and metadata handling code into reusable functions to reduce code duplication and improve error handling.
This commit is contained in:
Ignacio Porte 2025-09-19 13:26:31 -03:00
parent e990a6cde2
commit efe5688552

View file

@ -24,6 +24,11 @@ fn get_macaroon_path_default(network: &str) -> PathBuf {
.join(format!(".lnd/data/chain/bitcoin/{network}/admin.macaroon"))
}
fn print_and_exit(msg: &str) -> ! {
println!("{msg}");
exit(1)
}
/// A cli for interacting with lndk.
#[derive(Debug, Parser)]
#[command(name = "lndk-cli")]
@ -175,13 +180,8 @@ async fn main() {
Commands::DecodeOffer { offer_string } => {
println!("Decoding offer: {offer_string}.");
match decode(offer_string) {
Ok(offer) => {
println!("Decoded offer: {:?}.", offer)
}
Err(e) => {
println!("ERROR ({}): {}", e.code(), e);
exit(1)
}
Ok(offer) => println!("Decoded offer: {:?}.", offer),
Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)),
}
}
Commands::DecodeInvoice { invoice_string } => {
@ -192,10 +192,7 @@ async fn main() {
Ok(invoice) => {
println!("Decoded invoice: {:?}.", invoice);
}
Err(e) => {
println!("ERROR ({}): {}", e.code(), e);
exit(1);
}
Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)),
}
}
Commands::PayOffer {
@ -207,37 +204,19 @@ async fn main() {
fee_limit_percent,
} => {
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
let grpc_host = args.grpc_host;
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| {
println!("ERROR creating endpoint: {e:?}");
exit(1)
})
.tls_config(tls)
.unwrap_or_else(|e| {
println!("ERROR tls config: {e:?}");
exit(1)
})
.connect()
.await
.unwrap_or_else(|e| {
println!("ERROR connecting: {e:?}");
exit(1)
});
let mut client = OffersClient::new(channel);
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
let (mut client, macaroon) = create_authenticated_client(
channel,
args.macaroon_path,
args.macaroon_hex,
&args.network,
);
let offer = match decode(offer_string.to_string()) {
Ok(offer) => offer,
Err(e) => {
println!("ERROR ({}): {}", e.code(), e);
exit(1)
}
Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)),
};
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(PayOfferRequest {
offer: offer.to_string(),
amount,
@ -246,7 +225,7 @@ async fn main() {
fee_limit,
fee_limit_percent,
});
add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1));
add_metadata(&mut request, macaroon);
match client.pay_offer(request).await {
Ok(_) => println!("Successfully paid for offer!"),
@ -260,43 +239,26 @@ async fn main() {
response_invoice_timeout,
} => {
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
let grpc_host = args.grpc_host;
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| {
println!("ERROR: failed to create endpoint {e:?}");
exit(1)
})
.tls_config(tls)
.unwrap_or_else(|e| {
println!("ERROR: failed to configure tls {e:?}");
exit(1)
})
.connect()
.await
.unwrap_or_else(|e| {
println!("ERROR: failed to connect {e:?}");
exit(1)
});
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
let (mut client, macaroon) = create_authenticated_client(
channel,
args.macaroon_path,
args.macaroon_hex,
&args.network,
);
let mut client = OffersClient::new(channel);
let offer = match decode(offer_string.to_string()) {
Ok(offer) => offer,
Err(e) => {
println!("ERROR ({}): {}", e.code(), e);
exit(1)
}
Err(e) => print_and_exit(&format!("ERROR ({}): {}", e.code(), e)),
};
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(GetInvoiceRequest {
offer: offer.to_string(),
amount,
payer_note,
response_invoice_timeout,
});
add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1));
add_metadata(&mut request, macaroon);
match client.get_invoice(request).await {
Ok(response) => println!("Invoice: {:?}.", response.get_ref()),
Err(err) => print_grpc_error(err),
@ -309,35 +271,21 @@ async fn main() {
fee_limit_percent,
} => {
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
let grpc_host = args.grpc_host.clone();
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| {
println!("ERROR: failed to create endpoint {e:?}");
exit(1)
})
.tls_config(tls)
.unwrap_or_else(|e| {
println!("ERROR: failed to configure tls {e:?}");
exit(1)
})
.connect()
.await
.unwrap_or_else(|e| {
println!("ERROR: failed to connect {e:?}");
exit(1)
});
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
let (mut client, macaroon) = create_authenticated_client(
channel,
args.macaroon_path,
args.macaroon_hex,
&args.network,
);
let mut client = OffersClient::new(channel);
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(PayInvoiceRequest {
invoice: invoice_string.to_owned(),
amount,
fee_limit,
fee_limit_percent,
});
add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1));
add_metadata(&mut request, macaroon);
match client.pay_invoice(request).await {
Ok(_) => println!("Successfully paid for offer!"),
Err(err) => print_grpc_error(err),
@ -351,28 +299,14 @@ async fn main() {
quantity,
} => {
let tls = read_cert_from_args_or_exit(args.cert_pem, args.cert_path);
let grpc_host = args.grpc_host.clone();
let grpc_port = args.grpc_port;
let channel = Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| {
println!("ERROR: failed to create endpoint {e:?}");
exit(1)
})
.tls_config(tls)
.unwrap_or_else(|e| {
println!("ERROR: failed to configure tls {e:?}");
exit(1)
})
.connect()
.await
.unwrap_or_else(|e| {
println!("ERROR: failed to connect {e:?}");
exit(1)
});
let channel = create_grpc_channel(args.grpc_host, args.grpc_port, tls).await;
let (mut client, macaroon) = create_authenticated_client(
channel,
args.macaroon_path,
args.macaroon_hex,
&args.network,
);
let mut client = OffersClient::new(channel);
let macaroon =
read_macaroon_from_args(args.macaroon_path, args.macaroon_hex, &args.network);
let mut request = Request::new(CreateOfferRequest {
amount,
quantity,
@ -380,7 +314,7 @@ async fn main() {
issuer,
expiry,
});
add_metadata(&mut request, macaroon).unwrap_or_else(|_| exit(1));
add_metadata(&mut request, macaroon);
match client.create_offer(request).await {
Ok(response) => println!("Offer: {:?}.", response.get_ref()),
Err(err) => print_grpc_error(err),
@ -389,13 +323,34 @@ async fn main() {
}
}
fn add_metadata<R>(request: &mut Request<R>, macaroon: String) -> Result<(), ()> {
let macaroon = macaroon.parse().map_err(|e| {
println!("ERROR: failed to parse provided macaroon string into tonic metadata {e:?}")
})?;
request.metadata_mut().insert("macaroon", macaroon);
async fn create_grpc_channel(grpc_host: String, grpc_port: u16, tls: ClientTlsConfig) -> Channel {
Channel::from_shared(format!("{grpc_host}:{grpc_port}"))
.unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to create endpoint {e:?}")))
.tls_config(tls)
.unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to configure tls {e:?}")))
.connect()
.await
.unwrap_or_else(|e| print_and_exit(&format!("ERROR: failed to connect {e:?}")))
}
Ok(())
fn create_authenticated_client(
channel: Channel,
macaroon_path: Option<PathBuf>,
macaroon_hex: Option<String>,
network: &str,
) -> (OffersClient<Channel>, String) {
let client = OffersClient::new(channel);
let macaroon = read_macaroon_from_args(macaroon_path, macaroon_hex, network);
(client, macaroon)
}
fn add_metadata<R>(request: &mut Request<R>, macaroon: String) {
let macaroon = macaroon.parse().unwrap_or_else(|e| {
print_and_exit(&format!(
"ERROR: failed to parse provided macaroon string into tonic metadata {e:?}"
))
});
request.metadata_mut().insert("macaroon", macaroon);
}
fn read_macaroon_from_file(path: PathBuf) -> Result<String, std::io::Error> {
@ -443,10 +398,7 @@ fn read_cert_from_args_or_exit(
) -> ClientTlsConfig {
match read_cert_from_args(cert_pem, cert_path) {
Ok(config) => config,
Err(err) => {
println!("{}", err);
exit(1);
}
Err(err) => print_and_exit(&format!("ERROR: {}", err)),
}
}
@ -457,38 +409,34 @@ fn read_macaroon_from_args(
) -> String {
// Make sure both macaroon options are not set.
if macaroon_path.is_some() && macaroon_hex.is_some() {
println!("ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set.");
exit(1)
print_and_exit("ERROR: Only one of `macaroon_path` or `macaroon_hex` should be set.");
}
// Let's grab the macaroon string now. If neither macaroon_path nor macaroon_hex are
// set, use the default macaroon path.
match macaroon_path {
Some(path) => read_macaroon_from_file(path.clone()).unwrap_or_else(|e| {
println!("ERROR: failed to read macaroon from file {e:?}");
exit(1)
print_and_exit(&format!("ERROR: failed to read macaroon from file {e:?}"))
}),
None => match &macaroon_hex {
Some(macaroon) => macaroon.clone(),
None => {
let path = get_macaroon_path_default(network);
read_macaroon_from_file(path).unwrap_or_else(|e| {
println!("ERROR: failed to read macaroon from file {e:?}");
exit(1)
print_and_exit(&format!("ERROR: failed to read macaroon from file {e:?}"))
})
}
},
}
}
fn print_grpc_error(err: tonic::Status) {
fn print_grpc_error(err: tonic::Status) -> ! {
let details = err.get_error_details();
if let Some(error_info) = details.error_info() {
println!("ERROR ({}): {}", error_info.reason, err.message());
print_and_exit(&format!("ERROR ({}): {}", error_info.reason, err.message()));
} else {
println!("ERROR: {}", err.message());
print_and_exit(&format!("ERROR: {}", err.message()));
}
exit(1)
}
#[cfg(test)]