mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-06 21:56:13 +01:00
refactor: ln backends within mint
This commit is contained in:
@@ -3,34 +3,19 @@
|
||||
#![warn(missing_docs)]
|
||||
#![warn(rustdoc::bare_urls)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use cdk::cdk_lightning::{self, MintLightning};
|
||||
use cdk::mint::Mint;
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::types::LnKey;
|
||||
use router_handlers::*;
|
||||
|
||||
mod router_handlers;
|
||||
|
||||
/// Create mint [`Router`] with required endpoints for cashu mint
|
||||
pub async fn create_mint_router(
|
||||
mint_url: &str,
|
||||
mint: Arc<Mint>,
|
||||
ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
|
||||
quote_ttl: u64,
|
||||
) -> Result<Router> {
|
||||
let state = MintState {
|
||||
ln,
|
||||
mint,
|
||||
mint_url: MintUrl::from_str(mint_url)?,
|
||||
quote_ttl,
|
||||
};
|
||||
pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
|
||||
let state = MintState { mint };
|
||||
|
||||
let v1_router = Router::new()
|
||||
.route("/keys", get(get_keys))
|
||||
@@ -61,8 +46,5 @@ pub async fn create_mint_router(
|
||||
/// CDK Mint State
|
||||
#[derive(Clone)]
|
||||
pub struct MintState {
|
||||
ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
|
||||
mint: Arc<Mint>,
|
||||
mint_url: MintUrl,
|
||||
quote_ttl: u64,
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::Result;
|
||||
use axum::extract::{Json, Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use cdk::cdk_lightning::{to_unit, MintLightning, PayInvoiceResponse};
|
||||
use cdk::error::{Error, ErrorResponse};
|
||||
use cdk::mint::MeltQuote;
|
||||
use cdk::error::ErrorResponse;
|
||||
use cdk::nuts::nut05::MeltBolt11Response;
|
||||
use cdk::nuts::{
|
||||
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
|
||||
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteState,
|
||||
MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
|
||||
MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest,
|
||||
SwapResponse,
|
||||
CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
|
||||
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
|
||||
MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
|
||||
SwapRequest, SwapResponse,
|
||||
};
|
||||
use cdk::util::unix_time;
|
||||
|
||||
use crate::{LnKey, MintState};
|
||||
use crate::MintState;
|
||||
|
||||
pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysResponse>, Response> {
|
||||
let pubkeys = state.mint.pubkeys().await.map_err(|err| {
|
||||
@@ -51,52 +48,13 @@ pub async fn get_mint_bolt11_quote(
|
||||
State(state): State<MintState>,
|
||||
Json(payload): Json<MintQuoteBolt11Request>,
|
||||
) -> Result<Json<MintQuoteBolt11Response>, Response> {
|
||||
let ln = state
|
||||
.ln
|
||||
.get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
|
||||
.ok_or_else(|| {
|
||||
tracing::info!("Bolt11 mint request for unsupported unit");
|
||||
|
||||
into_response(Error::UnitUnsupported)
|
||||
})?;
|
||||
|
||||
let quote_expiry = unix_time() + state.quote_ttl;
|
||||
|
||||
if payload.description.is_some() && !ln.get_settings().invoice_description {
|
||||
tracing::error!("Backend does not support invoice description");
|
||||
return Err(into_response(Error::InvoiceDescriptionUnsupported));
|
||||
}
|
||||
|
||||
let create_invoice_response = ln
|
||||
.create_invoice(
|
||||
payload.amount,
|
||||
&payload.unit,
|
||||
payload.description.unwrap_or("".to_string()),
|
||||
quote_expiry,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not create invoice: {}", err);
|
||||
into_response(Error::InvalidPaymentRequest)
|
||||
})?;
|
||||
|
||||
let quote = state
|
||||
.mint
|
||||
.new_mint_quote(
|
||||
state.mint_url,
|
||||
create_invoice_response.request.to_string(),
|
||||
payload.unit,
|
||||
payload.amount,
|
||||
create_invoice_response.expiry.unwrap_or(0),
|
||||
create_invoice_response.request_lookup_id,
|
||||
)
|
||||
.get_mint_bolt11_quote(payload)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not create new mint quote: {}", err);
|
||||
into_response(err)
|
||||
})?;
|
||||
.map_err(into_response)?;
|
||||
|
||||
Ok(Json(quote.into()))
|
||||
Ok(Json(quote))
|
||||
}
|
||||
|
||||
pub async fn get_check_mint_bolt11_quote(
|
||||
@@ -135,42 +93,13 @@ pub async fn get_melt_bolt11_quote(
|
||||
State(state): State<MintState>,
|
||||
Json(payload): Json<MeltQuoteBolt11Request>,
|
||||
) -> Result<Json<MeltQuoteBolt11Response>, Response> {
|
||||
let ln = state
|
||||
.ln
|
||||
.get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
|
||||
.ok_or_else(|| {
|
||||
tracing::info!("Could not get ln backend for {}, bolt11 ", payload.unit);
|
||||
|
||||
into_response(Error::UnitUnsupported)
|
||||
})?;
|
||||
|
||||
let payment_quote = ln.get_payment_quote(&payload).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
"Could not get payment quote for mint quote, {} bolt11, {}",
|
||||
payload.unit,
|
||||
err
|
||||
);
|
||||
|
||||
into_response(Error::UnitUnsupported)
|
||||
})?;
|
||||
|
||||
let quote = state
|
||||
.mint
|
||||
.new_melt_quote(
|
||||
payload.request.to_string(),
|
||||
payload.unit,
|
||||
payment_quote.amount,
|
||||
payment_quote.fee,
|
||||
unix_time() + state.quote_ttl,
|
||||
payment_quote.request_lookup_id,
|
||||
)
|
||||
.get_melt_bolt11_quote(&payload)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not create melt quote: {}", err);
|
||||
into_response(err)
|
||||
})?;
|
||||
.map_err(into_response)?;
|
||||
|
||||
Ok(Json(quote.into()))
|
||||
Ok(Json(quote))
|
||||
}
|
||||
|
||||
pub async fn get_check_melt_bolt11_quote(
|
||||
@@ -193,206 +122,13 @@ pub async fn post_melt_bolt11(
|
||||
State(state): State<MintState>,
|
||||
Json(payload): Json<MeltBolt11Request>,
|
||||
) -> Result<Json<MeltBolt11Response>, Response> {
|
||||
use std::sync::Arc;
|
||||
async fn check_payment_state(
|
||||
ln: Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Send + Sync>,
|
||||
melt_quote: &MeltQuote,
|
||||
) -> Result<PayInvoiceResponse> {
|
||||
match ln
|
||||
.check_outgoing_payment(&melt_quote.request_lookup_id)
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok(response),
|
||||
Err(check_err) => {
|
||||
// If we cannot check the status of the payment we keep the proofs stuck as pending.
|
||||
tracing::error!(
|
||||
"Could not check the status of payment for {},. Proofs stuck as pending",
|
||||
melt_quote.id
|
||||
);
|
||||
tracing::error!("Checking payment error: {}", check_err);
|
||||
bail!("Could not check payment status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let quote = match state.mint.verify_melt_request(&payload).await {
|
||||
Ok(quote) => quote,
|
||||
Err(err) => {
|
||||
tracing::debug!("Error attempting to verify melt quote: {}", err);
|
||||
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!(
|
||||
"Could not reset melt quote {} state: {}",
|
||||
payload.quote,
|
||||
err
|
||||
);
|
||||
}
|
||||
return Err(into_response(err));
|
||||
}
|
||||
};
|
||||
|
||||
let settled_internally_amount =
|
||||
match state.mint.handle_internal_melt_mint("e, &payload).await {
|
||||
Ok(amount) => amount,
|
||||
Err(err) => {
|
||||
tracing::error!("Attempting to settle internally failed");
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!(
|
||||
"Could not reset melt quote {} state: {}",
|
||||
payload.quote,
|
||||
err
|
||||
);
|
||||
}
|
||||
return Err(into_response(err));
|
||||
}
|
||||
};
|
||||
|
||||
let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
|
||||
Some(amount_spent) => (None, amount_spent),
|
||||
None => {
|
||||
// If the quote unit is SAT or MSAT we can check that the expected fees are
|
||||
// provided. We also check if the quote is less then the invoice
|
||||
// amount in the case that it is a mmp However, if the quote is not
|
||||
// of a bitcoin unit we cannot do these checks as the mint
|
||||
// is unaware of a conversion rate. In this case it is assumed that the quote is
|
||||
// correct and the mint should pay the full invoice amount if inputs
|
||||
// > `then quote.amount` are included. This is checked in the
|
||||
// `verify_melt` method.
|
||||
let partial_amount = match quote.unit {
|
||||
CurrencyUnit::Sat | CurrencyUnit::Msat => {
|
||||
match state
|
||||
.mint
|
||||
.check_melt_expected_ln_fees("e, &payload)
|
||||
.await
|
||||
{
|
||||
Ok(amount) => amount,
|
||||
Err(err) => {
|
||||
tracing::error!("Fee is not expected: {}", err);
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return Err(into_response(Error::UnitUnsupported));
|
||||
}
|
||||
};
|
||||
|
||||
let pre = match ln
|
||||
.pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
|
||||
.await
|
||||
{
|
||||
Ok(pay)
|
||||
if pay.status == MeltQuoteState::Unknown
|
||||
|| pay.status == MeltQuoteState::Failed =>
|
||||
{
|
||||
let check_response = check_payment_state(Arc::clone(ln), "e)
|
||||
.await
|
||||
.map_err(|_| into_response(Error::Internal))?;
|
||||
|
||||
if check_response.status == MeltQuoteState::Paid {
|
||||
tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
|
||||
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
|
||||
check_response
|
||||
}
|
||||
Ok(pay) => pay,
|
||||
Err(err) => {
|
||||
// If the error is that the invoice was already paid we do not want to hold
|
||||
// hold the proofs as pending to we reset them and return an error.
|
||||
if matches!(err, cdk::cdk_lightning::Error::InvoiceAlreadyPaid) {
|
||||
tracing::debug!("Invoice already paid, resetting melt quote");
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::RequestAlreadyPaid));
|
||||
}
|
||||
|
||||
tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
|
||||
|
||||
let check_response = check_payment_state(Arc::clone(ln), "e)
|
||||
.await
|
||||
.map_err(|_| into_response(Error::Internal))?;
|
||||
// If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
|
||||
if check_response.status == MeltQuoteState::Paid {
|
||||
tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
|
||||
|
||||
return Err(into_response(Error::Internal));
|
||||
}
|
||||
check_response
|
||||
}
|
||||
};
|
||||
|
||||
match pre.status {
|
||||
MeltQuoteState::Paid => (),
|
||||
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
|
||||
tracing::info!("Lightning payment for quote {} failed.", payload.quote);
|
||||
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(into_response(Error::PaymentFailed));
|
||||
}
|
||||
MeltQuoteState::Pending => {
|
||||
tracing::warn!(
|
||||
"LN payment pending, proofs are stuck as pending for quote: {}",
|
||||
payload.quote
|
||||
);
|
||||
return Err(into_response(Error::PendingQuote));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from unit of backend to quote unit
|
||||
// Note: this should never fail since these conversions happen earlier and would fail there.
|
||||
// Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
|
||||
let amount_spent = to_unit(pre.total_spent, &pre.unit, "e.unit).unwrap_or_default();
|
||||
|
||||
let payment_lookup_id = pre.payment_lookup_id;
|
||||
|
||||
if payment_lookup_id != quote.request_lookup_id {
|
||||
tracing::info!(
|
||||
"Payment lookup id changed post payment from {} to {}",
|
||||
quote.request_lookup_id,
|
||||
payment_lookup_id
|
||||
);
|
||||
|
||||
let mut melt_quote = quote;
|
||||
melt_quote.request_lookup_id = payment_lookup_id;
|
||||
|
||||
if let Err(err) = state.mint.localstore.add_melt_quote(melt_quote).await {
|
||||
tracing::warn!("Could not update payment lookup id: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
(pre.payment_preimage, amount_spent)
|
||||
}
|
||||
};
|
||||
|
||||
// If we made it here the payment has been made.
|
||||
// We process the melt burning the inputs and returning change
|
||||
let res = state
|
||||
.mint
|
||||
.process_melt_request(&payload, preimage, amount_spent_quote_unit)
|
||||
.melt_bolt11(&payload)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not process melt request: {}", err);
|
||||
into_response(err)
|
||||
})?;
|
||||
.map_err(into_response)?;
|
||||
|
||||
Ok(Json(res.into()))
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
pub async fn post_check(
|
||||
|
||||
@@ -13,11 +13,11 @@ use cdk::{
|
||||
types::LnKey,
|
||||
};
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::Notify;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::{handle_paid_invoice, init_regtest::create_mint};
|
||||
use crate::init_regtest::create_mint;
|
||||
|
||||
pub async fn start_fake_mint<D>(addr: &str, port: u16, database: D) -> Result<()>
|
||||
where
|
||||
@@ -36,7 +36,10 @@ where
|
||||
// Parse input
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let mint = create_mint(database).await?;
|
||||
let mut ln_backends: HashMap<
|
||||
LnKey,
|
||||
Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
|
||||
> = HashMap::new();
|
||||
|
||||
let fee_reserve = FeeReserve {
|
||||
min_fee_reserve: 1.into(),
|
||||
@@ -52,28 +55,18 @@ where
|
||||
0,
|
||||
);
|
||||
|
||||
let mut ln_backends: HashMap<
|
||||
LnKey,
|
||||
Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
|
||||
> = HashMap::new();
|
||||
|
||||
ln_backends.insert(
|
||||
LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11),
|
||||
Arc::new(fake_wallet),
|
||||
);
|
||||
|
||||
let quote_ttl = 100000;
|
||||
let mint = create_mint(database, ln_backends.clone()).await?;
|
||||
|
||||
let mint_arc = Arc::new(mint);
|
||||
|
||||
let v1_service = cdk_axum::create_mint_router(
|
||||
&format!("http://{}:{}", addr, port),
|
||||
Arc::clone(&mint_arc),
|
||||
ln_backends.clone(),
|
||||
quote_ttl,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mint_service = Router::new()
|
||||
.merge(v1_service)
|
||||
@@ -81,28 +74,13 @@ where
|
||||
|
||||
let mint = Arc::clone(&mint_arc);
|
||||
|
||||
for wallet in ln_backends.values() {
|
||||
let wallet_clone = Arc::clone(wallet);
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
match wallet_clone.wait_any_invoice().await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) =
|
||||
handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
|
||||
{
|
||||
// nosemgrep: direct-panic
|
||||
panic!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// nosemgrep: direct-panic
|
||||
panic!("Could not get invoice stream: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let shutdown = Arc::new(Notify::new());
|
||||
|
||||
tokio::spawn({
|
||||
let shutdown = Arc::clone(&shutdown);
|
||||
async move { mint.wait_for_paid_invoices(shutdown).await }
|
||||
});
|
||||
|
||||
println!("Staring Axum server");
|
||||
axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
|
||||
.serve(mint_service.into_make_service())
|
||||
|
||||
@@ -8,14 +8,14 @@ use cdk::{
|
||||
cdk_lightning::MintLightning,
|
||||
mint::{FeeReserve, Mint},
|
||||
nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings},
|
||||
types::LnKey,
|
||||
types::{LnKey, QuoteTTL},
|
||||
};
|
||||
use cdk_cln::Cln as CdkCln;
|
||||
use futures::StreamExt;
|
||||
use ln_regtest_rs::{
|
||||
bitcoin_client::BitcoinClient, bitcoind::Bitcoind, cln::Clnd, cln_client::ClnClient, lnd::Lnd,
|
||||
lnd_client::LndClient,
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
@@ -140,7 +140,13 @@ pub async fn create_cln_backend(cln_client: &ClnClient) -> Result<CdkCln> {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn create_mint<D>(database: D) -> Result<Mint>
|
||||
pub async fn create_mint<D>(
|
||||
database: D,
|
||||
ln_backends: HashMap<
|
||||
LnKey,
|
||||
Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
|
||||
>,
|
||||
) -> Result<Mint>
|
||||
where
|
||||
D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
|
||||
{
|
||||
@@ -160,11 +166,15 @@ where
|
||||
let mut supported_units: HashMap<CurrencyUnit, (u64, u8)> = HashMap::new();
|
||||
supported_units.insert(CurrencyUnit::Sat, (0, 32));
|
||||
|
||||
let quote_ttl = QuoteTTL::new(10000, 10000);
|
||||
|
||||
let mint = Mint::new(
|
||||
&get_mint_url(),
|
||||
&mnemonic.to_seed_normalized(""),
|
||||
mint_info,
|
||||
quote_ttl,
|
||||
Arc::new(database),
|
||||
ln_backends,
|
||||
supported_units,
|
||||
)
|
||||
.await?;
|
||||
@@ -189,7 +199,6 @@ where
|
||||
// Parse input
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let mint = create_mint(database).await?;
|
||||
let cln_client = init_cln_client().await?;
|
||||
|
||||
let cln_backend = create_cln_backend(&cln_client).await?;
|
||||
@@ -204,18 +213,13 @@ where
|
||||
Arc::new(cln_backend),
|
||||
);
|
||||
|
||||
let quote_ttl = 100000;
|
||||
let mint = create_mint(database, ln_backends.clone()).await?;
|
||||
|
||||
let mint_arc = Arc::new(mint);
|
||||
|
||||
let v1_service = cdk_axum::create_mint_router(
|
||||
&get_mint_url(),
|
||||
Arc::clone(&mint_arc),
|
||||
ln_backends.clone(),
|
||||
quote_ttl,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mint_service = Router::new()
|
||||
.merge(v1_service)
|
||||
@@ -223,28 +227,13 @@ where
|
||||
|
||||
let mint = Arc::clone(&mint_arc);
|
||||
|
||||
for wallet in ln_backends.values() {
|
||||
let wallet_clone = Arc::clone(wallet);
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
match wallet_clone.wait_any_invoice().await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) =
|
||||
handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
|
||||
{
|
||||
// nosemgrep: direct-panic
|
||||
panic!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// nosemgrep: direct-panic
|
||||
panic!("Could not get invoice stream: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let shutdown = Arc::new(Notify::new());
|
||||
|
||||
tokio::spawn({
|
||||
let shutdown = Arc::clone(&shutdown);
|
||||
async move { mint.wait_for_paid_invoices(shutdown).await }
|
||||
});
|
||||
|
||||
println!("Staring Axum server");
|
||||
axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
|
||||
.serve(mint_service.into_make_service())
|
||||
@@ -253,25 +242,6 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update mint quote when called for a paid invoice
|
||||
async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
|
||||
println!("Invoice with lookup id paid: {}", request_lookup_id);
|
||||
if let Ok(Some(mint_quote)) = mint
|
||||
.localstore
|
||||
.get_mint_quote_by_request_lookup_id(request_lookup_id)
|
||||
.await
|
||||
{
|
||||
println!(
|
||||
"Quote {} paid by lookup id {}",
|
||||
mint_quote.id, request_lookup_id
|
||||
);
|
||||
mint.localstore
|
||||
.update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fund_ln(
|
||||
bitcoin_client: &BitcoinClient,
|
||||
cln_client: &ClnClient,
|
||||
|
||||
@@ -14,12 +14,12 @@ use cdk::nuts::{
|
||||
CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState,
|
||||
Nuts, PaymentMethod, PreMintSecrets, Proofs, State,
|
||||
};
|
||||
use cdk::types::LnKey;
|
||||
use cdk::types::{LnKey, QuoteTTL};
|
||||
use cdk::wallet::client::HttpClient;
|
||||
use cdk::{Mint, Wallet};
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use futures::StreamExt;
|
||||
use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
|
||||
use tokio::sync::Notify;
|
||||
use tokio::time::sleep;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
@@ -72,26 +72,22 @@ pub async fn start_mint(
|
||||
|
||||
let mnemonic = Mnemonic::generate(12)?;
|
||||
|
||||
let quote_ttl = QuoteTTL::new(10000, 10000);
|
||||
|
||||
let mint = Mint::new(
|
||||
&get_mint_url(),
|
||||
&mnemonic.to_seed_normalized(""),
|
||||
mint_info,
|
||||
quote_ttl,
|
||||
Arc::new(MintMemoryDatabase::default()),
|
||||
ln_backends.clone(),
|
||||
supported_units,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let quote_ttl = 100000;
|
||||
|
||||
let mint_arc = Arc::new(mint);
|
||||
|
||||
let v1_service = cdk_axum::create_mint_router(
|
||||
&get_mint_url(),
|
||||
Arc::clone(&mint_arc),
|
||||
ln_backends.clone(),
|
||||
quote_ttl,
|
||||
)
|
||||
.await?;
|
||||
let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)).await?;
|
||||
|
||||
let mint_service = Router::new()
|
||||
.merge(v1_service)
|
||||
@@ -99,28 +95,12 @@ pub async fn start_mint(
|
||||
|
||||
let mint = Arc::clone(&mint_arc);
|
||||
|
||||
for wallet in ln_backends.values() {
|
||||
let wallet_clone = Arc::clone(wallet);
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
match wallet_clone.wait_any_invoice().await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) =
|
||||
handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
|
||||
{
|
||||
// nosemgrep: direct-panic
|
||||
panic!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// nosemgrep: direct-panic
|
||||
panic!("Could not get invoice stream: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let shutdown = Arc::new(Notify::new());
|
||||
|
||||
tokio::spawn({
|
||||
let shutdown = Arc::clone(&shutdown);
|
||||
async move { mint.wait_for_paid_invoices(shutdown).await }
|
||||
});
|
||||
|
||||
axum::Server::bind(
|
||||
&format!("{}:{}", get_mint_addr(), get_mint_port())
|
||||
@@ -133,25 +113,6 @@ pub async fn start_mint(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update mint quote when called for a paid invoice
|
||||
async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
|
||||
println!("Invoice with lookup id paid: {}", request_lookup_id);
|
||||
if let Ok(Some(mint_quote)) = mint
|
||||
.localstore
|
||||
.get_mint_quote_by_request_lookup_id(request_lookup_id)
|
||||
.await
|
||||
{
|
||||
println!(
|
||||
"Quote {} paid by lookup id {}",
|
||||
mint_quote.id, request_lookup_id
|
||||
);
|
||||
mint.localstore
|
||||
.update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wallet_mint(
|
||||
wallet: Arc<Wallet>,
|
||||
amount: Amount,
|
||||
|
||||
@@ -5,10 +5,12 @@ use bip39::Mnemonic;
|
||||
use cdk::amount::{Amount, SplitTarget};
|
||||
use cdk::cdk_database::mint_memory::MintMemoryDatabase;
|
||||
use cdk::dhke::construct_proofs;
|
||||
use cdk::mint::MintQuote;
|
||||
use cdk::nuts::{
|
||||
CurrencyUnit, Id, MintBolt11Request, MintInfo, Nuts, PreMintSecrets, Proofs, SecretKey,
|
||||
SpendingConditions, SwapRequest,
|
||||
};
|
||||
use cdk::types::QuoteTTL;
|
||||
use cdk::util::unix_time;
|
||||
use cdk::Mint;
|
||||
use std::collections::HashMap;
|
||||
@@ -36,11 +38,15 @@ async fn new_mint(fee: u64) -> Mint {
|
||||
|
||||
let mnemonic = Mnemonic::generate(12).unwrap();
|
||||
|
||||
let quote_ttl = QuoteTTL::new(10000, 10000);
|
||||
|
||||
Mint::new(
|
||||
MINT_URL,
|
||||
&mnemonic.to_seed_normalized(""),
|
||||
mint_info,
|
||||
quote_ttl,
|
||||
Arc::new(MintMemoryDatabase::default()),
|
||||
HashMap::new(),
|
||||
supported_units,
|
||||
)
|
||||
.await
|
||||
@@ -59,16 +65,16 @@ async fn mint_proofs(
|
||||
) -> Result<Proofs> {
|
||||
let request_lookup = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let mint_quote = mint
|
||||
.new_mint_quote(
|
||||
MINT_URL.parse()?,
|
||||
"".to_string(),
|
||||
CurrencyUnit::Sat,
|
||||
amount,
|
||||
unix_time() + 36000,
|
||||
request_lookup.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let quote = MintQuote::new(
|
||||
mint.mint_url.clone(),
|
||||
"".to_string(),
|
||||
CurrencyUnit::Sat,
|
||||
amount,
|
||||
unix_time() + 36000,
|
||||
request_lookup.to_string(),
|
||||
);
|
||||
|
||||
mint.localstore.add_mint_quote(quote.clone()).await?;
|
||||
|
||||
mint.pay_mint_quote_for_request_id(&request_lookup).await?;
|
||||
let keyset_id = Id::from(&keys);
|
||||
@@ -76,7 +82,7 @@ async fn mint_proofs(
|
||||
let premint = PreMintSecrets::random(keyset_id, amount, split_target)?;
|
||||
|
||||
let mint_request = MintBolt11Request {
|
||||
quote: mint_quote.id,
|
||||
quote: quote.id,
|
||||
outputs: premint.blinded_messages(),
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use cdk::nuts::{
|
||||
nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MeltQuoteState, MintInfo,
|
||||
MintMethodSettings, MintVersion, MppMethodSettings, Nuts, PaymentMethod,
|
||||
};
|
||||
use cdk::types::LnKey;
|
||||
use cdk::types::{LnKey, QuoteTTL};
|
||||
use cdk_cln::Cln;
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use cdk_lnbits::LNbits;
|
||||
@@ -32,8 +32,7 @@ use cdk_strike::Strike;
|
||||
use clap::Parser;
|
||||
use cli::CLIArgs;
|
||||
use config::{DatabaseEngine, LnBackend};
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use url::Url;
|
||||
@@ -425,11 +424,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
|
||||
|
||||
let quote_ttl = QuoteTTL::new(10000, 10000);
|
||||
|
||||
let mint = Mint::new(
|
||||
&settings.info.url,
|
||||
&mnemonic.to_seed_normalized(""),
|
||||
mint_info,
|
||||
quote_ttl,
|
||||
localstore,
|
||||
ln_backends.clone(),
|
||||
supported_units,
|
||||
)
|
||||
.await?;
|
||||
@@ -449,17 +452,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Pending melt quotes where the paynment has **failed** inputs are reset to unspent
|
||||
check_pending_melt_quotes(Arc::clone(&mint), &ln_backends).await?;
|
||||
|
||||
let mint_url = settings.info.url;
|
||||
let listen_addr = settings.info.listen_host;
|
||||
let listen_port = settings.info.listen_port;
|
||||
let quote_ttl = settings
|
||||
let _quote_ttl = settings
|
||||
.info
|
||||
.seconds_quote_is_valid_for
|
||||
.unwrap_or(DEFAULT_QUOTE_TTL_SECS);
|
||||
|
||||
let v1_service =
|
||||
cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), ln_backends.clone(), quote_ttl)
|
||||
.await?;
|
||||
let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint)).await?;
|
||||
|
||||
let mut mint_service = Router::new()
|
||||
.merge(v1_service)
|
||||
@@ -469,45 +469,35 @@ async fn main() -> anyhow::Result<()> {
|
||||
mint_service = mint_service.merge(router);
|
||||
}
|
||||
|
||||
// Spawn task to wait for invoces to be paid and update mint quotes
|
||||
for (_, ln) in ln_backends {
|
||||
let mint = Arc::clone(&mint);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match ln.wait_any_invoice().await {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) =
|
||||
handle_paid_invoice(mint.clone(), &request_lookup_id).await
|
||||
{
|
||||
tracing::warn!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Could not get invoice stream: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let shutdown = Arc::new(Notify::new());
|
||||
|
||||
axum::Server::bind(
|
||||
tokio::spawn({
|
||||
let shutdown = Arc::clone(&shutdown);
|
||||
async move { mint.wait_for_paid_invoices(shutdown).await }
|
||||
});
|
||||
|
||||
let axum_result = axum::Server::bind(
|
||||
&format!("{}:{}", listen_addr, listen_port)
|
||||
.as_str()
|
||||
.parse()?,
|
||||
)
|
||||
.serve(mint_service.into_make_service())
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
shutdown.notify_waiters();
|
||||
|
||||
match axum_result {
|
||||
Ok(_) => {
|
||||
tracing::info!("Axum server stopped with okay status");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Axum server stopped with error");
|
||||
tracing::error!("{}", err);
|
||||
|
||||
bail!("Axum exited with error")
|
||||
}
|
||||
}
|
||||
|
||||
/// Update mint quote when called for a paid invoice
|
||||
async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
|
||||
tracing::debug!("Invoice with lookup id paid: {}", request_lookup_id);
|
||||
mint.pay_mint_quote_for_request_id(request_lookup_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ sync_wrapper = "0.1.2"
|
||||
bech32 = "0.9.1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1", features = [
|
||||
tokio = { version = "1.21", features = [
|
||||
"rt-multi-thread",
|
||||
"time",
|
||||
"macros",
|
||||
@@ -54,7 +54,7 @@ tokio = { version = "1", features = [
|
||||
] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros", "sync", "time"] }
|
||||
tokio = { version = "1.21", features = ["rt", "macros", "sync", "time"] }
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
|
||||
|
||||
|
||||
@@ -4,23 +4,27 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
|
||||
use bitcoin::secp256k1::{self, Secp256k1};
|
||||
use futures::StreamExt;
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::instrument;
|
||||
|
||||
use self::nut05::QuoteState;
|
||||
use self::nut05::{MeltBolt11Response, QuoteState};
|
||||
use self::nut11::EnforceSigFlag;
|
||||
use crate::cdk_database::{self, MintDatabase};
|
||||
use crate::cdk_lightning::to_unit;
|
||||
use crate::cdk_lightning::{self, to_unit, MintLightning, PayInvoiceResponse};
|
||||
use crate::dhke::{hash_to_curve, sign_message, verify_message};
|
||||
use crate::error::Error;
|
||||
use crate::fees::calculate_fee;
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::nut11::enforce_sig_flag;
|
||||
use crate::nuts::*;
|
||||
use crate::types::{LnKey, QuoteTTL};
|
||||
use crate::util::unix_time;
|
||||
use crate::Amount;
|
||||
|
||||
@@ -35,8 +39,12 @@ pub struct Mint {
|
||||
pub mint_url: MintUrl,
|
||||
/// Mint Info
|
||||
pub mint_info: MintInfo,
|
||||
/// Quotes ttl
|
||||
pub quote_ttl: QuoteTTL,
|
||||
/// Mint Storage backend
|
||||
pub localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
|
||||
/// Ln backends for mint
|
||||
pub ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
|
||||
/// Active Mint Keysets
|
||||
keysets: Arc<RwLock<HashMap<Id, MintKeySet>>>,
|
||||
secp_ctx: Secp256k1<secp256k1::All>,
|
||||
@@ -49,7 +57,9 @@ impl Mint {
|
||||
mint_url: &str,
|
||||
seed: &[u8],
|
||||
mint_info: MintInfo,
|
||||
quote_ttl: QuoteTTL,
|
||||
localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
|
||||
ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
|
||||
// Hashmap where the key is the unit and value is (input fee ppk, max_order)
|
||||
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||
) -> Result<Self, Error> {
|
||||
@@ -160,9 +170,11 @@ impl Mint {
|
||||
mint_url: MintUrl::from_str(mint_url)?,
|
||||
keysets: Arc::new(RwLock::new(active_keysets)),
|
||||
secp_ctx,
|
||||
quote_ttl,
|
||||
xpriv,
|
||||
localstore,
|
||||
mint_info,
|
||||
ln,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,17 +202,12 @@ impl Mint {
|
||||
&self.mint_info
|
||||
}
|
||||
|
||||
/// New mint quote
|
||||
#[instrument(skip_all)]
|
||||
pub async fn new_mint_quote(
|
||||
/// Checks that minting is enabled, request is supported unit and within range
|
||||
fn check_mint_request_acceptable(
|
||||
&self,
|
||||
mint_url: MintUrl,
|
||||
request: String,
|
||||
unit: CurrencyUnit,
|
||||
amount: Amount,
|
||||
expiry: u64,
|
||||
ln_lookup: String,
|
||||
) -> Result<MintQuote, Error> {
|
||||
unit: CurrencyUnit,
|
||||
) -> Result<(), Error> {
|
||||
let nut04 = &self.mint_info.nuts.nut04;
|
||||
|
||||
if nut04.disabled {
|
||||
@@ -236,18 +243,72 @@ impl Mint {
|
||||
}
|
||||
}
|
||||
|
||||
let quote = MintQuote::new(mint_url, request, unit, amount, expiry, ln_lookup.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create new mint bolt11 quote
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_mint_bolt11_quote(
|
||||
&self,
|
||||
mint_quote_request: MintQuoteBolt11Request,
|
||||
) -> Result<MintQuoteBolt11Response, Error> {
|
||||
let MintQuoteBolt11Request {
|
||||
amount,
|
||||
unit,
|
||||
description,
|
||||
} = mint_quote_request;
|
||||
|
||||
self.check_mint_request_acceptable(amount, unit)?;
|
||||
|
||||
let ln = self
|
||||
.ln
|
||||
.get(&LnKey::new(unit, PaymentMethod::Bolt11))
|
||||
.ok_or_else(|| {
|
||||
tracing::info!("Bolt11 mint request for unsupported unit");
|
||||
|
||||
Error::UnitUnsupported
|
||||
})?;
|
||||
|
||||
let quote_expiry = unix_time() + self.quote_ttl.mint_ttl;
|
||||
|
||||
if description.is_some() && !ln.get_settings().invoice_description {
|
||||
tracing::error!("Backend does not support invoice description");
|
||||
return Err(Error::InvoiceDescriptionUnsupported);
|
||||
}
|
||||
|
||||
let create_invoice_response = ln
|
||||
.create_invoice(
|
||||
amount,
|
||||
&unit,
|
||||
description.unwrap_or("".to_string()),
|
||||
quote_expiry,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not create invoice: {}", err);
|
||||
Error::InvalidPaymentRequest
|
||||
})?;
|
||||
|
||||
let quote = MintQuote::new(
|
||||
self.mint_url.clone(),
|
||||
create_invoice_response.request.to_string(),
|
||||
unit,
|
||||
amount,
|
||||
create_invoice_response.expiry.unwrap_or(0),
|
||||
create_invoice_response.request_lookup_id.clone(),
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
"New mint quote {} for {} {} with request id {}",
|
||||
quote.id,
|
||||
amount,
|
||||
unit,
|
||||
&ln_lookup
|
||||
create_invoice_response.request_lookup_id,
|
||||
);
|
||||
|
||||
self.localstore.add_mint_quote(quote.clone()).await?;
|
||||
|
||||
Ok(quote)
|
||||
Ok(quote.into())
|
||||
}
|
||||
|
||||
/// Check mint quote
|
||||
@@ -345,24 +406,72 @@ impl Mint {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// New melt quote
|
||||
#[instrument(skip_all)]
|
||||
pub async fn new_melt_quote(
|
||||
/// Wait for any invoice to be paid
|
||||
/// For each backend starts a task that waits for any invoice to be paid
|
||||
/// Once invoice is paid mint quote status is updated
|
||||
#[allow(clippy::incompatible_msrv)]
|
||||
// Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0)
|
||||
pub async fn wait_for_paid_invoices(&self, shutdown: Arc<Notify>) -> Result<(), Error> {
|
||||
let mint_arc = Arc::new(self.clone());
|
||||
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
for (key, ln) in self.ln.iter() {
|
||||
let mint = Arc::clone(&mint_arc);
|
||||
let ln = Arc::clone(ln);
|
||||
let shutdown = Arc::clone(&shutdown);
|
||||
let key = *key;
|
||||
join_set.spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.notified() => {
|
||||
tracing::info!("Shutdown signal received, stopping task for {:?}", key);
|
||||
break;
|
||||
}
|
||||
result = ln.wait_any_invoice() => {
|
||||
match result {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id).await {
|
||||
tracing::warn!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Could not get invoice stream for {:?}: {}",key, err);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn a task to manage the JoinSet
|
||||
while let Some(result) = join_set.join_next().await {
|
||||
match result {
|
||||
Ok(_) => tracing::info!("A task completed successfully."),
|
||||
Err(err) => tracing::warn!("A task failed: {:?}", err),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_melt_request_acceptable(
|
||||
&self,
|
||||
request: String,
|
||||
unit: CurrencyUnit,
|
||||
amount: Amount,
|
||||
fee_reserve: Amount,
|
||||
expiry: u64,
|
||||
request_lookup_id: String,
|
||||
) -> Result<MeltQuote, Error> {
|
||||
unit: CurrencyUnit,
|
||||
method: PaymentMethod,
|
||||
) -> Result<(), Error> {
|
||||
let nut05 = &self.mint_info.nuts.nut05;
|
||||
|
||||
if nut05.disabled {
|
||||
return Err(Error::MeltingDisabled);
|
||||
}
|
||||
|
||||
match nut05.get_settings(&unit, &PaymentMethod::Bolt11) {
|
||||
match nut05.get_settings(&unit, &method) {
|
||||
Some(settings) => {
|
||||
if settings
|
||||
.max_amount
|
||||
@@ -391,13 +500,61 @@ impl Mint {
|
||||
}
|
||||
}
|
||||
|
||||
let quote = MeltQuote::new(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get melt bolt11 quote
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_melt_bolt11_quote(
|
||||
&self,
|
||||
melt_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<MeltQuoteBolt11Response, Error> {
|
||||
let MeltQuoteBolt11Request {
|
||||
request,
|
||||
unit,
|
||||
amount,
|
||||
fee_reserve,
|
||||
expiry,
|
||||
request_lookup_id.clone(),
|
||||
options: _,
|
||||
} = melt_request;
|
||||
|
||||
let amount = match melt_request.options {
|
||||
Some(mpp_amount) => mpp_amount.amount,
|
||||
None => {
|
||||
let amount_msat = request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::InvoiceAmountUndefined)?;
|
||||
|
||||
to_unit(amount_msat, &CurrencyUnit::Msat, unit)
|
||||
.map_err(|_err| Error::UnsupportedUnit)?
|
||||
}
|
||||
};
|
||||
|
||||
self.check_melt_request_acceptable(amount, *unit, PaymentMethod::Bolt11)?;
|
||||
|
||||
let ln = self
|
||||
.ln
|
||||
.get(&LnKey::new(*unit, PaymentMethod::Bolt11))
|
||||
.ok_or_else(|| {
|
||||
tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
|
||||
|
||||
Error::UnitUnsupported
|
||||
})?;
|
||||
|
||||
let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
"Could not get payment quote for mint quote, {} bolt11, {}",
|
||||
unit,
|
||||
err
|
||||
);
|
||||
|
||||
Error::UnitUnsupported
|
||||
})?;
|
||||
|
||||
let quote = MeltQuote::new(
|
||||
request.to_string(),
|
||||
*unit,
|
||||
payment_quote.amount,
|
||||
payment_quote.fee,
|
||||
unix_time() + self.quote_ttl.melt_ttl,
|
||||
payment_quote.request_lookup_id.clone(),
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
@@ -405,12 +562,12 @@ impl Mint {
|
||||
quote.id,
|
||||
amount,
|
||||
unit,
|
||||
request_lookup_id
|
||||
payment_quote.request_lookup_id
|
||||
);
|
||||
|
||||
self.localstore.add_melt_quote(quote.clone()).await?;
|
||||
|
||||
Ok(quote)
|
||||
Ok(quote.into())
|
||||
}
|
||||
|
||||
/// Fee required for proof set
|
||||
@@ -1257,6 +1414,212 @@ impl Mint {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Melt Bolt11
|
||||
#[instrument(skip_all)]
|
||||
pub async fn melt_bolt11(
|
||||
&self,
|
||||
melt_request: &MeltBolt11Request,
|
||||
) -> Result<MeltBolt11Response, Error> {
|
||||
use std::sync::Arc;
|
||||
async fn check_payment_state(
|
||||
ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
|
||||
melt_quote: &MeltQuote,
|
||||
) -> anyhow::Result<PayInvoiceResponse> {
|
||||
match ln
|
||||
.check_outgoing_payment(&melt_quote.request_lookup_id)
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok(response),
|
||||
Err(check_err) => {
|
||||
// If we cannot check the status of the payment we keep the proofs stuck as pending.
|
||||
tracing::error!(
|
||||
"Could not check the status of payment for {},. Proofs stuck as pending",
|
||||
melt_quote.id
|
||||
);
|
||||
tracing::error!("Checking payment error: {}", check_err);
|
||||
bail!("Could not check payment status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let quote = match self.verify_melt_request(melt_request).await {
|
||||
Ok(quote) => quote,
|
||||
Err(err) => {
|
||||
tracing::debug!("Error attempting to verify melt quote: {}", err);
|
||||
|
||||
if let Err(err) = self.process_unpaid_melt(melt_request).await {
|
||||
tracing::error!(
|
||||
"Could not reset melt quote {} state: {}",
|
||||
melt_request.quote,
|
||||
err
|
||||
);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let settled_internally_amount =
|
||||
match self.handle_internal_melt_mint("e, melt_request).await {
|
||||
Ok(amount) => amount,
|
||||
Err(err) => {
|
||||
tracing::error!("Attempting to settle internally failed");
|
||||
if let Err(err) = self.process_unpaid_melt(melt_request).await {
|
||||
tracing::error!(
|
||||
"Could not reset melt quote {} state: {}",
|
||||
melt_request.quote,
|
||||
err
|
||||
);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
|
||||
Some(amount_spent) => (None, amount_spent),
|
||||
None => {
|
||||
// If the quote unit is SAT or MSAT we can check that the expected fees are
|
||||
// provided. We also check if the quote is less then the invoice
|
||||
// amount in the case that it is a mmp However, if the quote is not
|
||||
// of a bitcoin unit we cannot do these checks as the mint
|
||||
// is unaware of a conversion rate. In this case it is assumed that the quote is
|
||||
// correct and the mint should pay the full invoice amount if inputs
|
||||
// > `then quote.amount` are included. This is checked in the
|
||||
// `verify_melt` method.
|
||||
let partial_amount = match quote.unit {
|
||||
CurrencyUnit::Sat | CurrencyUnit::Msat => {
|
||||
match self.check_melt_expected_ln_fees("e, melt_request).await {
|
||||
Ok(amount) => amount,
|
||||
Err(err) => {
|
||||
tracing::error!("Fee is not expected: {}", err);
|
||||
if let Err(err) = self.process_unpaid_melt(melt_request).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(Error::Internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let ln = match self.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) = self.process_unpaid_melt(melt_request).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
|
||||
return Err(Error::UnitUnsupported);
|
||||
}
|
||||
};
|
||||
|
||||
let pre = match ln
|
||||
.pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
|
||||
.await
|
||||
{
|
||||
Ok(pay)
|
||||
if pay.status == MeltQuoteState::Unknown
|
||||
|| pay.status == MeltQuoteState::Failed =>
|
||||
{
|
||||
let check_response = check_payment_state(Arc::clone(ln), "e)
|
||||
.await
|
||||
.map_err(|_| Error::Internal)?;
|
||||
|
||||
if check_response.status == MeltQuoteState::Paid {
|
||||
tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
|
||||
|
||||
return Err(Error::Internal);
|
||||
}
|
||||
|
||||
check_response
|
||||
}
|
||||
Ok(pay) => pay,
|
||||
Err(err) => {
|
||||
// If the error is that the invoice was already paid we do not want to hold
|
||||
// hold the proofs as pending to we reset them and return an error.
|
||||
if matches!(err, cdk_lightning::Error::InvoiceAlreadyPaid) {
|
||||
tracing::debug!("Invoice already paid, resetting melt quote");
|
||||
if let Err(err) = self.process_unpaid_melt(melt_request).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(Error::RequestAlreadyPaid);
|
||||
}
|
||||
|
||||
tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
|
||||
|
||||
let check_response = check_payment_state(Arc::clone(ln), "e)
|
||||
.await
|
||||
.map_err(|_| Error::Internal)?;
|
||||
// If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
|
||||
if check_response.status == MeltQuoteState::Paid {
|
||||
tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
|
||||
|
||||
return Err(Error::Internal);
|
||||
}
|
||||
check_response
|
||||
}
|
||||
};
|
||||
|
||||
match pre.status {
|
||||
MeltQuoteState::Paid => (),
|
||||
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
|
||||
tracing::info!(
|
||||
"Lightning payment for quote {} failed.",
|
||||
melt_request.quote
|
||||
);
|
||||
if let Err(err) = self.process_unpaid_melt(melt_request).await {
|
||||
tracing::error!("Could not reset melt quote state: {}", err);
|
||||
}
|
||||
return Err(Error::PaymentFailed);
|
||||
}
|
||||
MeltQuoteState::Pending => {
|
||||
tracing::warn!(
|
||||
"LN payment pending, proofs are stuck as pending for quote: {}",
|
||||
melt_request.quote
|
||||
);
|
||||
return Err(Error::PendingQuote);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from unit of backend to quote unit
|
||||
// Note: this should never fail since these conversions happen earlier and would fail there.
|
||||
// Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
|
||||
let amount_spent =
|
||||
to_unit(pre.total_spent, &pre.unit, "e.unit).unwrap_or_default();
|
||||
|
||||
let payment_lookup_id = pre.payment_lookup_id;
|
||||
|
||||
if payment_lookup_id != quote.request_lookup_id {
|
||||
tracing::info!(
|
||||
"Payment lookup id changed post payment from {} to {}",
|
||||
quote.request_lookup_id,
|
||||
payment_lookup_id
|
||||
);
|
||||
|
||||
let mut melt_quote = quote;
|
||||
melt_quote.request_lookup_id = payment_lookup_id;
|
||||
|
||||
if let Err(err) = self.localstore.add_melt_quote(melt_quote).await {
|
||||
tracing::warn!("Could not update payment lookup id: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
(pre.payment_preimage, amount_spent)
|
||||
}
|
||||
};
|
||||
|
||||
// If we made it here the payment has been made.
|
||||
// We process the melt burning the inputs and returning change
|
||||
let res = self
|
||||
.process_melt_request(melt_request, preimage, amount_spent_quote_unit)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("Could not process melt request: {}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
Ok(res.into())
|
||||
}
|
||||
|
||||
/// Process melt request marking [`Proofs`] as spent
|
||||
/// The melt request must be verifyed using [`Self::verify_melt_request`]
|
||||
/// before calling [`Self::process_melt_request`]
|
||||
@@ -1680,6 +2043,7 @@ mod tests {
|
||||
mint_info: MintInfo,
|
||||
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||
melt_requests: Vec<(MeltBolt11Request, LnKey)>,
|
||||
quote_ttl: QuoteTTL,
|
||||
}
|
||||
|
||||
async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> {
|
||||
@@ -1703,7 +2067,9 @@ mod tests {
|
||||
config.mint_url,
|
||||
config.seed,
|
||||
config.mint_info,
|
||||
config.quote_ttl,
|
||||
localstore,
|
||||
HashMap::new(),
|
||||
config.supported_units,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -155,6 +155,22 @@ impl LnKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// Secs wuotes are valid
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct QuoteTTL {
|
||||
/// Seconds mint quote is valid
|
||||
pub mint_ttl: u64,
|
||||
/// Seconds melt quote is valid
|
||||
pub melt_ttl: u64,
|
||||
}
|
||||
|
||||
impl QuoteTTL {
|
||||
/// Create new [`QuoteTTL`]
|
||||
pub fn new(mint_ttl: u64, melt_ttl: u64) -> QuoteTTL {
|
||||
Self { mint_ttl, melt_ttl }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
Reference in New Issue
Block a user