mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-09 07:06:12 +01:00
feat: strike api for mint backend
feat: Use mint melt settings
This commit is contained in:
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -35,6 +35,7 @@ jobs:
|
||||
-p cdk-axum,
|
||||
-p cdk-cln,
|
||||
-p cdk-fake-wallet,
|
||||
-p cdk-strike,
|
||||
--bin cdk-cli,
|
||||
--bin cdk-mintd,
|
||||
--examples
|
||||
|
||||
@@ -23,6 +23,7 @@ keywords = ["bitcoin", "e-cash", "cashu"]
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.74"
|
||||
anyhow = "1"
|
||||
axum = "0.7.5"
|
||||
bitcoin = { version = "0.30", default-features = false } # lightning-invoice uses v0.30
|
||||
bip39 = "2.0"
|
||||
cdk = { version = "0.2", path = "./crates/cdk", default-features = false }
|
||||
@@ -32,6 +33,7 @@ cdk-redb = { version = "0.2", path = "./crates/cdk-redb", default-features = fal
|
||||
cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false }
|
||||
cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = false }
|
||||
cdk-fake-wallet = { version = "0.1", path = "./crates/cdk-fake-wallet", default-features = false }
|
||||
cdk-strike = { version = "0.1", path = "./crates/cdk-strike", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
thiserror = "1"
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
|
||||
@@ -20,6 +20,7 @@ The project is split up into several crates in the `crates/` directory:
|
||||
* [**cdk-rexie**](./crates/cdk-rexie/): Rexie Storage backend for browsers
|
||||
* [**cdk-axum**](./crates/cdk-axum/): Axum webserver for mint.
|
||||
* [**cdk-cln**](./crates/cdk-cln/): CLN Lightning backend for mint.
|
||||
* [**cdk-strike**](./crates/cdk-strike/): Strike Lightning backend for mint.
|
||||
* [**cdk-fake-wallet**](./crates/cdk-fake-wallet/): Fake Lightning backend for mint. To be used only for testing, quotes are automatically filled.
|
||||
* Binaries:
|
||||
* [**cdk-cli**](./crates/cdk-cli/): Cashu wallet CLI.
|
||||
|
||||
@@ -11,7 +11,7 @@ description = "Cashu CDK axum webserver"
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
async-trait.workspace = true
|
||||
axum = "0.7.5"
|
||||
axum.workspace = true
|
||||
axum-macros = "0.4.1"
|
||||
cdk = { workspace = true, default-features = false, features = ["mint"] }
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -55,7 +55,7 @@ pub async fn get_mint_bolt11_quote(
|
||||
let ln = state
|
||||
.ln
|
||||
.get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
|
||||
.ok_or({
|
||||
.ok_or_else(|| {
|
||||
tracing::info!("Bolt11 mint request for unsupported unit");
|
||||
|
||||
into_response(Error::UnsupportedUnit)
|
||||
@@ -135,7 +135,7 @@ pub async fn get_melt_bolt11_quote(
|
||||
let ln = state
|
||||
.ln
|
||||
.get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
|
||||
.ok_or({
|
||||
.ok_or_else(|| {
|
||||
tracing::info!("Could not get ln backend for {}, bolt11 ", payload.unit);
|
||||
|
||||
into_response(Error::UnsupportedUnit)
|
||||
@@ -339,17 +339,17 @@ pub async fn post_melt_bolt11(
|
||||
}
|
||||
}
|
||||
|
||||
let ln = state
|
||||
.ln
|
||||
.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11))
|
||||
.ok_or({
|
||||
let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
|
||||
Some(ln) => ln,
|
||||
None => {
|
||||
tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
|
||||
into_response(Error::UnsupportedUnit)
|
||||
})?;
|
||||
return Err(into_response(Error::UnsupportedUnit));
|
||||
}
|
||||
};
|
||||
|
||||
let pre = match ln
|
||||
.pay_invoice(quote.clone(), partial_msats, max_fee_msats)
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cdk::cdk_lightning::{
|
||||
self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
|
||||
Settings,
|
||||
self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
|
||||
PaymentQuoteResponse, Settings,
|
||||
};
|
||||
use cdk::mint::FeeReserve;
|
||||
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
|
||||
@@ -35,22 +35,16 @@ pub struct Cln {
|
||||
rpc_socket: PathBuf,
|
||||
cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
|
||||
fee_reserve: FeeReserve,
|
||||
min_melt_amount: u64,
|
||||
max_melt_amount: u64,
|
||||
min_mint_amount: u64,
|
||||
max_mint_amount: u64,
|
||||
mint_enabled: bool,
|
||||
melt_enabled: bool,
|
||||
mint_settings: MintMeltSettings,
|
||||
melt_settings: MintMeltSettings,
|
||||
}
|
||||
|
||||
impl Cln {
|
||||
pub async fn new(
|
||||
rpc_socket: PathBuf,
|
||||
fee_reserve: FeeReserve,
|
||||
min_melt_amount: u64,
|
||||
max_melt_amount: u64,
|
||||
min_mint_amount: u64,
|
||||
max_mint_amount: u64,
|
||||
mint_settings: MintMeltSettings,
|
||||
melt_settings: MintMeltSettings,
|
||||
) -> Result<Self, Error> {
|
||||
let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
|
||||
|
||||
@@ -58,12 +52,8 @@ impl Cln {
|
||||
rpc_socket,
|
||||
cln_client: Arc::new(Mutex::new(cln_client)),
|
||||
fee_reserve,
|
||||
min_mint_amount,
|
||||
max_mint_amount,
|
||||
min_melt_amount,
|
||||
max_melt_amount,
|
||||
mint_enabled: true,
|
||||
melt_enabled: true,
|
||||
mint_settings,
|
||||
melt_settings,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -75,13 +65,9 @@ impl MintLightning for Cln {
|
||||
fn get_settings(&self) -> Settings {
|
||||
Settings {
|
||||
mpp: true,
|
||||
min_mint_amount: self.min_mint_amount,
|
||||
max_mint_amount: self.max_mint_amount,
|
||||
min_melt_amount: self.min_melt_amount,
|
||||
max_melt_amount: self.max_melt_amount,
|
||||
unit: CurrencyUnit::Msat,
|
||||
mint_enabled: self.mint_enabled,
|
||||
melt_enabled: self.melt_enabled,
|
||||
mint_settings: self.mint_settings,
|
||||
melt_settings: self.melt_settings,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use async_trait::async_trait;
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::secp256k1::{Secp256k1, SecretKey};
|
||||
use cdk::cdk_lightning::{
|
||||
self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
|
||||
Settings,
|
||||
self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
|
||||
PaymentQuoteResponse, Settings,
|
||||
};
|
||||
use cdk::mint;
|
||||
use cdk::mint::FeeReserve;
|
||||
@@ -29,36 +29,26 @@ pub mod error;
|
||||
#[derive(Clone)]
|
||||
pub struct FakeWallet {
|
||||
fee_reserve: FeeReserve,
|
||||
min_melt_amount: u64,
|
||||
max_melt_amount: u64,
|
||||
min_mint_amount: u64,
|
||||
max_mint_amount: u64,
|
||||
mint_enabled: bool,
|
||||
melt_enabled: bool,
|
||||
sender: tokio::sync::mpsc::Sender<String>,
|
||||
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
|
||||
mint_settings: MintMeltSettings,
|
||||
melt_settings: MintMeltSettings,
|
||||
}
|
||||
|
||||
impl FakeWallet {
|
||||
pub fn new(
|
||||
fee_reserve: FeeReserve,
|
||||
min_melt_amount: u64,
|
||||
max_melt_amount: u64,
|
||||
min_mint_amount: u64,
|
||||
max_mint_amount: u64,
|
||||
mint_settings: MintMeltSettings,
|
||||
melt_settings: MintMeltSettings,
|
||||
) -> Self {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(8);
|
||||
|
||||
Self {
|
||||
fee_reserve,
|
||||
min_mint_amount,
|
||||
max_mint_amount,
|
||||
min_melt_amount,
|
||||
max_melt_amount,
|
||||
mint_enabled: true,
|
||||
melt_enabled: true,
|
||||
sender,
|
||||
receiver: Arc::new(Mutex::new(Some(receiver))),
|
||||
mint_settings,
|
||||
melt_settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,13 +60,9 @@ impl MintLightning for FakeWallet {
|
||||
fn get_settings(&self) -> Settings {
|
||||
Settings {
|
||||
mpp: true,
|
||||
min_mint_amount: self.min_mint_amount,
|
||||
max_mint_amount: self.max_mint_amount,
|
||||
min_melt_amount: self.min_melt_amount,
|
||||
max_melt_amount: self.max_melt_amount,
|
||||
unit: CurrencyUnit::Msat,
|
||||
mint_enabled: self.mint_enabled,
|
||||
melt_enabled: self.melt_enabled,
|
||||
melt_settings: self.melt_settings,
|
||||
mint_settings: self.mint_settings,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ cdk-redb = { workspace = true, default-features = false, features = ["mint"] }
|
||||
cdk-sqlite = { workspace = true, default-features = false, features = ["mint"] }
|
||||
cdk-cln = { workspace = true, default-features = false }
|
||||
cdk-fake-wallet = { workspace = true, default-features = false }
|
||||
cdk-strike.workspace = true
|
||||
cdk-axum = { workspace = true, default-features = false }
|
||||
config = { version = "0.13.3", features = ["toml"] }
|
||||
clap = { version = "4.4.8", features = ["derive", "env", "default"] }
|
||||
|
||||
@@ -25,9 +25,13 @@ mnemonic = ""
|
||||
|
||||
[ln]
|
||||
|
||||
# Required ln backend `cln`
|
||||
# Required ln backend `cln`, `strike`, `fakewallet`
|
||||
ln_backend = "cln"
|
||||
|
||||
# CLN
|
||||
# Required if using cln backend path to rpc
|
||||
cln_path = ""
|
||||
# cln_path = ""
|
||||
|
||||
# Strike
|
||||
# Required if using strike backed
|
||||
# strike_api_key=""
|
||||
|
||||
@@ -20,14 +20,17 @@ pub struct Info {
|
||||
pub enum LnBackend {
|
||||
#[default]
|
||||
Cln,
|
||||
FakeWallet, // Greenlight,
|
||||
// Ldk,
|
||||
Strike,
|
||||
FakeWallet,
|
||||
// Greenlight,
|
||||
// Ldk,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Ln {
|
||||
pub ln_backend: LnBackend,
|
||||
pub cln_path: Option<PathBuf>,
|
||||
pub strike_api_key: Option<String>,
|
||||
pub greenlight_invite_code: Option<String>,
|
||||
pub invoice_description: Option<String>,
|
||||
pub fee_percent: f32,
|
||||
@@ -115,6 +118,7 @@ impl Settings {
|
||||
//LnBackend::Greenlight => (),
|
||||
//LnBackend::Ldk => (),
|
||||
LnBackend::FakeWallet => (),
|
||||
LnBackend::Strike => assert!(settings.ln.strike_api_key.is_some()),
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
|
||||
@@ -12,22 +12,24 @@ use anyhow::{anyhow, Result};
|
||||
use axum::Router;
|
||||
use bip39::Mnemonic;
|
||||
use cdk::cdk_database::{self, MintDatabase};
|
||||
use cdk::cdk_lightning::MintLightning;
|
||||
use cdk::cdk_lightning;
|
||||
use cdk::cdk_lightning::{MintLightning, MintMeltSettings};
|
||||
use cdk::mint::{FeeReserve, Mint};
|
||||
use cdk::nuts::{
|
||||
nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings,
|
||||
MintVersion, MppMethodSettings, Nuts, PaymentMethod,
|
||||
};
|
||||
use cdk::{cdk_lightning, Amount};
|
||||
use cdk_axum::LnKey;
|
||||
use cdk_cln::Cln;
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use cdk_redb::MintRedbDatabase;
|
||||
use cdk_sqlite::MintSqliteDatabase;
|
||||
use cdk_strike::Strike;
|
||||
use clap::Parser;
|
||||
use cli::CLIArgs;
|
||||
use config::{DatabaseEngine, LnBackend};
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -117,10 +119,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
min_fee_reserve: absolute_ln_fee_reserve,
|
||||
percent_fee_reserve: relative_ln_fee,
|
||||
};
|
||||
let ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync> = match settings
|
||||
.ln
|
||||
.ln_backend
|
||||
{
|
||||
let (ln, ln_router): (
|
||||
Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
|
||||
Option<Router>,
|
||||
) = match settings.ln.ln_backend {
|
||||
LnBackend::Cln => {
|
||||
let cln_socket = expand_path(
|
||||
settings
|
||||
@@ -133,9 +135,56 @@ async fn main() -> anyhow::Result<()> {
|
||||
)
|
||||
.ok_or(anyhow!("cln socket not defined"))?;
|
||||
|
||||
Arc::new(Cln::new(cln_socket, fee_reserve, 1000, 1000000, 1000, 100000).await?)
|
||||
(
|
||||
Arc::new(
|
||||
Cln::new(
|
||||
cln_socket,
|
||||
fee_reserve,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
None,
|
||||
)
|
||||
}
|
||||
LnBackend::FakeWallet => Arc::new(FakeWallet::new(fee_reserve, 1000, 1000000, 1000, 10000)),
|
||||
LnBackend::Strike => {
|
||||
let api_key = settings
|
||||
.ln
|
||||
.strike_api_key
|
||||
.expect("Checked when validaing config");
|
||||
|
||||
// Channel used for strike web hook
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(8);
|
||||
|
||||
let webhook_endpoint = "/webhook/invoice";
|
||||
|
||||
let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint);
|
||||
|
||||
let strike = Strike::new(
|
||||
api_key,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(Mutex::new(Some(receiver))),
|
||||
webhook_url,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let router = strike
|
||||
.create_invoice_webhook(webhook_endpoint, sender)
|
||||
.await?;
|
||||
|
||||
(Arc::new(strike), Some(router))
|
||||
}
|
||||
LnBackend::FakeWallet => (
|
||||
Arc::new(FakeWallet::new(
|
||||
fee_reserve,
|
||||
MintMeltSettings::default(),
|
||||
MintMeltSettings::default(),
|
||||
)),
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
let mut ln_backends = HashMap::new();
|
||||
@@ -167,15 +216,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
let n4 = MintMethodSettings {
|
||||
method: key.method.clone(),
|
||||
unit: key.unit,
|
||||
min_amount: Some(Amount::from(settings.min_mint_amount)),
|
||||
max_amount: Some(Amount::from(settings.max_mint_amount)),
|
||||
min_amount: Some(settings.mint_settings.min_amount),
|
||||
max_amount: Some(settings.mint_settings.max_amount),
|
||||
};
|
||||
|
||||
let n5 = MeltMethodSettings {
|
||||
method: key.method.clone(),
|
||||
unit: key.unit,
|
||||
min_amount: Some(Amount::from(settings.min_melt_amount)),
|
||||
max_amount: Some(Amount::from(settings.max_melt_amount)),
|
||||
min_amount: Some(settings.melt_settings.min_amount),
|
||||
max_amount: Some(settings.melt_settings.max_amount),
|
||||
};
|
||||
|
||||
nut_04.methods.push(n4);
|
||||
@@ -260,6 +309,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
.nest("/", v1_service)
|
||||
.layer(CorsLayer::permissive());
|
||||
|
||||
let mint_service = match ln_router {
|
||||
Some(ln_router) => mint_service.nest("/", ln_router),
|
||||
None => mint_service,
|
||||
};
|
||||
|
||||
// Spawn task to wait for invoces to be paid and update mint quotes
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -313,13 +367,20 @@ async fn check_pending_quotes(
|
||||
mint: Arc<Mint>,
|
||||
ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
|
||||
) -> Result<()> {
|
||||
let pending_quotes = mint.get_pending_mint_quotes().await?;
|
||||
let mut pending_quotes = mint.get_pending_mint_quotes().await?;
|
||||
tracing::trace!("There are {} pending mint quotes.", pending_quotes.len());
|
||||
let mut unpaid_quotes = mint.get_unpaid_mint_quotes().await?;
|
||||
tracing::trace!("There are {} unpaid mint quotes.", unpaid_quotes.len());
|
||||
|
||||
for quote in pending_quotes {
|
||||
unpaid_quotes.append(&mut pending_quotes);
|
||||
|
||||
for quote in unpaid_quotes {
|
||||
tracing::trace!("Checking status of mint quote: {}", quote.id);
|
||||
let lookup_id = quote.request_lookup_id;
|
||||
let state = ln.check_invoice_status(&lookup_id).await?;
|
||||
|
||||
if state != quote.state {
|
||||
tracing::trace!("Mintquote status changed: {}", quote.id);
|
||||
mint.localstore
|
||||
.update_mint_quote_state("e.id, state)
|
||||
.await?;
|
||||
|
||||
23
crates/cdk-strike/Cargo.toml
Normal file
23
crates/cdk-strike/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "cdk-strike"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["CDK Developers"]
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true # MSRV
|
||||
license.workspace = true
|
||||
description = "CDK ln backend for Strike api"
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
bitcoin.workspace = true
|
||||
cdk = { workspace = true, default-features = false, features = ["mint"] }
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
strike-rs = "0.1.0"
|
||||
23
crates/cdk-strike/src/error.rs
Normal file
23
crates/cdk-strike/src/error.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Error for Strike ln backend
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Strike Error
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Invoice amount not defined
|
||||
#[error("Unknown invoice amount")]
|
||||
UnknownInvoiceAmount,
|
||||
/// Unknown invoice
|
||||
#[error("Unknown invoice")]
|
||||
UnknownInvoice,
|
||||
/// Anyhow error
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<Error> for cdk::cdk_lightning::Error {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::Lightning(Box::new(e))
|
||||
}
|
||||
}
|
||||
279
crates/cdk-strike/src/lib.rs
Normal file
279
crates/cdk-strike/src/lib.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
//! CDK lightning backend for Strike
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![warn(rustdoc::bare_urls)]
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use cdk::cdk_lightning::{
|
||||
self, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
|
||||
PaymentQuoteResponse, Settings,
|
||||
};
|
||||
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
|
||||
use cdk::util::unix_time;
|
||||
use cdk::{mint, Bolt11Invoice};
|
||||
use error::Error;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::Stream;
|
||||
use strike_rs::{
|
||||
Amount as StrikeAmount, Currency as StrikeCurrencyUnit, InvoiceRequest, InvoiceState,
|
||||
PayInvoiceQuoteRequest, Strike as StrikeApi,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Strike
|
||||
#[derive(Clone)]
|
||||
pub struct Strike {
|
||||
strike_api: StrikeApi,
|
||||
mint_settings: MintMeltSettings,
|
||||
melt_settings: MintMeltSettings,
|
||||
unit: CurrencyUnit,
|
||||
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
|
||||
webhook_url: String,
|
||||
}
|
||||
|
||||
impl Strike {
|
||||
/// Create new [`Strike`] wallet
|
||||
pub async fn new(
|
||||
api_key: String,
|
||||
mint_settings: MintMeltSettings,
|
||||
melt_settings: MintMeltSettings,
|
||||
unit: CurrencyUnit,
|
||||
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
|
||||
webhook_url: String,
|
||||
) -> Result<Self, Error> {
|
||||
let strike = StrikeApi::new(&api_key, None)?;
|
||||
Ok(Self {
|
||||
strike_api: strike,
|
||||
mint_settings,
|
||||
melt_settings,
|
||||
receiver,
|
||||
unit,
|
||||
webhook_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MintLightning for Strike {
|
||||
type Err = cdk_lightning::Error;
|
||||
|
||||
fn get_settings(&self) -> Settings {
|
||||
Settings {
|
||||
mpp: false,
|
||||
unit: self.unit,
|
||||
mint_settings: self.mint_settings,
|
||||
melt_settings: self.melt_settings,
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_any_invoice(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
||||
self.strike_api
|
||||
.subscribe_to_invoice_webhook(self.webhook_url.clone())
|
||||
.await?;
|
||||
|
||||
let receiver = self
|
||||
.receiver
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.ok_or(anyhow!("No receiver"))?;
|
||||
|
||||
let strike_api = self.strike_api.clone();
|
||||
|
||||
Ok(futures::stream::unfold(
|
||||
(receiver, strike_api),
|
||||
|(mut receiver, strike_api)| async move {
|
||||
match receiver.recv().await {
|
||||
Some(msg) => {
|
||||
let check = strike_api.find_invoice(&msg).await;
|
||||
|
||||
match check {
|
||||
Ok(state) => {
|
||||
if state.state == InvoiceState::Paid {
|
||||
Some((msg, (receiver, strike_api)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed())
|
||||
}
|
||||
|
||||
async fn get_payment_quote(
|
||||
&self,
|
||||
melt_quote_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||
if melt_quote_request.unit != self.unit {
|
||||
return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
|
||||
}
|
||||
|
||||
let payment_quote_request = PayInvoiceQuoteRequest {
|
||||
ln_invoice: melt_quote_request.request.to_string(),
|
||||
source_currency: strike_rs::Currency::BTC,
|
||||
};
|
||||
let quote = self.strike_api.payment_quote(payment_quote_request).await?;
|
||||
|
||||
let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?;
|
||||
|
||||
Ok(PaymentQuoteResponse {
|
||||
request_lookup_id: quote.payment_quote_id,
|
||||
amount: from_strike_amount(quote.amount, &melt_quote_request.unit)?,
|
||||
fee,
|
||||
})
|
||||
}
|
||||
|
||||
async fn pay_invoice(
|
||||
&self,
|
||||
melt_quote: mint::MeltQuote,
|
||||
_partial_msats: Option<u64>,
|
||||
_max_fee_msats: Option<u64>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let pay_response = self
|
||||
.strike_api
|
||||
.pay_quote(&melt_quote.request_lookup_id)
|
||||
.await?;
|
||||
|
||||
let state = match pay_response.state {
|
||||
InvoiceState::Paid => MeltQuoteState::Paid,
|
||||
InvoiceState::Unpaid => MeltQuoteState::Unpaid,
|
||||
InvoiceState::Completed => MeltQuoteState::Paid,
|
||||
InvoiceState::Pending => MeltQuoteState::Pending,
|
||||
};
|
||||
|
||||
let total_spent_msats = from_strike_amount(pay_response.total_amount, &melt_quote.unit)?;
|
||||
|
||||
let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
|
||||
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_hash: bolt11.payment_hash().to_string(),
|
||||
payment_preimage: None,
|
||||
status: state,
|
||||
total_spent_msats,
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_invoice(
|
||||
&self,
|
||||
amount: u64,
|
||||
description: String,
|
||||
unix_expiry: u64,
|
||||
) -> Result<CreateInvoiceResponse, Self::Err> {
|
||||
let time_now = unix_time();
|
||||
assert!(unix_expiry > time_now);
|
||||
let request_lookup_id = Uuid::new_v4();
|
||||
|
||||
let invoice_request = InvoiceRequest {
|
||||
correlation_id: Some(request_lookup_id.to_string()),
|
||||
amount: to_strike_unit(amount, &self.unit),
|
||||
description: Some(description),
|
||||
};
|
||||
|
||||
let create_invoice_response = self.strike_api.create_invoice(invoice_request).await?;
|
||||
|
||||
let quote = self
|
||||
.strike_api
|
||||
.invoice_quote(&create_invoice_response.invoice_id)
|
||||
.await?;
|
||||
|
||||
Ok(CreateInvoiceResponse {
|
||||
request_lookup_id: create_invoice_response.invoice_id,
|
||||
request: quote.ln_invoice.parse()?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
&self,
|
||||
request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
let invoice = self.strike_api.find_invoice(request_lookup_id).await?;
|
||||
|
||||
let state = match invoice.state {
|
||||
InvoiceState::Paid => MintQuoteState::Paid,
|
||||
InvoiceState::Unpaid => MintQuoteState::Unpaid,
|
||||
InvoiceState::Completed => MintQuoteState::Paid,
|
||||
InvoiceState::Pending => MintQuoteState::Pending,
|
||||
};
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Strike {
|
||||
/// Create invoice webhook
|
||||
pub async fn create_invoice_webhook(
|
||||
&self,
|
||||
webhook_endpoint: &str,
|
||||
sender: tokio::sync::mpsc::Sender<String>,
|
||||
) -> anyhow::Result<Router> {
|
||||
self.strike_api
|
||||
.create_invoice_webhook_router(webhook_endpoint, sender)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_strike_amount(
|
||||
strike_amount: StrikeAmount,
|
||||
target_unit: &CurrencyUnit,
|
||||
) -> anyhow::Result<u64> {
|
||||
match target_unit {
|
||||
CurrencyUnit::Sat => strike_amount.to_sats(),
|
||||
CurrencyUnit::Msat => Ok(strike_amount.to_sats()? * 1000),
|
||||
CurrencyUnit::Usd => {
|
||||
if strike_amount.currency == StrikeCurrencyUnit::USD {
|
||||
Ok((strike_amount.amount * 100.0).round() as u64)
|
||||
} else {
|
||||
bail!("Could not convert ");
|
||||
}
|
||||
}
|
||||
CurrencyUnit::Eur => {
|
||||
if strike_amount.currency == StrikeCurrencyUnit::EUR {
|
||||
Ok((strike_amount.amount * 100.0).round() as u64)
|
||||
} else {
|
||||
bail!("Could not convert ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_strike_unit<T>(amount: T, current_unit: &CurrencyUnit) -> StrikeAmount
|
||||
where
|
||||
T: Into<u64>,
|
||||
{
|
||||
let amount = amount.into();
|
||||
match current_unit {
|
||||
CurrencyUnit::Sat => StrikeAmount::from_sats(amount),
|
||||
CurrencyUnit::Msat => StrikeAmount::from_sats(amount / 1000),
|
||||
CurrencyUnit::Usd => {
|
||||
let dollars = (amount as f64 / 100_f64) * 100.0;
|
||||
|
||||
StrikeAmount {
|
||||
currency: StrikeCurrencyUnit::USD,
|
||||
amount: dollars.round() / 100.0,
|
||||
}
|
||||
}
|
||||
CurrencyUnit::Eur => {
|
||||
let euro = (amount as f64 / 100_f64) * 100.0;
|
||||
|
||||
StrikeAmount {
|
||||
currency: StrikeCurrencyUnit::EUR,
|
||||
amount: euro.round() / 100.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::mint;
|
||||
use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
|
||||
use crate::{mint, Amount};
|
||||
|
||||
/// CDK Lightning Error
|
||||
#[derive(Debug, Error)]
|
||||
@@ -121,19 +121,32 @@ pub struct Settings {
|
||||
/// MPP supported
|
||||
pub mpp: bool,
|
||||
/// Min amount to mint
|
||||
pub min_mint_amount: u64,
|
||||
pub mint_settings: MintMeltSettings,
|
||||
/// Max amount to mint
|
||||
pub max_mint_amount: u64,
|
||||
/// Min amount to melt
|
||||
pub min_melt_amount: u64,
|
||||
/// Max amount to melt
|
||||
pub max_melt_amount: u64,
|
||||
pub melt_settings: MintMeltSettings,
|
||||
/// Base unit of backend
|
||||
pub unit: CurrencyUnit,
|
||||
/// Minting enabled
|
||||
pub mint_enabled: bool,
|
||||
/// Melting enabled
|
||||
pub melt_enabled: bool,
|
||||
}
|
||||
|
||||
/// Mint or melt settings
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintMeltSettings {
|
||||
/// Min Amount
|
||||
pub min_amount: Amount,
|
||||
/// Max Amount
|
||||
pub max_amount: Amount,
|
||||
/// Enabled
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for MintMeltSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_amount: Amount::from(1),
|
||||
max_amount: Amount::from(500000),
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MSAT_IN_SAT: u64 = 1000;
|
||||
|
||||
@@ -262,6 +262,17 @@ impl Mint {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get pending mint quotes
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_unpaid_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
|
||||
let mint_quotes = self.localstore.get_mint_quotes().await?;
|
||||
|
||||
Ok(mint_quotes
|
||||
.into_iter()
|
||||
.filter(|p| p.state == MintQuoteState::Unpaid)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Remove mint quote
|
||||
#[instrument(skip_all)]
|
||||
pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
|
||||
|
||||
@@ -35,6 +35,7 @@ buildargs=(
|
||||
"-p cdk-cln"
|
||||
"-p cdk-axum"
|
||||
"-p cdk-fake-wallet"
|
||||
"-p cdk-strike"
|
||||
"--bin cdk-cli"
|
||||
"--bin cdk-mintd"
|
||||
"--examples"
|
||||
|
||||
Reference in New Issue
Block a user