Skip to content

Commit

Permalink
Merge pull request #127 from Kodylow:alby-client
Browse files Browse the repository at this point in the history
feat: add alby client for lightning
  • Loading branch information
ngutech21 authored Sep 18, 2023
2 parents 5561a9e + 1f6be6d commit a1f5d8d
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 43 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ MINT_LIGHTNING_BACKEND=Lnbits
LNBITS_URL=https://legend.lnbits.com
LNBITS_ADMIN_KEY=YOUR_ADMIN_KEY

ALBY_API_KEY=YOUR_API_KEY

# absolute path to the lnd macaroon file
LND_MACAROON_PATH="/.../admin.macaroon"
# absolute path to the tls certificate
Expand Down
144 changes: 144 additions & 0 deletions moksha-mint/src/alby.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use hyper::{header::CONTENT_TYPE, http::HeaderValue};
use serde::{Deserialize, Serialize};
use url::Url;

use crate::model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult};

#[derive(Debug, thiserror::Error)]
pub enum AlbyError {
#[error("reqwest error: {0}")]
ReqwestError(#[from] reqwest::Error),

#[error("url error: {0}")]
UrlError(#[from] url::ParseError),

#[error("serde error: {0}")]
SerdeError(#[from] serde_json::Error),

#[error("Not found")]
NotFound,

#[error("Unauthorized")]
Unauthorized,
}

#[derive(Clone)]
pub struct AlbyClient {
api_key: String,
alby_url: Url,
reqwest_client: reqwest::Client,
}

impl AlbyClient {
pub fn new(api_key: &str) -> Result<AlbyClient, AlbyError> {
let alby_url = Url::parse("https://api.getalby.com")?;

let reqwest_client = reqwest::Client::builder().build()?;

Ok(AlbyClient {
api_key: api_key.to_owned(),
alby_url,
reqwest_client,
})
}
}

impl AlbyClient {
pub async fn make_get(&self, endpoint: &str) -> Result<String, AlbyError> {
let url = self.alby_url.join(endpoint)?;
let response = self
.reqwest_client
.get(url)
.bearer_auth(self.api_key.clone())
.send()
.await?;

if response.status() == reqwest::StatusCode::NOT_FOUND {
return Err(AlbyError::NotFound);
}

Ok(response.text().await?)
}

pub async fn make_post(&self, endpoint: &str, body: &str) -> Result<String, AlbyError> {
let url = self.alby_url.join(endpoint)?;
let response = self
.reqwest_client
.post(url)
.bearer_auth(self.api_key.clone())
.header(
CONTENT_TYPE,
HeaderValue::from_str("application/json").expect("Invalid header value"),
)
.body(body.to_string())
.send()
.await?;

if response.status() == reqwest::StatusCode::NOT_FOUND {
return Err(AlbyError::NotFound);
}

if response.status() == reqwest::StatusCode::UNAUTHORIZED {
return Err(AlbyError::Unauthorized);
}

Ok(response.text().await?)
}
}

impl AlbyClient {
pub async fn create_invoice(
&self,
params: &CreateInvoiceParams,
) -> Result<CreateInvoiceResult, AlbyError> {
let params = serde_json::json!({
"amount": params.amount,
"description": params.memo,
});

let body = self
.make_post("invoices", &serde_json::to_string(&params)?)
.await?;

let response: serde_json::Value = serde_json::from_str(&body)?;
let payment_request = response["payment_request"]
.as_str()
.expect("payment_request is empty")
.to_owned();
let payment_hash = response["payment_hash"]
.as_str()
.expect("payment_hash is empty")
.to_owned();

Ok(CreateInvoiceResult {
payment_hash: payment_hash.as_bytes().to_vec(),
payment_request,
})
}

pub async fn pay_invoice(&self, bolt11: &str) -> Result<PayInvoiceResult, AlbyError> {
let body = self
.make_post(
"payments/bolt11",
&serde_json::to_string(&serde_json::json!({"invoice": bolt11 }))?,
)
.await?;

let response: serde_json::Value = serde_json::from_str(&body)?;

Ok(PayInvoiceResult {
payment_hash: response["payment_hash"]
.as_str()
.expect("payment_hash is empty")
.to_owned(),
})
}

pub async fn is_invoice_paid(&self, payment_hash: &str) -> Result<bool, AlbyError> {
let body = self.make_get(&format!("invoices/{payment_hash}")).await?;

Ok(serde_json::from_str::<serde_json::Value>(&body)?["settled"]
.as_bool()
.unwrap_or(false))
}
}
10 changes: 9 additions & 1 deletion moksha-mint/src/bin/moksha-mint.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use mokshamint::{
info::MintInfoSettings,
lightning::{LightningType, LnbitsLightningSettings, LndLightningSettings},
lightning::{
AlbyLightningSettings, LightningType, LnbitsLightningSettings, LndLightningSettings,
},
MintBuilder,
};
use std::{env, fmt, net::SocketAddr, path::PathBuf};
Expand Down Expand Up @@ -44,6 +46,12 @@ pub async fn main() -> anyhow::Result<()> {
.expect("Please provide lnd info");
LightningType::Lnd(lnd_settings)
}
"Alby" => {
let alby_settings = envy::prefixed("ALBY_")
.from_env::<AlbyLightningSettings>()
.expect("Please provide alby info");
LightningType::Alby(alby_settings)
}
_ => panic!(
"env MINT_LIGHTNING_BACKEND not found or invalid values. Valid values are Lnbits and Lnd"
),
Expand Down
10 changes: 8 additions & 2 deletions moksha-mint/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use thiserror::Error;
use tonic_lnd::ConnectError;
use tracing::{event, Level};

use crate::lnbits::LNBitsError;
use crate::{alby::AlbyError, lnbits::LNBitsError};

#[derive(Error, Debug)]
pub enum MokshaMintError {
Expand All @@ -24,6 +24,9 @@ pub enum MokshaMintError {
#[error("Failed to pay invoice {0} - Error {1}")]
PayInvoice(String, LNBitsError),

#[error("Failed to pay invoice {0} - Error {1}")]
PayInvoiceAlby(String, AlbyError),

#[error("DB Error {0}")]
Db(#[from] rocksdb::Error),

Expand Down Expand Up @@ -58,7 +61,10 @@ pub enum MokshaMintError {
InvalidAmount,

#[error("Lightning Error {0}")]
Lightning(#[from] LNBitsError),
LightningLNBits(#[from] LNBitsError),

#[error("Lightning Error {0}")]
LightningAlby(#[from] AlbyError),
}

impl IntoResponse for MokshaMintError {
Expand Down
7 changes: 5 additions & 2 deletions moksha-mint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use error::MokshaMintError;
use hyper::http::{HeaderName, HeaderValue};
use hyper::Method;
use info::{MintInfoResponse, MintInfoSettings, Parameter};
use lightning::{Lightning, LightningType, LnbitsLightning};
use lightning::{AlbyLightning, Lightning, LightningType, LnbitsLightning};
use mint::{LightningFeeConfig, Mint};
use model::{GetMintQuery, PostMintQuery};
use moksha_core::model::{
Expand All @@ -33,6 +33,7 @@ use tracing::{event, info, Level};
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

mod alby;
mod database;
mod error;
pub mod info;
Expand Down Expand Up @@ -88,7 +89,9 @@ impl MintBuilder {
lnbits_settings.admin_key.expect("LNBITS_ADMIN_KEY not set"),
lnbits_settings.url.expect("LNBITS_URL not set"),
)),

Some(LightningType::Alby(alby_settings)) => Arc::new(AlbyLightning::new(
alby_settings.api_key.expect("ALBY_API_KEY not set"),
)),
Some(LightningType::Lnd(lnd_settings)) => Arc::new(
lightning::LndLightning::new(
lnd_settings.grpc_host.expect("LND_GRPC_HOST not set"),
Expand Down
Loading

0 comments on commit a1f5d8d

Please sign in to comment.