use std::collections::HashMap;
use regex::Regex;
use reqwest::{header, Client, Response};
use serde::Serialize;
use super::cryptography::{Cryptography, KeyPair};
use crate::models::*;
#[derive(Debug, Clone)]
pub struct BTCPayClient {
host: String,
client_id: String,
token: Option<String>,
keypair: KeyPair,
client: Client,
}
impl BTCPayClient {
pub fn new(host: &str, keypair: KeyPair, merchant: Option<&str>) -> Result<Self, Error> {
let token = merchant.map(String::from);
let mut headers = header::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
headers.insert(
header::ACCEPT,
header::HeaderValue::from_static("application/json"),
);
headers.insert(
"X-Accept-Version",
header::HeaderValue::from_static("2.0.0"),
);
let client = Client::builder()
.user_agent(concat!("rust-btcpay/", env!("CARGO_PKG_VERSION")))
.default_headers(headers)
.build()?;
Ok(BTCPayClient {
host: Regex::new(r"/+$").unwrap().replace(host, "").into(),
client_id: Cryptography::get_sin_from_key(&keypair),
token,
keypair,
client,
})
}
pub async fn pair_client(&self, code: &str) -> Result<PairClientResponse, Error> {
if !Regex::new(r"^\w{7}$").unwrap().is_match(code) {
return Err(Error::InvalidPairingCode(code.into()));
}
let req = PairClientRequest {
id: self.client_id.clone(),
pairing_code: code.into(),
};
let intermediate = self
.unsigned_request("/tokens", &req)
.await?
.json::<serde_json::Value>()
.await?;
let token = intermediate["data"]
.as_array()
.ok_or(Error::InvalidResponse)?[0]["token"]
.as_str()
.ok_or(Error::InvalidResponse)?
.to_string();
Ok(PairClientResponse { merchant: token })
}
pub async fn create_invoice(&self, args: CreateInvoiceArgs) -> Result<Invoice, Error> {
if !Regex::new(r"^[A-Z]{3}$").unwrap().is_match(&args.currency) {
return Err(Error::InvalidCurrency(args.currency.into()));
}
let mut intermediate = self
.signed_post_request("/invoices", &args)
.await?
.json::<serde_json::Value>()
.await?;
Ok(serde_json::from_value(intermediate["data"].take())?)
}
pub async fn get_invoice(&self, invoice_id: &str) -> Result<Invoice, Error> {
let mut intermediate = self
.signed_get_request(
&format!("/invoices/{}", invoice_id),
&HashMap::<String, String>::new(),
)
.await?
.json::<serde_json::Value>()
.await?;
Ok(serde_json::from_value(intermediate["data"].take())?)
}
pub async fn get_invoices(&self, args: GetInvoicesArgs) -> Result<Vec<Invoice>, Error> {
let mut intermediate = self
.signed_get_request("/invoices", &args)
.await?
.json::<serde_json::Value>()
.await?;
Ok(serde_json::from_value(intermediate["data"].take())?)
}
fn create_signed_headers(&self, uri: &str, payload: &str) -> header::HeaderMap {
let mut headers = header::HeaderMap::new();
headers.insert(
"X-Identity",
self.keypair.public.to_string().parse().unwrap(),
);
headers.insert(
"X-Signature",
Cryptography::sign(
(uri.to_string() + payload).as_bytes(),
self.keypair.secret(),
)
.unwrap()
.to_string()
.parse()
.unwrap(),
);
headers
}
async fn signed_get_request<T: Serialize>(
&self,
path: &str,
params: &T,
) -> Result<Response, Error> {
if self.token.is_none() {
return Err(Error::MerchantTokenRequired);
}
let mut serialized = serde_json::to_value(params)?;
serialized["token"] = self.token.clone().unwrap().into();
let query = serde_urlencoded::to_string(serialized.clone())?;
let full_path = self.host.clone() + path;
Ok(self
.client
.get(&full_path)
.query(&serialized)
.headers(self.create_signed_headers(&full_path, &format!("?{}", query)))
.send()
.await?)
}
async fn signed_post_request<T: Serialize>(
&self,
path: &str,
payload: &T,
) -> Result<Response, Error> {
if self.token.is_none() {
return Err(Error::MerchantTokenRequired);
}
let mut serialized = serde_json::to_value(payload)?;
serialized["token"] = self.token.clone().unwrap().into();
let body = serde_json::to_string(&serialized)?;
let full_path = self.host.clone() + path;
Ok(self
.client
.post(&full_path)
.headers(self.create_signed_headers(&full_path, &body))
.body(body)
.send()
.await?)
}
async fn unsigned_request<T: Serialize>(
&self,
path: &str,
payload: &T,
) -> Result<Response, Error> {
let full_path = self.host.clone() + path;
Ok(self.client.post(&full_path).json(payload).send().await?)
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PairClientRequest {
id: String,
pairing_code: String,
}
#[derive(Debug)]
pub enum Error {
InvalidPairingCode(String),
InvalidCurrency(String),
MerchantTokenRequired,
InvalidResponse,
Request(reqwest::Error),
JSON(serde_json::Error),
URLEncode(serde_urlencoded::ser::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for Error {}
impl From<reqwest::Error> for Error {
fn from(other: reqwest::Error) -> Error {
Error::Request(other)
}
}
impl From<serde_json::Error> for Error {
fn from(other: serde_json::Error) -> Error {
Error::JSON(other)
}
}
impl From<serde_urlencoded::ser::Error> for Error {
fn from(other: serde_urlencoded::ser::Error) -> Error {
Error::URLEncode(other)
}
}