#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![warn(rustdoc::bare_urls)]
use std::fmt;
use std::str::FromStr;
use anyhow::{anyhow, bail};
use rand::distributions::Alphanumeric;
use rand::Rng;
use reqwest::{Client, IntoUrl, Url};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
mod error;
pub(crate) mod hex;
pub mod invoice;
pub mod pay_ln;
pub mod webhooks;
pub use error::Error;
pub use invoice::*;
pub use pay_ln::*;
#[derive(Debug, Clone)]
pub struct Strike {
api_key: String,
base_url: Url,
client: Client,
webhook_secret: String,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Currency {
USD,
EUR,
BTC,
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::USD => write!(f, "USD"),
Self::EUR => write!(f, "EUR"),
Self::BTC => write!(f, "BTC"),
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Amount {
pub currency: Currency,
#[serde(deserialize_with = "parse_f64_from_string")]
pub amount: f64,
}
fn parse_f64_from_string<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
s.parse::<f64>().map_err(serde::de::Error::custom)
}
impl Amount {
pub fn from_sats(amount: u64) -> Self {
Self {
currency: Currency::BTC,
amount: amount as f64 / 100_000_000.0,
}
}
pub fn to_sats(&self) -> anyhow::Result<u64> {
match self.currency {
Currency::BTC => Ok((self.amount * 100_000_000.0) as u64),
_ => bail!("Unit cannot be converted to sats"),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum InvoiceState {
Completed,
Paid,
Unpaid,
Pending,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ConversionRate {
#[serde(deserialize_with = "parse_f64_from_string")]
pub amount: f64,
#[serde(rename = "sourceCurrency")]
pub source_currency: Currency,
#[serde(rename = "targetCurrency")]
pub target_currency: Currency,
}
impl Strike {
pub fn new(api_key: &str, api_url: Option<String>) -> anyhow::Result<Self> {
let base_url = match api_url {
Some(url) => Url::from_str(&url)?,
None => Url::from_str("https://api.strike.me")?,
};
let client = reqwest::Client::builder().build()?;
let secret: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(15)
.map(char::from)
.collect();
Ok(Self {
api_key: api_key.to_string(),
base_url,
client,
webhook_secret: secret,
})
}
async fn make_get<U>(&self, url: U) -> Result<Value, Error>
where
U: IntoUrl,
{
let res = self
.client
.get(url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.header("accept", "application/json")
.send()
.await;
match res {
Ok(res) => Ok(res.json::<Value>().await.unwrap_or_default()),
Err(err) => Err(err.into()),
}
}
async fn make_post<U, T>(&self, url: U, data: Option<T>) -> anyhow::Result<Value>
where
U: IntoUrl,
T: Serialize,
{
let value = match data {
Some(data) => {
self.client
.post(url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.header("accept", "application/json")
.json(&data)
.send()
.await?
.json::<Value>()
.await?
}
None => {
self.client
.post(url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Length", "0")
.header("accept", "application/json")
.send()
.await?
.json::<Value>()
.await?
}
};
Ok(value)
}
async fn make_patch<U>(&self, url: U) -> anyhow::Result<Value>
where
U: IntoUrl,
{
Ok(self
.client
.patch(url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Length", "0")
.header("accept", "application/json")
.send()
.await?
.json::<Value>()
.await?)
}
async fn make_delete<U>(&self, url: U) -> anyhow::Result<()>
where
U: IntoUrl,
{
self.client
.delete(url)
.header("Authorization", format!("Bearer {}", self.api_key))
.send()
.await
.map_err(|err| anyhow!("Error making delete: {}", err.to_string()))?;
Ok(())
}
}