use bitcoin::secp256k1::PublicKey;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::{error::MutinyError, utils};
#[derive(Clone, Debug)]
pub(crate) struct LspClient {
pub pubkey: PublicKey,
pub connection_string: String,
pub url: String,
pub http_client: Client,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct GetInfoResponse {
pub pubkey: PublicKey,
pub connection_methods: Vec<GetInfoAddress>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct GetInfoAddress {
#[serde(rename = "type")]
pub item_type: GetInfoAddressType,
pub port: u16,
pub address: String,
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum GetInfoAddressType {
Dns,
IPV4,
IPV6,
TORV2,
TORV3,
Websocket,
}
#[derive(Serialize, Deserialize)]
pub struct ProposalRequest {
pub bolt11: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
pub fee_id: String,
}
#[derive(Serialize, Deserialize)]
pub struct ProposalResponse {
pub jit_bolt11: String,
}
#[derive(Serialize, Deserialize)]
pub struct FeeRequest {
pub pubkey: String,
pub amount_msat: u64,
}
#[derive(Serialize, Deserialize)]
pub struct FeeResponse {
pub id: String,
pub fee_amount_msat: u64,
}
#[derive(Deserialize, Debug)]
struct ErrorResponse {
error: String,
message: String,
}
const GET_INFO_PATH: &str = "/api/v1/info";
const PROPOSAL_PATH: &str = "/api/v1/proposal";
const FEE_PATH: &str = "/api/v1/fee";
impl LspClient {
pub async fn new(url: &str) -> Result<Self, MutinyError> {
let http_client = Client::new();
let request = http_client
.get(format!("{}{}", url, GET_INFO_PATH))
.build()
.map_err(|_| MutinyError::LspGenericError)?;
let response: reqwest::Response = utils::fetch_with_timeout(&http_client, request).await?;
let get_info_response: GetInfoResponse = response
.json()
.await
.map_err(|_| MutinyError::LspGenericError)?;
let connection_string = get_info_response
.connection_methods
.iter()
.filter(|address| {
matches!(
address.item_type,
GetInfoAddressType::IPV4 | GetInfoAddressType::IPV6 | GetInfoAddressType::TORV3
)
})
.min_by_key(|address| match address.item_type {
GetInfoAddressType::IPV4 => 0,
GetInfoAddressType::IPV6 => 1,
GetInfoAddressType::TORV3 => 2,
_ => unreachable!(),
})
.map(|address| {
format!(
"{}@{}:{}",
get_info_response.pubkey, address.address, address.port
)
})
.ok_or_else(|| anyhow::anyhow!("No suitable connection method found"))?;
Ok(LspClient {
pubkey: get_info_response.pubkey,
url: String::from(url),
connection_string,
http_client,
})
}
pub(crate) async fn get_lsp_invoice(
&self,
bolt11: String,
fee_id: String,
) -> Result<String, MutinyError> {
let payload = ProposalRequest {
bolt11,
host: None,
port: None,
fee_id,
};
let request = self
.http_client
.post(format!("{}{}", &self.url, PROPOSAL_PATH))
.json(&payload)
.build()
.map_err(|_| MutinyError::LspGenericError)?;
let response: reqwest::Response =
utils::fetch_with_timeout(&self.http_client, request).await?;
let status = response.status().as_u16();
if (200..300).contains(&status) {
let proposal_response: ProposalResponse = response
.json()
.await
.map_err(|_| MutinyError::LspGenericError)?;
return Ok(proposal_response.jit_bolt11);
} else if response.status().as_u16() >= 400 {
let response_body = response
.text()
.await
.map_err(|_| MutinyError::LspGenericError)?;
if let Ok(error_body) = serde_json::from_str::<ErrorResponse>(&response_body) {
if error_body.error == "Internal Server Error" {
if error_body.message == "Cannot fund new channel at this time" {
return Err(MutinyError::LspFundingError);
} else if error_body.message.starts_with("Failed to connect to peer") {
return Err(MutinyError::LspConnectionError);
} else if error_body.message == "Invoice amount is too high" {
return Err(MutinyError::LspAmountTooHighError);
}
}
}
}
Err(MutinyError::LspGenericError)
}
pub(crate) async fn get_lsp_fee_msat(
&self,
fee_request: FeeRequest,
) -> Result<FeeResponse, MutinyError> {
let request = self
.http_client
.post(format!("{}{}", &self.url, FEE_PATH))
.json(&fee_request)
.build()
.map_err(|_| MutinyError::LspGenericError)?;
let response: reqwest::Response =
utils::fetch_with_timeout(&self.http_client, request).await?;
let fee_response: FeeResponse = response
.json()
.await
.map_err(|_| MutinyError::LspGenericError)?;
Ok(fee_response)
}
}