use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::process::Command;
use reqwest::Client;
use thiserror::Error;
use crate::types::{
AddInvoiceRequest, AddInvoiceResponse, LookupInvoiceResponse, SendPaymentSyncRequest,
SendPaymentSyncResponse,
};
pub type Result<T> = std::result::Result<T, NodeError>;
#[derive(Error, Debug)]
pub enum NodeError {
#[error("I/O error")]
IoError(#[from] std::io::Error),
#[error("Request error")]
RequestError(#[from] reqwest::Error),
#[error("Request header error")]
RequestHeaderError(#[from] reqwest::header::InvalidHeaderValue),
}
#[derive(Clone, Debug)]
pub struct Node {
pub host: String,
pub client: Client,
}
impl Node {
pub async fn init<P: AsRef<Path>>(
host: String,
macaroon_path: P,
cert_path: P,
) -> Result<Self> {
let mut cert_file = File::open(cert_path)?;
let mut cert_raw = Vec::new();
cert_file.read_to_end(&mut cert_raw)?;
let cert = reqwest::Certificate::from_pem(&cert_raw)?;
let cmd_output = Command::new("xxd")
.args(["-ps", "-u", "-c", "1000"])
.arg(macaroon_path.as_ref())
.output()?;
let mut macaroon = cmd_output.stdout;
macaroon.retain(|&z| {
((z >= b'0' as _) && (z <= b'9' as _)) | ((z >= b'A' as _) && (z <= b'F' as _))
});
let mut headers = reqwest::header::HeaderMap::new();
let mut macaroon_value = reqwest::header::HeaderValue::from_bytes(&macaroon)?;
macaroon_value.set_sensitive(true);
headers.insert("Grpc-Metadata-macaroon", macaroon_value);
let client = reqwest::Client::builder()
.add_root_certificate(cert)
.default_headers(headers)
.build()?;
Ok(Node { host, client })
}
async fn on_response(response: reqwest::Response) -> Result<reqwest::Response> {
let status = response.status();
match status {
reqwest::StatusCode::OK => Ok(response),
_ => match response.error_for_status() {
Ok(res) => Ok(res),
Err(err) => Err(err.into()),
},
}
}
pub async fn add_invoice(&self, invoice: &AddInvoiceRequest) -> Result<AddInvoiceResponse> {
let url = format!("{host}/v1/invoices", host = self.host);
let mut response = self.client.post(&url).json(invoice).send().await?;
response = Self::on_response(response).await?;
let data: AddInvoiceResponse = response.json().await?;
Ok(data)
}
pub async fn lookup_invoice(&self, payment_hash: &String) -> Result<LookupInvoiceResponse> {
let url = format!(
"{host}/v1/invoice/{payment_hash}",
host = self.host,
payment_hash = payment_hash
);
let mut response = self.client.get(&url).send().await?;
response = Self::on_response(response).await?;
let data: LookupInvoiceResponse = match response.json().await {
Ok(data) => data,
Err(err) => {
return Err(err.into());
}
};
Ok(data)
}
pub async fn pay_invoice(
&self,
payment_request: &SendPaymentSyncRequest,
) -> Result<SendPaymentSyncResponse> {
let url = format!("{host}/v1/channels/transactions", host = self.host);
let mut response = self.client.post(&url).json(payment_request).send().await?;
response = Self::on_response(response).await?;
let data: SendPaymentSyncResponse = response.json().await?;
Ok(data)
}
}