Skip to content

Commit

Permalink
Merge pull request #128 from Kodylow/strike-client
Browse files Browse the repository at this point in the history
feat: add strike client for lightning backend
  • Loading branch information
ngutech21 authored Sep 18, 2023
2 parents a1f5d8d + 80bfd17 commit 31c5118
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 14 deletions.
17 changes: 12 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,24 @@ MINT_INFO_CONTACT=[["email","[email protected]"]]
LIGHTNING_FEE_PERCENT=1.0
LIGHTNING_RESERVE_FEE_MIN=4000

# configure the lightning backend. Valid values are Lnbits and Lnd. If Lnbits is configured all variables starting with LNBITS_ are required.
# If Lnd is configured all variables starting with LND_ are required.
# configure the lightning backend.
# currently supported backends are:
# - Lnbits
# - Alby
# - Strike
# - Lnd
# you are required to set the corresponding environment variables for the backend you want to use
MINT_LIGHTNING_BACKEND=Lnbits
#MINT_LIGHTNING_BACKEND=Lnd


LNBITS_URL=https://legend.lnbits.com
LNBITS_ADMIN_KEY=YOUR_ADMIN_KEY

#MINT_LIGHTNING_BACKEND=Alby
ALBY_API_KEY=YOUR_API_KEY

#MINT_LIGHTNING_BACKEND=Strike
STRIKE_API_KEY=YOUR_API_KEY

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

use crate::model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult};
Expand Down
8 changes: 7 additions & 1 deletion 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::{alby::AlbyError, lnbits::LNBitsError};
use crate::{alby::AlbyError, lnbits::LNBitsError, strike::StrikeError};

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

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

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

Expand Down Expand Up @@ -65,6 +68,9 @@ pub enum MokshaMintError {

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

#[error("Lightning Error {0}")]
LightningStrike(#[from] StrikeError),
}

impl IntoResponse for MokshaMintError {
Expand Down
6 changes: 5 additions & 1 deletion 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::{AlbyLightning, Lightning, LightningType, LnbitsLightning};
use lightning::{AlbyLightning, Lightning, LightningType, LnbitsLightning, StrikeLightning};
use mint::{LightningFeeConfig, Mint};
use model::{GetMintQuery, PostMintQuery};
use moksha_core::model::{
Expand Down Expand Up @@ -41,6 +41,7 @@ pub mod lightning;
mod lnbits;
pub mod mint;
mod model;
mod strike;

#[derive(Debug, Default)]
pub struct MintBuilder {
Expand Down Expand Up @@ -92,6 +93,9 @@ impl MintBuilder {
Some(LightningType::Alby(alby_settings)) => Arc::new(AlbyLightning::new(
alby_settings.api_key.expect("ALBY_API_KEY not set"),
)),
Some(LightningType::Strike(strike_settings)) => Arc::new(StrikeLightning::new(
strike_settings.api_key.expect("STRIKE_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
126 changes: 121 additions & 5 deletions moksha-mint/src/lightning.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use async_trait::async_trait;
// use serde_derive::{Deserialize, Serialize};
use std::fmt::{self, Formatter};
use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard};
use tonic_lnd::Client;
Expand All @@ -12,9 +11,10 @@ use crate::{
error::MokshaMintError,
lnbits::LNBitsClient,
model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult},
strike::{StrikeClient, StrikeError},
};

use lightning_invoice::Bolt11Invoice as LNInvoice;
use lightning_invoice::{Bolt11Invoice as LNInvoice, SignedRawBolt11Invoice};

#[cfg(test)]
use mockall::automock;
Expand All @@ -24,6 +24,7 @@ use std::{path::PathBuf, str::FromStr, sync::Arc};
pub enum LightningType {
Lnbits(LnbitsLightningSettings),
Alby(AlbyLightningSettings),
Strike(StrikeLightningSettings),
Lnd(LndLightningSettings),
}

Expand All @@ -32,13 +33,13 @@ impl fmt::Display for LightningType {
match self {
LightningType::Lnbits(settings) => write!(f, "Lnbits: {}", settings),
LightningType::Alby(settings) => write!(f, "Alby: {}", settings),
LightningType::Strike(settings) => write!(f, "Strike: {}", settings),
LightningType::Lnd(settings) => write!(f, "Lnd: {}", settings),
}
}
}

#[cfg_attr(test, automock)]
#[allow(implied_bounds_entailment)]
#[async_trait]
pub trait Lightning: Send + Sync {
async fn is_invoice_paid(&self, invoice: String) -> Result<bool, MokshaMintError>;
Expand Down Expand Up @@ -94,7 +95,6 @@ impl LnbitsLightningSettings {
}
}

#[allow(implied_bounds_entailment)]
#[async_trait]
impl Lightning for LnbitsLightning {
async fn is_invoice_paid(&self, invoice: String) -> Result<bool, MokshaMintError> {
Expand Down Expand Up @@ -162,7 +162,6 @@ impl AlbyLightning {
}
}

#[allow(implied_bounds_entailment)]
#[async_trait]
impl Lightning for AlbyLightning {
async fn is_invoice_paid(&self, invoice: String) -> Result<bool, MokshaMintError> {
Expand Down Expand Up @@ -198,6 +197,123 @@ impl Lightning for AlbyLightning {
}
}

#[derive(Clone)]
pub struct StrikeLightning {
pub client: StrikeClient,
}

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct StrikeLightningSettings {
pub api_key: Option<String>,
}

impl fmt::Display for StrikeLightningSettings {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "api_key: {}", self.api_key.as_ref().unwrap(),)
}
}

impl StrikeLightningSettings {
pub fn new(api_key: &str) -> Self {
Self {
api_key: Some(api_key.to_owned()),
}
}
}

impl StrikeLightning {
pub fn new(api_key: String) -> Self {
Self {
client: StrikeClient::new(&api_key).expect("Can not create Strike client"),
}
}
}

#[async_trait]
impl Lightning for StrikeLightning {
async fn is_invoice_paid(&self, invoice: String) -> Result<bool, MokshaMintError> {
let decoded_invoice = self.decode_invoice(invoice).await?;
let description_hash = decoded_invoice
.into_signed_raw()
.description_hash()
.unwrap()
.0;

// invoiceId is the first 32 bytes of the description hash
let invoice_id = format_as_uuid_string(&description_hash[..16]);
Ok(self.client.is_invoice_paid(&invoice_id).await?)
}

async fn create_invoice(&self, amount: u64) -> Result<CreateInvoiceResult, MokshaMintError> {
let strike_invoice_id = self
.client
.create_strike_invoice(&CreateInvoiceParams {
amount,
unit: "sat".to_string(),
memo: None,
expiry: Some(10000),
webhook: None,
internal: None,
})
.await?;

let payment_request = self.client.create_strike_quote(&strike_invoice_id).await?;

// strike doesn't return the payment_hash so we have to read the invoice into a Bolt11 and extract it
let invoice =
LNInvoice::from_signed(payment_request.parse::<SignedRawBolt11Invoice>().unwrap())
.unwrap();
let payment_hash = invoice.payment_hash().to_vec();

Ok(CreateInvoiceResult {
payment_hash,
payment_request,
})
}

async fn pay_invoice(
&self,
payment_request: String,
) -> Result<PayInvoiceResult, MokshaMintError> {
// strike doesn't return the payment_hash so we have to read the invoice into a Bolt11 and extract it
let invoice = self.decode_invoice(payment_request.clone()).await?;
let payment_hash = invoice.payment_hash().to_vec();

let payment_quote_id = self
.client
.create_ln_payment_quote(&invoice.into_signed_raw().to_string())
.await?;

let payment_result = self
.client
.execute_ln_payment_quote(&payment_quote_id)
.await?;

if !payment_result {
return Err(MokshaMintError::PayInvoiceStrike(
payment_request,
StrikeError::PaymentFailed,
));
}

Ok(PayInvoiceResult {
payment_hash: hex::encode(payment_hash),
})
}
}

fn format_as_uuid_string(bytes: &[u8]) -> String {
let byte_str = hex::encode(bytes);
format!(
"{}-{}-{}-{}-{}",
&byte_str[..8],
&byte_str[8..12],
&byte_str[12..16],
&byte_str[16..20],
&byte_str[20..]
)
}

fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
where
D: Deserializer<'de>,
Expand Down
1 change: 0 additions & 1 deletion moksha-mint/src/lnbits.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use hyper::{header::CONTENT_TYPE, http::HeaderValue};
use serde::{Deserialize, Serialize};
use url::Url;

use crate::model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult};
Expand Down
Loading

0 comments on commit 31c5118

Please sign in to comment.