feat: mintd axum server

feat: deafult NUT-04 and NUT-05 settings to enable bolt11 sats
This commit is contained in:
thesimplekid
2024-06-10 23:26:08 +01:00
parent 16aeec92c7
commit 6a315fc3b9
49 changed files with 1772 additions and 246 deletions

View File

@@ -32,7 +32,9 @@ jobs:
-p cdk --no-default-features --features mint, -p cdk --no-default-features --features mint,
-p cdk-redb, -p cdk-redb,
-p cdk-sqlite, -p cdk-sqlite,
-p cdk-axum,
--bin cdk-cli, --bin cdk-cli,
--bin cdk-mintd,
--examples --examples
] ]
steps: steps:

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
.idea/ .idea/
*.redb *.redb
*.sqlite* *.sqlite*
config.toml

View File

@@ -30,6 +30,7 @@ cdk-rexie = { version = "0.1", path = "./crates/cdk-rexie", default-features = f
cdk-sqlite = { version = "0.1", path = "./crates/cdk-sqlite", default-features = false } cdk-sqlite = { version = "0.1", path = "./crates/cdk-sqlite", default-features = false }
cdk-redb = { version = "0.1", path = "./crates/cdk-redb", default-features = false } cdk-redb = { version = "0.1", path = "./crates/cdk-redb", default-features = false }
cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false } cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false }
cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = false }
tokio = { version = "1", default-features = false } tokio = { version = "1", default-features = false }
thiserror = "1" thiserror = "1"
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
@@ -39,6 +40,8 @@ serde-wasm-bindgen = "0.6.5"
futures = { version = "0.3.28", default-feature = false } futures = { version = "0.3.28", default-feature = false }
web-sys = { version = "0.3.69", default-features = false, features = ["console"] } web-sys = { version = "0.3.69", default-features = false, features = ["console"] }
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
lightning-invoice = { version = "0.31", features = ["serde"] }
home = "0.5.9"
[profile] [profile]

View File

@@ -8,6 +8,7 @@ pub enum JsCurrencyUnit {
Sat, Sat,
Msat, Msat,
Usd, Usd,
Eur,
} }
impl From<CurrencyUnit> for JsCurrencyUnit { impl From<CurrencyUnit> for JsCurrencyUnit {
@@ -16,7 +17,7 @@ impl From<CurrencyUnit> for JsCurrencyUnit {
CurrencyUnit::Sat => JsCurrencyUnit::Sat, CurrencyUnit::Sat => JsCurrencyUnit::Sat,
CurrencyUnit::Msat => JsCurrencyUnit::Msat, CurrencyUnit::Msat => JsCurrencyUnit::Msat,
CurrencyUnit::Usd => JsCurrencyUnit::Usd, CurrencyUnit::Usd => JsCurrencyUnit::Usd,
CurrencyUnit::Custom(_) => todo!(), CurrencyUnit::Eur => JsCurrencyUnit::Eur,
} }
} }
} }
@@ -27,6 +28,7 @@ impl From<JsCurrencyUnit> for CurrencyUnit {
JsCurrencyUnit::Sat => CurrencyUnit::Sat, JsCurrencyUnit::Sat => CurrencyUnit::Sat,
JsCurrencyUnit::Msat => CurrencyUnit::Msat, JsCurrencyUnit::Msat => CurrencyUnit::Msat,
JsCurrencyUnit::Usd => CurrencyUnit::Usd, JsCurrencyUnit::Usd => CurrencyUnit::Usd,
JsCurrencyUnit::Eur => CurrencyUnit::Eur,
} }
} }
} }

View File

@@ -1,4 +1,5 @@
use std::ops::Deref; use std::ops::Deref;
use std::str::FromStr;
use cdk::nuts::{CurrencyUnit, KeySet, KeysResponse, KeysetResponse}; use cdk::nuts::{CurrencyUnit, KeySet, KeysResponse, KeysetResponse};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@@ -33,7 +34,7 @@ impl JsKeySet {
Self { Self {
inner: KeySet { inner: KeySet {
id: *id.deref(), id: *id.deref(),
unit: CurrencyUnit::from(&unit), unit: CurrencyUnit::from_str(&unit).unwrap(),
keys: keys.deref().clone(), keys: keys.deref().clone(),
}, },
} }

View File

@@ -1,6 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use cdk::types::MeltQuote; use cdk::wallet::types::MeltQuote;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::nuts::JsCurrencyUnit; use crate::nuts::JsCurrencyUnit;

View File

@@ -1,6 +1,6 @@
use std::ops::Deref; use std::ops::Deref;
use cdk::types::MintQuote; use cdk::wallet::MintQuote;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::nuts::JsCurrencyUnit; use crate::nuts::JsCurrencyUnit;

View File

@@ -0,0 +1,19 @@
[package]
name = "cdk-axum"
version = "0.1.0"
edition = "2021"
license.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = "1.0.75"
async-trait.workspace = true
axum = "0.7.5"
axum-macros = "0.4.1"
cdk = { workspace = true, default-features = false, features = ["mint"] }
tokio.workspace = true
tower-http = { version = "0.5.2", features = ["cors"] }
tracing.workspace = true
futures = "0.3.28"

View File

@@ -0,0 +1,63 @@
//! Axum server for Mint
#![warn(missing_docs)]
#![warn(rustdoc::bare_urls)]
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 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: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
quote_ttl: u64,
) -> Result<Router> {
let state = MintState {
ln,
mint,
mint_url: mint_url.to_string(),
quote_ttl,
};
let v1_router = Router::new()
.route("/keys", get(get_keys))
.route("/keysets", get(get_keysets))
.route("/keys/:keyset_id", get(get_keyset_pubkeys))
.route("/swap", post(post_swap))
.route("/mint/quote/bolt11", post(get_mint_bolt11_quote))
.route(
"/mint/quote/bolt11/:quote_id",
get(get_check_mint_bolt11_quote),
)
.route("/mint/bolt11", post(post_mint_bolt11))
.route("/melt/quote/bolt11", post(get_melt_bolt11_quote))
.route(
"/melt/quote/bolt11/:quote_id",
get(get_check_melt_bolt11_quote),
)
.route("/melt/bolt11", post(post_melt_bolt11))
.route("/checkstate", post(post_check))
.route("/info", get(get_mint_info))
.route("/restore", post(post_restore));
let mint_router = Router::new().nest("/v1", v1_router).with_state(state);
Ok(mint_router)
}
#[derive(Clone)]
struct MintState {
ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
mint: Arc<Mint>,
mint_url: String,
quote_ttl: u64,
}

View File

@@ -0,0 +1,402 @@
use std::str::FromStr;
use anyhow::Result;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use cdk::cdk_lightning::to_unit;
use cdk::error::{Error, ErrorResponse};
use cdk::nuts::nut05::MeltBolt11Response;
use cdk::nuts::{
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
};
use cdk::util::unix_time;
use cdk::Bolt11Invoice;
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| {
tracing::error!("Could not get keys: {}", err);
into_response(err)
})?;
Ok(Json(pubkeys))
}
pub async fn get_keyset_pubkeys(
State(state): State<MintState>,
Path(keyset_id): Path<Id>,
) -> Result<Json<KeysResponse>, Response> {
let pubkeys = state.mint.keyset_pubkeys(&keyset_id).await.map_err(|err| {
tracing::error!("Could not get keyset pubkeys: {}", err);
into_response(err)
})?;
Ok(Json(pubkeys))
}
pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetResponse>, Response> {
let mint = state.mint.keysets().await.map_err(|err| {
tracing::error!("Could not get keyset: {}", err);
into_response(err)
})?;
Ok(Json(mint))
}
pub async fn get_mint_bolt11_quote(
State(state): State<MintState>,
Json(payload): Json<MintQuoteBolt11Request>,
) -> Result<Json<MintQuoteBolt11Response>, Response> {
let amount =
to_unit(payload.amount, &payload.unit, &state.ln.get_base_unit()).map_err(|err| {
tracing::error!("Backed does not support unit: {}", err);
into_response(Error::UnsupportedUnit)
})?;
let quote_expiry = unix_time() + state.quote_ttl;
let create_invoice_response = state
.ln
.create_invoice(amount, "".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.into(),
create_invoice_response.request.to_string(),
payload.unit,
payload.amount,
quote_expiry,
create_invoice_response.request_lookup_id,
)
.await
.map_err(|err| {
tracing::error!("Could not create new mint quote: {}", err);
into_response(err)
})?;
Ok(Json(quote.into()))
}
pub async fn get_check_mint_bolt11_quote(
State(state): State<MintState>,
Path(quote_id): Path<String>,
) -> Result<Json<MintQuoteBolt11Response>, Response> {
let quote = state
.mint
.check_mint_quote(&quote_id)
.await
.map_err(|err| {
tracing::error!("Could not check mint quote {}: {}", quote_id, err);
into_response(err)
})?;
Ok(Json(quote))
}
pub async fn post_mint_bolt11(
State(state): State<MintState>,
Json(payload): Json<MintBolt11Request>,
) -> Result<Json<MintBolt11Response>, Response> {
let res = state
.mint
.process_mint_request(payload)
.await
.map_err(|err| {
tracing::error!("Could not process mint: {}", err);
into_response(err)
})?;
Ok(Json(res))
}
pub async fn get_melt_bolt11_quote(
State(state): State<MintState>,
Json(payload): Json<MeltQuoteBolt11Request>,
) -> Result<Json<MeltQuoteBolt11Response>, Response> {
let invoice_amount_msat = payload
.request
.amount_milli_satoshis()
.ok_or(Error::InvoiceAmountUndefined)
.map_err(into_response)?;
// Convert amount to quote unit
let amount =
to_unit(invoice_amount_msat, &CurrencyUnit::Msat, &payload.unit).map_err(|err| {
tracing::error!("Backed does not support unit: {}", err);
into_response(Error::UnsupportedUnit)
})?;
let payment_quote = state.ln.get_payment_quote(&payload).await.unwrap();
let quote = state
.mint
.new_melt_quote(
payload.request.to_string(),
payload.unit,
amount.into(),
payment_quote.fee.into(),
unix_time() + state.quote_ttl,
payment_quote.request_lookup_id,
)
.await
.map_err(|err| {
tracing::error!("Could not create melt quote: {}", err);
into_response(err)
})?;
Ok(Json(quote.into()))
}
pub async fn get_check_melt_bolt11_quote(
State(state): State<MintState>,
Path(quote_id): Path<String>,
) -> Result<Json<MeltQuoteBolt11Response>, Response> {
let quote = state
.mint
.check_melt_quote(&quote_id)
.await
.map_err(|err| {
tracing::error!("Could not check melt quote: {}", err);
into_response(err)
})?;
Ok(Json(quote))
}
pub async fn post_melt_bolt11(
State(state): State<MintState>,
Json(payload): Json<MeltBolt11Request>,
) -> Result<Json<MeltBolt11Response>, Response> {
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: {}", err);
}
return Err(into_response(Error::MeltRequestInvalid));
}
};
// Check to see if there is a corresponding mint quote for a melt.
// In this case the mint can settle the payment internally and no ln payment is needed
let mint_quote = match state
.mint
.localstore
.get_mint_quote_by_request(&quote.request)
.await
{
Ok(mint_quote) => mint_quote,
Err(err) => {
tracing::debug!("Error attempting to get mint quote: {}", 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::DatabaseError));
}
};
let inputs_amount_quote_unit = payload.proofs_amount();
let (preimage, amount_spent_quote_unit) = match mint_quote {
Some(mint_quote) => {
let mut mint_quote = mint_quote;
if mint_quote.amount > inputs_amount_quote_unit {
tracing::debug!(
"Not enough inuts provided: {} needed {}",
inputs_amount_quote_unit,
mint_quote.amount
);
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::InsufficientInputProofs));
}
mint_quote.state = MintQuoteState::Paid;
let amount = quote.amount;
if let Err(_err) = state.mint.update_mint_quote(mint_quote).await {
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::DatabaseError));
}
(None, amount)
}
None => {
let invoice = match Bolt11Invoice::from_str(&quote.request) {
Ok(bolt11) => bolt11,
Err(_) => {
tracing::error!("Melt quote has invalid payment request");
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::InvalidPaymentRequest));
}
};
let mut partial_msats = None;
let mut max_fee_msats = 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 id 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.
if quote.unit == CurrencyUnit::Msat || quote.unit == CurrencyUnit::Sat {
let quote_msats = to_unit(quote.amount, &quote.unit, &CurrencyUnit::Msat)
.expect("Quote unit is checked above that it can convert to msat");
let invoice_amount_msats = match invoice.amount_milli_satoshis() {
Some(amount) => amount,
None => {
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::InvoiceAmountUndefined));
}
};
partial_msats = match invoice_amount_msats > quote_msats {
true => Some(invoice_amount_msats - quote_msats),
false => None,
};
let max_fee = to_unit(quote.fee_reserve, &quote.unit, &CurrencyUnit::Msat)
.expect("Quote unit is checked above that it can convert to msat");
max_fee_msats = Some(max_fee);
let amount_to_pay_msats = match partial_msats {
Some(amount_to_pay) => amount_to_pay,
None => invoice_amount_msats,
};
let input_amount_msats =
to_unit(inputs_amount_quote_unit, &quote.unit, &CurrencyUnit::Msat)
.expect("Quote unit is checked above that it can convert to msat");
if amount_to_pay_msats + max_fee > input_amount_msats {
tracing::debug!(
"Not enough inuts provided: {} msats needed {} msats",
input_amount_msats,
amount_to_pay_msats
);
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::InsufficientInputProofs));
}
}
let pre = match state
.ln
.pay_invoice(quote.clone(), partial_msats, max_fee_msats)
.await
{
Ok(pay) => pay,
Err(err) => {
tracing::error!("Could not pay invoice: {}", 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::PaymentFailed));
}
};
let amount_spent = to_unit(
pre.total_spent_msats,
&state.ln.get_base_unit(),
&quote.unit,
)
.map_err(|_| into_response(Error::UnsupportedUnit))?;
(pre.payment_preimage, amount_spent.into())
}
};
let res = state
.mint
.process_melt_request(&payload, preimage, amount_spent_quote_unit)
.await
.map_err(|err| {
tracing::error!("Could not process melt request: {}", err);
into_response(err)
})?;
Ok(Json(res.into()))
}
pub async fn post_check(
State(state): State<MintState>,
Json(payload): Json<CheckStateRequest>,
) -> Result<Json<CheckStateResponse>, Response> {
let state = state.mint.check_state(&payload).await.map_err(|err| {
tracing::error!("Could not check state of proofs");
into_response(err)
})?;
Ok(Json(state))
}
pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintInfo>, Response> {
Ok(Json(state.mint.mint_info().clone()))
}
pub async fn post_swap(
State(state): State<MintState>,
Json(payload): Json<SwapRequest>,
) -> Result<Json<SwapResponse>, Response> {
let swap_response = state
.mint
.process_swap_request(payload)
.await
.map_err(|err| {
tracing::error!("Could not process swap request: {}", err);
into_response(err)
})?;
Ok(Json(swap_response))
}
pub async fn post_restore(
State(state): State<MintState>,
Json(payload): Json<RestoreRequest>,
) -> Result<Json<RestoreResponse>, Response> {
let restore_response = state.mint.restore(payload).await.map_err(|err| {
tracing::error!("Could not process restore: {}", err);
into_response(err)
})?;
Ok(Json(restore_response))
}
pub fn into_response<T>(error: T) -> Response
where
T: Into<ErrorResponse>,
{
(
StatusCode::INTERNAL_SERVER_ERROR,
Json::<ErrorResponse>(error.into()),
)
.into_response()
}

View File

@@ -24,7 +24,7 @@ tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
rand = "0.8.5" rand = "0.8.5"
home = "0.5.9" home.workspace = true
nostr-sdk = { version = "0.32.0", default-features = false, features = [ nostr-sdk = { version = "0.32.0", default-features = false, features = [
"nip04", "nip04",
"nip44" "nip44"

View File

@@ -8,6 +8,9 @@ pub enum Error {
/// Unknown invoice /// Unknown invoice
#[error("Unknown invoice")] #[error("Unknown invoice")]
UnknownInvoice, UnknownInvoice,
/// Invoice amount not defined
#[error("Unknown invoice amount")]
UnknownInvoiceAmount,
/// Cln Error /// Cln Error
#[error(transparent)] #[error(transparent)]
Cln(#[from] cln_rpc::Error), Cln(#[from] cln_rpc::Error),

View File

@@ -7,10 +7,13 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::cdk_lightning::{self, MintLightning, PayInvoiceResponse}; use cdk::cdk_lightning::{
use cdk::nuts::{MeltQuoteState, MintQuoteState}; self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
};
use cdk::mint::FeeReserve;
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
use cdk::util::{hex, unix_time}; use cdk::util::{hex, unix_time};
use cdk::Bolt11Invoice; use cdk::{mint, Bolt11Invoice};
use cln_rpc::model::requests::{ use cln_rpc::model::requests::{
InvoiceRequest, ListinvoicesRequest, PayRequest, WaitanyinvoiceRequest, InvoiceRequest, ListinvoicesRequest, PayRequest, WaitanyinvoiceRequest,
}; };
@@ -28,15 +31,17 @@ pub mod error;
pub struct Cln { pub struct Cln {
rpc_socket: PathBuf, rpc_socket: PathBuf,
cln_client: Arc<Mutex<cln_rpc::ClnRpc>>, cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
fee_reserve: FeeReserve,
} }
impl Cln { impl Cln {
pub async fn new(rpc_socket: PathBuf) -> Result<Self, Error> { pub async fn new(rpc_socket: PathBuf, fee_reserve: FeeReserve) -> Result<Self, Error> {
let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?; let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
Ok(Self { Ok(Self {
rpc_socket, rpc_socket,
cln_client: Arc::new(Mutex::new(cln_client)), cln_client: Arc::new(Mutex::new(cln_client)),
fee_reserve,
}) })
} }
} }
@@ -45,6 +50,10 @@ impl Cln {
impl MintLightning for Cln { impl MintLightning for Cln {
type Err = cdk_lightning::Error; type Err = cdk_lightning::Error;
fn get_base_unit(&self) -> CurrencyUnit {
CurrencyUnit::Msat
}
async fn wait_any_invoice( async fn wait_any_invoice(
&self, &self,
) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err> { ) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err> {
@@ -88,16 +97,47 @@ impl MintLightning for Cln {
.boxed()) .boxed())
} }
async fn get_payment_quote(
&self,
melt_quote_request: &MeltQuoteBolt11Request,
) -> Result<PaymentQuoteResponse, Self::Err> {
let invoice_amount_msat = melt_quote_request
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit(
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * amount as f32) as u64;
let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
let fee = match relative_fee_reserve > absolute_fee_reserve {
true => relative_fee_reserve,
false => absolute_fee_reserve,
};
Ok(PaymentQuoteResponse {
request_lookup_id: melt_quote_request.request.to_string(),
amount,
fee,
})
}
async fn pay_invoice( async fn pay_invoice(
&self, &self,
bolt11: Bolt11Invoice, melt_quote: mint::MeltQuote,
partial_msats: Option<u64>, partial_msats: Option<u64>,
max_fee_msats: Option<u64>, max_fee_msats: Option<u64>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<PayInvoiceResponse, Self::Err> {
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
let cln_response = cln_client let cln_response = cln_client
.call(Request::Pay(PayRequest { .call(Request::Pay(PayRequest {
bolt11: bolt11.to_string(), bolt11: melt_quote.request.to_string(),
amount_msat: None, amount_msat: None,
label: None, label: None,
riskfactor: None, riskfactor: None,
@@ -142,12 +182,13 @@ impl MintLightning for Cln {
amount_msats: u64, amount_msats: u64,
description: String, description: String,
unix_expiry: u64, unix_expiry: u64,
) -> Result<Bolt11Invoice, Self::Err> { ) -> Result<CreateInvoiceResponse, Self::Err> {
let time_now = unix_time(); let time_now = unix_time();
assert!(unix_expiry > time_now); assert!(unix_expiry > time_now);
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
let label = Uuid::new_v4().to_string();
let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount_msats)); let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount_msats));
let cln_response = cln_client let cln_response = cln_client
.call(cln_rpc::Request::Invoice(InvoiceRequest { .call(cln_rpc::Request::Invoice(InvoiceRequest {
@@ -164,26 +205,28 @@ impl MintLightning for Cln {
.await .await
.map_err(Error::from)?; .map_err(Error::from)?;
let invoice = match cln_response { match cln_response {
cln_rpc::Response::Invoice(invoice_res) => { cln_rpc::Response::Invoice(invoice_res) => Ok(CreateInvoiceResponse {
Bolt11Invoice::from_str(&invoice_res.bolt11)? request_lookup_id: label,
} request: Bolt11Invoice::from_str(&invoice_res.bolt11)?,
}),
_ => { _ => {
tracing::warn!("CLN returned wrong response kind"); tracing::warn!("CLN returned wrong response kind");
return Err(Error::WrongClnResponse.into()); Err(Error::WrongClnResponse.into())
} }
}; }
Ok(invoice)
} }
async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err> { async fn check_invoice_status(
&self,
request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err> {
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
let cln_response = cln_client let cln_response = cln_client
.call(Request::ListInvoices(ListinvoicesRequest { .call(Request::ListInvoices(ListinvoicesRequest {
payment_hash: Some(payment_hash.to_string()), payment_hash: None,
label: None, label: Some(request_lookup_id.to_string()),
invstring: None, invstring: None,
offer_id: None, offer_id: None,
index: None, index: None,
@@ -201,8 +244,8 @@ impl MintLightning for Cln {
} }
None => { None => {
tracing::info!( tracing::info!(
"Check invoice called on unknown payment_hash: {}", "Check invoice called on unknown look up id: {}",
payment_hash request_lookup_id
); );
return Err(Error::WrongClnResponse.into()); return Err(Error::WrongClnResponse.into());
} }

View File

@@ -0,0 +1,30 @@
[package]
name = "cdk-mintd"
version = "0.1.0"
edition = "2021"
authors = ["CDK Developers"]
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true # MSRV
license.workspace = true
[dependencies]
anyhow = "1.0.75"
axum = "0.7.5"
axum-macros = "0.4.1"
cdk = { workspace = true, default-features = false, features = ["mint"] }
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-axum = { workspace = true, default-features = false }
config = { version = "0.13.3", features = ["toml"] }
clap = { version = "4.4.8", features = ["derive", "env", "default"] }
tokio.workspace = true
tracing.workspace = true
tracing-subscriber = "0.3.18"
futures = "0.3.28"
serde.workspace = true
bip39.workspace = true
tower-http = { version = "0.5.2", features = ["cors"] }
lightning-invoice.workspace = true
home.workspace = true

View File

@@ -0,0 +1,40 @@
[info]
url = "https://mint.thesimplekid.dev/"
listen_host = "127.0.0.1"
listen_port = 8085
mnemonic = ""
[mint_info]
# name = "cdk-mintd mutiney net mint"
# Hex publey of mint
# pubkey = ""
# description = "These are not real sats for testing only"
# description_long = "A longer mint for testing"
# motd = "Hello world"
# contact_email = "hello@cashu.me"
# Nostr pubkey of mint (Hex)
# contact_nostr_public_key = ""
[database]
# Database engine (sqlite/redb) defaults to sqlite
# engine = "sqlite"
[ln]
# Required ln backend `cln`
ln_backend = "cln"
# CLN
# Required if using cln backend path to rpc
cln_path = ""
# Required to start greenlight for the first time
# greenlight_invite_code = ""
# Fee reserve for melting as a percent of payment amount
fee_percent = 1.0
# Fee reserve for melting as an absolute value
reserve_fee_min = 1000

View File

@@ -0,0 +1,24 @@
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser)]
#[command(about = "A cashu mint written in rust", author = env!("CARGO_PKG_AUTHORS"), version = env!("CARGO_PKG_VERSION"))]
pub struct CLIArgs {
#[arg(
short,
long,
help = "Use the <directory> as the location of the database",
required = false
)]
pub work_dir: Option<PathBuf>,
#[arg(
short,
long,
help = "Use the <file name> as the location of the config file",
required = false
)]
pub config: Option<PathBuf>,
#[arg(short, long, help = "Recover Greenlight from seed", required = false)]
pub recover: Option<String>,
}

View File

@@ -0,0 +1,123 @@
use std::path::PathBuf;
use cdk::nuts::PublicKey;
use cdk::Amount;
use config::{Config, ConfigError, File};
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Info {
pub url: String,
pub listen_host: String,
pub listen_port: u16,
pub mnemonic: String,
pub seconds_quote_is_valid_for: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum LnBackend {
#[default]
Cln,
// Greenlight,
// Ldk,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Ln {
pub ln_backend: LnBackend,
pub cln_path: Option<PathBuf>,
pub greenlight_invite_code: Option<String>,
pub invoice_description: Option<String>,
pub fee_percent: f32,
pub reserve_fee_min: Amount,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum DatabaseEngine {
#[default]
Sqlite,
Redb,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Database {
pub engine: DatabaseEngine,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Settings {
pub info: Info,
pub mint_info: MintInfo,
pub ln: Ln,
pub database: Database,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MintInfo {
/// name of the mint and should be recognizable
pub name: Option<String>,
/// hex pubkey of the mint
pub pubkey: Option<PublicKey>,
/// short description of the mint
pub description: Option<String>,
/// long description
pub description_long: Option<String>,
/// message of the day that the wallet must display to the user
pub motd: Option<String>,
/// Nostr publickey
pub contact_nostr_public_key: Option<String>,
/// Contact email
pub contact_email: Option<String>,
}
impl Settings {
#[must_use]
pub fn new(config_file_name: &Option<PathBuf>) -> Self {
let default_settings = Self::default();
// attempt to construct settings with file
let from_file = Self::new_from_default(&default_settings, config_file_name);
match from_file {
Ok(f) => f,
Err(e) => {
warn!("Error reading config file ({:?})", e);
default_settings
}
}
}
fn new_from_default(
default: &Settings,
config_file_name: &Option<PathBuf>,
) -> Result<Self, ConfigError> {
let mut default_config_file_name = home::home_dir()
.ok_or(ConfigError::NotFound("Config Path".to_string()))?
.join("cashu-rs-mint");
default_config_file_name.push("config.toml");
let config: String = match config_file_name {
Some(value) => value.clone().to_string_lossy().to_string(),
None => default_config_file_name.to_string_lossy().to_string(),
};
let builder = Config::builder();
let config: Config = builder
// use defaults
.add_source(Config::try_from(default)?)
// override with file contents
.add_source(File::with_name(&config))
.build()?;
let settings: Settings = config.try_deserialize()?;
debug!("{settings:?}");
match settings.ln.ln_backend {
LnBackend::Cln => assert!(settings.ln.cln_path.is_some()),
//LnBackend::Greenlight => (),
//LnBackend::Ldk => (),
}
Ok(settings)
}
}

View File

@@ -0,0 +1,253 @@
//! CDK Mint Server
#![warn(missing_docs)]
#![warn(rustdoc::bare_urls)]
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use axum::Router;
use bip39::Mnemonic;
use cdk::cdk_database::{self, MintDatabase};
use cdk::cdk_lightning;
use cdk::cdk_lightning::MintLightning;
use cdk::mint::{FeeReserve, Mint};
use cdk::nuts::{ContactInfo, MintInfo, MintVersion, Nuts};
use cdk_cln::Cln;
use cdk_redb::MintRedbDatabase;
use cdk_sqlite::MintSqliteDatabase;
use clap::Parser;
use cli::CLIArgs;
use config::{DatabaseEngine, LnBackend};
use futures::StreamExt;
use tower_http::cors::CorsLayer;
mod cli;
mod config;
const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
const DEFAULT_QUOTE_TTL_SECS: u64 = 1800;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
let args = CLIArgs::parse();
let work_dir = match args.work_dir {
Some(w) => w,
None => work_dir()?,
};
// get config file name from args
let config_file_arg = match args.config {
Some(c) => c,
None => work_dir.join("config.toml"),
};
let settings = config::Settings::new(&Some(config_file_arg));
let localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync> =
match settings.database.engine {
DatabaseEngine::Sqlite => {
let sql_db_path = work_dir.join("cdk-mintd.sqlite");
let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?;
sqlite_db.migrate().await;
Arc::new(sqlite_db)
}
DatabaseEngine::Redb => {
let redb_path = work_dir.join("cdk-mintd.redb");
Arc::new(MintRedbDatabase::new(&redb_path)?)
}
};
let mut contact_info: Option<Vec<ContactInfo>> = None;
if let Some(nostr_contact) = settings.mint_info.contact_nostr_public_key {
let nostr_contact = ContactInfo::new("nostr".to_string(), nostr_contact);
contact_info = match contact_info {
Some(mut vec) => {
vec.push(nostr_contact);
Some(vec)
}
None => Some(vec![nostr_contact]),
};
}
if let Some(email_contact) = settings.mint_info.contact_email {
let email_contact = ContactInfo::new("email".to_string(), email_contact);
contact_info = match contact_info {
Some(mut vec) => {
vec.push(email_contact);
Some(vec)
}
None => Some(vec![email_contact]),
};
}
let mint_version = MintVersion::new(
"cdk-mintd".to_string(),
CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
);
let mint_info = MintInfo::new(
settings.mint_info.name,
settings.mint_info.pubkey,
Some(mint_version),
settings.mint_info.description,
settings.mint_info.description_long,
contact_info,
Nuts::default(),
settings.mint_info.motd,
);
let relative_ln_fee = settings.ln.fee_percent;
let absolute_ln_fee_reserve = settings.ln.reserve_fee_min;
let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
let fee_reserve = FeeReserve {
min_fee_reserve: absolute_ln_fee_reserve,
percent_fee_reserve: relative_ln_fee,
};
let mint = Mint::new(
&settings.info.url,
&mnemonic.to_seed_normalized(""),
mint_info,
localstore,
absolute_ln_fee_reserve,
relative_ln_fee,
)
.await?;
let ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync> =
match settings.ln.ln_backend {
LnBackend::Cln => {
let cln_socket = expand_path(
settings
.ln
.cln_path
.clone()
.ok_or(anyhow!("cln socket not defined"))?
.to_str()
.ok_or(anyhow!("cln socket not defined"))?,
)
.ok_or(anyhow!("cln socket not defined"))?;
Arc::new(Cln::new(cln_socket, fee_reserve).await?)
}
};
let mint = Arc::new(mint);
// Check the status of any mint quotes that are pending
// In the event that the mint server is down but the ln node is not
// it is possible that a mint quote was paid but the mint has not been updated
// this will check and update the mint state of those quotes
check_pending_quotes(Arc::clone(&mint), Arc::clone(&ln)).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
.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), Arc::clone(&ln), quote_ttl)
.await?;
let mint_service = Router::new()
.nest("/", v1_service)
.layer(CorsLayer::permissive());
// Spawn task to wait for invoces to be paid and update mint quotes
tokio::spawn(async move {
loop {
match ln.wait_any_invoice().await {
Ok(mut stream) => {
while let Some(invoice) = stream.next().await {
if let Err(err) =
handle_paid_invoice(Arc::clone(&mint), &invoice.to_string()).await
{
tracing::warn!("{:?}", err);
}
}
}
Err(err) => {
tracing::warn!("Could not get invoice stream: {}", err);
}
}
}
});
let listener =
tokio::net::TcpListener::bind(format!("{}:{}", listen_addr, listen_port)).await?;
axum::serve(listener, mint_service).await?;
Ok(())
}
/// Update mint quote when called for a paid invoice
async fn handle_paid_invoice(mint: Arc<Mint>, request: &str) -> Result<()> {
if let Ok(Some(mint_quote)) = mint.localstore.get_mint_quote_by_request(request).await {
mint.localstore
.update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
.await?;
}
Ok(())
}
/// Used on mint start up to check status of all pending mint quotes
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?;
for quote in pending_quotes {
let lookup_id = quote.request_lookup_id;
let state = ln.check_invoice_status(&lookup_id).await?;
if state != quote.state {
mint.localstore
.update_mint_quote_state(&quote.id, state)
.await?;
}
}
Ok(())
}
fn expand_path(path: &str) -> Option<PathBuf> {
if path.starts_with('~') {
if let Some(home_dir) = home::home_dir().as_mut() {
let remainder = &path[2..];
home_dir.push(remainder);
let expanded_path = home_dir;
Some(expanded_path.clone())
} else {
None
}
} else {
Some(PathBuf::from(path))
}
}
fn work_dir() -> Result<PathBuf> {
let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
Ok(home_dir.join(".cdk-mintd"))
}

View File

@@ -24,3 +24,4 @@ thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
lightning-invoice.workspace = true

View File

@@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState}; use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
use cdk::types::{MeltQuote, MintQuote};
use cdk::{Amount, UncheckedUrl}; use cdk::{Amount, UncheckedUrl};
use redb::{Database, ReadableTable, TableDefinition}; use redb::{Database, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -24,18 +23,51 @@ pub struct V0MintQuote {
pub expiry: u64, pub expiry: u64,
} }
impl From<V0MintQuote> for MintQuote { /// Mint Quote Info
fn from(quote: V0MintQuote) -> MintQuote { #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct V1MintQuote {
pub id: String,
pub mint_url: UncheckedUrl,
pub amount: Amount,
pub unit: CurrencyUnit,
pub request: String,
pub state: MintQuoteState,
pub expiry: u64,
}
/// Melt Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct V1MeltQuote {
/// Quote id
pub id: String,
/// Quote unit
pub unit: CurrencyUnit,
/// Quote amount
pub amount: Amount,
/// Quote Payment request e.g. bolt11
pub request: String,
/// Quote fee reserve
pub fee_reserve: Amount,
/// Quote state
pub state: MeltQuoteState,
/// Expiration time of quote
pub expiry: u64,
/// Payment preimage
pub payment_preimage: Option<String>,
}
impl From<V0MintQuote> for V1MintQuote {
fn from(quote: V0MintQuote) -> V1MintQuote {
let state = match quote.paid { let state = match quote.paid {
true => MintQuoteState::Paid, true => MintQuoteState::Paid,
false => MintQuoteState::Unpaid, false => MintQuoteState::Unpaid,
}; };
MintQuote { V1MintQuote {
id: quote.id, id: quote.id,
mint_url: quote.mint_url, mint_url: quote.mint_url,
amount: quote.amount, amount: quote.amount,
unit: quote.unit, unit: quote.unit,
request: quote.request, request: quote.request.clone(),
state, state,
expiry: quote.expiry, expiry: quote.expiry,
} }
@@ -54,13 +86,13 @@ pub struct V0MeltQuote {
pub expiry: u64, pub expiry: u64,
} }
impl From<V0MeltQuote> for MeltQuote { impl From<V0MeltQuote> for V1MeltQuote {
fn from(quote: V0MeltQuote) -> MeltQuote { fn from(quote: V0MeltQuote) -> V1MeltQuote {
let state = match quote.paid { let state = match quote.paid {
true => MeltQuoteState::Paid, true => MeltQuoteState::Paid,
false => MeltQuoteState::Unpaid, false => MeltQuoteState::Unpaid,
}; };
MeltQuote { V1MeltQuote {
id: quote.id, id: quote.id,
amount: quote.amount, amount: quote.amount,
unit: quote.unit, unit: quote.unit,
@@ -94,7 +126,7 @@ fn migrate_mint_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
.collect(); .collect();
} }
let migrated_mint_quotes: HashMap<String, Option<MintQuote>> = mint_quotes let migrated_mint_quotes: HashMap<String, Option<V1MintQuote>> = mint_quotes
.into_iter() .into_iter()
.map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into()))) .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
.collect(); .collect();
@@ -147,7 +179,7 @@ fn migrate_melt_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
.collect(); .collect();
} }
let migrated_melt_quotes: HashMap<String, Option<MeltQuote>> = melt_quotes let migrated_melt_quotes: HashMap<String, Option<V1MeltQuote>> = melt_quotes
.into_iter() .into_iter()
.map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into()))) .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
.collect(); .collect();

View File

@@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use cdk::mint::MintQuote;
use cdk::nuts::{CurrencyUnit, MintQuoteState};
use cdk::{Amount, UncheckedUrl};
use lightning_invoice::Bolt11Invoice;
use redb::{Database, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize};
use super::Error;
const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
pub fn migrate_01_to_02(db: Arc<Database>) -> Result<u32, Error> {
migrate_mint_quotes_01_to_02(db)?;
Ok(2)
}
/// Mint Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
struct V1MintQuote {
pub id: String,
pub mint_url: UncheckedUrl,
pub amount: Amount,
pub unit: CurrencyUnit,
pub request: String,
pub state: MintQuoteState,
pub expiry: u64,
}
impl From<V1MintQuote> for MintQuote {
fn from(quote: V1MintQuote) -> MintQuote {
MintQuote {
id: quote.id,
mint_url: quote.mint_url,
amount: quote.amount,
unit: quote.unit,
request: quote.request.clone(),
state: quote.state,
expiry: quote.expiry,
request_lookup_id: Bolt11Invoice::from_str(&quote.request).unwrap().to_string(),
}
}
}
fn migrate_mint_quotes_01_to_02(db: Arc<Database>) -> Result<(), Error> {
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn
.open_table(MINT_QUOTES_TABLE)
.map_err(Error::from)?;
let mint_quotes: HashMap<String, Option<V1MintQuote>>;
{
mint_quotes = table
.iter()
.map_err(Error::from)?
.flatten()
.map(|(quote_id, mint_quote)| {
(
quote_id.value().to_string(),
serde_json::from_str(mint_quote.value()).ok(),
)
})
.collect();
}
let migrated_mint_quotes: HashMap<String, Option<MintQuote>> = mint_quotes
.into_iter()
.map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
.collect();
{
let write_txn = db.begin_write()?;
{
let mut table = write_txn
.open_table(MINT_QUOTES_TABLE)
.map_err(Error::from)?;
for (quote_id, quote) in migrated_mint_quotes {
match quote {
Some(quote) => {
let quote_str = serde_json::to_string(&quote)?;
table.insert(quote_id.as_str(), quote_str.as_str())?;
}
None => {
table.remove(quote_id.as_str())?;
}
}
}
}
write_txn.commit()?;
}
Ok(())
}

View File

@@ -7,21 +7,23 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::cdk_database;
use cdk::cdk_database::MintDatabase; use cdk::cdk_database::MintDatabase;
use cdk::dhke::hash_to_curve; use cdk::dhke::hash_to_curve;
use cdk::mint::MintKeySetInfo; use cdk::mint::{MintKeySetInfo, MintQuote};
use cdk::nuts::{ use cdk::nuts::{
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey, BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey,
}; };
use cdk::secret::Secret; use cdk::secret::Secret;
use cdk::types::{MeltQuote, MintQuote}; use cdk::{cdk_database, mint};
use migrations::migrate_01_to_02;
use redb::{Database, ReadableTable, TableDefinition}; use redb::{Database, ReadableTable, TableDefinition};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::error::Error; use super::error::Error;
use crate::migrations::migrate_00_to_01; use crate::migrations::migrate_00_to_01;
mod migrations;
const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets"); const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets");
const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets"); const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets");
const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes"); const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
@@ -34,7 +36,7 @@ const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config")
const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> = const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> =
TableDefinition::new("blinded_signatures"); TableDefinition::new("blinded_signatures");
const DATABASE_VERSION: u32 = 1; const DATABASE_VERSION: u32 = 2;
/// Mint Redbdatabase /// Mint Redbdatabase
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -72,6 +74,10 @@ impl MintRedbDatabase {
current_file_version = migrate_00_to_01(Arc::clone(&db))?; current_file_version = migrate_00_to_01(Arc::clone(&db))?;
} }
if current_file_version == 1 {
current_file_version = migrate_01_to_02(Arc::clone(&db))?;
}
if current_file_version != DATABASE_VERSION { if current_file_version != DATABASE_VERSION {
tracing::warn!( tracing::warn!(
"Database upgrade did not complete at {} current is {}", "Database upgrade did not complete at {} current is {}",
@@ -169,7 +175,7 @@ impl MintDatabase for MintRedbDatabase {
let mut active_keysets = HashMap::new(); let mut active_keysets = HashMap::new();
for (unit, id) in (table.iter().map_err(Error::from)?).flatten() { for (unit, id) in (table.iter().map_err(Error::from)?).flatten() {
let unit = CurrencyUnit::from(unit.value()); let unit = CurrencyUnit::from_str(unit.value())?;
let id = Id::from_str(id.value()).map_err(Error::from)?; let id = Id::from_str(id.value()).map_err(Error::from)?;
active_keysets.insert(unit, id); active_keysets.insert(unit, id);
@@ -309,6 +315,21 @@ impl MintDatabase for MintRedbDatabase {
Ok(current_state) Ok(current_state)
} }
async fn get_mint_quote_by_request(
&self,
request: &str,
) -> Result<Option<MintQuote>, Self::Err> {
let quotes = self.get_mint_quotes().await?;
let quote = quotes
.into_iter()
.filter(|q| q.request.eq(request))
.collect::<Vec<MintQuote>>()
.first()
.cloned();
Ok(quote)
}
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> { async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
@@ -344,7 +365,7 @@ impl MintDatabase for MintRedbDatabase {
Ok(()) Ok(())
} }
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?; let write_txn = db.begin_write().map_err(Error::from)?;
@@ -365,7 +386,7 @@ impl MintDatabase for MintRedbDatabase {
Ok(()) Ok(())
} }
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> { async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?; let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn let table = read_txn
@@ -383,7 +404,7 @@ impl MintDatabase for MintRedbDatabase {
state: MeltQuoteState, state: MeltQuoteState,
) -> Result<MeltQuoteState, Self::Err> { ) -> Result<MeltQuoteState, Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
let mut melt_quote: MeltQuote; let mut melt_quote: mint::MeltQuote;
{ {
let read_txn = db.begin_read().map_err(Error::from)?; let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn let table = read_txn
@@ -423,7 +444,7 @@ impl MintDatabase for MintRedbDatabase {
Ok(current_state) Ok(current_state)
} }
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> { async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?; let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn let table = read_txn

View File

@@ -7,14 +7,15 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::cdk_database;
use cdk::cdk_database::WalletDatabase; use cdk::cdk_database::WalletDatabase;
use cdk::nuts::{ use cdk::nuts::{
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
}; };
use cdk::types::{MeltQuote, MintQuote, ProofInfo}; use cdk::types::ProofInfo;
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::util::unix_time; use cdk::util::unix_time;
use cdk::wallet::MintQuote;
use cdk::{cdk_database, wallet};
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::instrument; use tracing::instrument;
@@ -432,7 +433,7 @@ impl WalletDatabase for WalletRedbDatabase {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?; let write_txn = db.begin_write().map_err(Error::from)?;
@@ -454,7 +455,7 @@ impl WalletDatabase for WalletRedbDatabase {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> { async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
let db = self.db.lock().await; let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?; let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn let table = read_txn

View File

@@ -9,9 +9,10 @@ use cdk::cdk_database::WalletDatabase;
use cdk::nuts::{ use cdk::nuts::{
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
}; };
use cdk::types::{MeltQuote, MintQuote, ProofInfo}; use cdk::types::ProofInfo;
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::util::unix_time; use cdk::util::unix_time;
use cdk::wallet::{MeltQuote, MintQuote};
use rexie::*; use rexie::*;
use thiserror::Error; use thiserror::Error;
use tokio::sync::Mutex; use tokio::sync::Mutex;

View File

@@ -28,3 +28,4 @@ tokio = { workspace = true, features = [
] } ] }
tracing.workspace = true tracing.workspace = true
serde_json.workspace = true serde_json.workspace = true
lightning-invoice.workspace = true

View File

@@ -8,6 +8,9 @@ pub enum Error {
/// SQLX Error /// SQLX Error
#[error(transparent)] #[error(transparent)]
SQLX(#[from] sqlx::Error), SQLX(#[from] sqlx::Error),
/// NUT00 Error
#[error(transparent)]
CDKNUT00(#[from] cdk::nuts::nut00::Error),
/// NUT01 Error /// NUT01 Error
#[error(transparent)] #[error(transparent)]
CDKNUT01(#[from] cdk::nuts::nut01::Error), CDKNUT01(#[from] cdk::nuts::nut01::Error),

View File

@@ -42,6 +42,7 @@ CREATE TABLE IF NOT EXISTS mint_quote (
CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid); CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid);
CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request); CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request);
CREATE INDEX IF NOT EXISTS expiry_index ON mint_quote(expiry);
CREATE TABLE IF NOT EXISTS melt_quote ( CREATE TABLE IF NOT EXISTS melt_quote (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@@ -55,6 +56,7 @@ CREATE TABLE IF NOT EXISTS melt_quote (
CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid); CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid);
CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request); CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request);
CREATE INDEX IF NOT EXISTS expiry_index ON melt_quote(expiry);
CREATE TABLE IF NOT EXISTS blind_signature ( CREATE TABLE IF NOT EXISTS blind_signature (
y BLOB PRIMARY KEY, y BLOB PRIMARY KEY,

View File

@@ -0,0 +1,2 @@
ALTER TABLE mint_quote ADD request_lookup_id TEXT;
ALTER TABLE melt_quote ADD request_lookup_id TEXT;

View File

@@ -7,15 +7,15 @@ use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use bitcoin::bip32::DerivationPath; use bitcoin::bip32::DerivationPath;
use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_database::{self, MintDatabase};
use cdk::mint::MintKeySetInfo; use cdk::mint::{MintKeySetInfo, MintQuote};
use cdk::nuts::nut05::QuoteState; use cdk::nuts::nut05::QuoteState;
use cdk::nuts::{ use cdk::nuts::{
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
}; };
use cdk::secret::Secret; use cdk::secret::Secret;
use cdk::types::{MeltQuote, MintQuote}; use cdk::{mint, Amount};
use cdk::Amount;
use error::Error; use error::Error;
use lightning_invoice::Bolt11Invoice;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
use sqlx::{ConnectOptions, Row}; use sqlx::{ConnectOptions, Row};
@@ -116,7 +116,10 @@ WHERE active = 1
let keysets = recs let keysets = recs
.iter() .iter()
.filter_map(|r| match Id::from_str(r.get("id")) { .filter_map(|r| match Id::from_str(r.get("id")) {
Ok(id) => Some((CurrencyUnit::from(r.get::<'_, &str, &str>("unit")), id)), Ok(id) => Some((
CurrencyUnit::from_str(r.get::<'_, &str, &str>("unit")).unwrap(),
id,
)),
Err(_) => None, Err(_) => None,
}) })
.collect(); .collect();
@@ -128,8 +131,8 @@ WHERE active = 1
sqlx::query( sqlx::query(
r#" r#"
INSERT OR REPLACE INTO mint_quote INSERT OR REPLACE INTO mint_quote
(id, mint_url, amount, unit, request, state, expiry) (id, mint_url, amount, unit, request, state, expiry, request_lookup_id)
VALUES (?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
) )
.bind(quote.id.to_string()) .bind(quote.id.to_string())
@@ -139,6 +142,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
.bind(quote.request) .bind(quote.request)
.bind(quote.state.to_string()) .bind(quote.state.to_string())
.bind(quote.expiry as i64) .bind(quote.expiry as i64)
.bind(quote.request_lookup_id)
.execute(&self.pool) .execute(&self.pool)
.await .await
// TODO: should check if error is not found and return none // TODO: should check if error is not found and return none
@@ -169,6 +173,31 @@ WHERE id=?;
Ok(Some(sqlite_row_to_mint_quote(rec)?)) Ok(Some(sqlite_row_to_mint_quote(rec)?))
} }
async fn get_mint_quote_by_request(
&self,
request: &str,
) -> Result<Option<MintQuote>, Self::Err> {
let rec = sqlx::query(
r#"
SELECT *
FROM mint_quote
WHERE request=?;
"#,
)
.bind(request)
.fetch_one(&self.pool)
.await;
let rec = match rec {
Ok(rec) => rec,
Err(err) => match err {
sqlx::Error::RowNotFound => return Ok(None),
_ => return Err(Error::SQLX(err).into()),
},
};
Ok(Some(sqlite_row_to_mint_quote(rec)?))
}
async fn update_mint_quote_state( async fn update_mint_quote_state(
&self, &self,
quote_id: &str, quote_id: &str,
@@ -236,12 +265,12 @@ WHERE id=?
Ok(()) Ok(())
} }
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
sqlx::query( sqlx::query(
r#" r#"
INSERT OR REPLACE INTO melt_quote INSERT OR REPLACE INTO melt_quote
(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage) (id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
) )
.bind(quote.id.to_string()) .bind(quote.id.to_string())
@@ -252,13 +281,14 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
.bind(quote.state.to_string()) .bind(quote.state.to_string())
.bind(quote.expiry as i64) .bind(quote.expiry as i64)
.bind(quote.payment_preimage) .bind(quote.payment_preimage)
.bind(quote.request_lookup_id)
.execute(&self.pool) .execute(&self.pool)
.await .await
.map_err(Error::from)?; .map_err(Error::from)?;
Ok(()) Ok(())
} }
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> { async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err> {
let rec = sqlx::query( let rec = sqlx::query(
r#" r#"
SELECT * SELECT *
@@ -280,7 +310,7 @@ WHERE id=?;
Ok(Some(sqlite_row_to_melt_quote(rec)?)) Ok(Some(sqlite_row_to_melt_quote(rec)?))
} }
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> { async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
let rec = sqlx::query( let rec = sqlx::query(
r#" r#"
SELECT * SELECT *
@@ -661,7 +691,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
Ok(MintKeySetInfo { Ok(MintKeySetInfo {
id: Id::from_str(&row_id).map_err(Error::from)?, id: Id::from_str(&row_id).map_err(Error::from)?,
unit: CurrencyUnit::from(&row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
active: row_active, active: row_active,
valid_from: row_valid_from as u64, valid_from: row_valid_from as u64,
valid_to: row_valid_to.map(|v| v as u64), valid_to: row_valid_to.map(|v| v as u64),
@@ -678,19 +708,30 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
let row_request: String = row.try_get("request").map_err(Error::from)?; let row_request: String = row.try_get("request").map_err(Error::from)?;
let row_state: String = row.try_get("state").map_err(Error::from)?; let row_state: String = row.try_get("state").map_err(Error::from)?;
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
let row_request_lookup_id: Option<String> =
row.try_get("request_lookup_id").map_err(Error::from)?;
let request_lookup_id = match row_request_lookup_id {
Some(id) => id,
None => match Bolt11Invoice::from_str(&row_request) {
Ok(invoice) => invoice.payment_hash().to_string(),
Err(_) => row_request.clone(),
},
};
Ok(MintQuote { Ok(MintQuote {
id: row_id, id: row_id,
mint_url: row_mint_url.into(), mint_url: row_mint_url.into(),
amount: Amount::from(row_amount as u64), amount: Amount::from(row_amount as u64),
unit: CurrencyUnit::from(row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
request: row_request, request: row_request,
state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, state: MintQuoteState::from_str(&row_state).map_err(Error::from)?,
expiry: row_expiry as u64, expiry: row_expiry as u64,
request_lookup_id,
}) })
} }
fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> { fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<mint::MeltQuote, Error> {
let row_id: String = row.try_get("id").map_err(Error::from)?; let row_id: String = row.try_get("id").map_err(Error::from)?;
let row_unit: String = row.try_get("unit").map_err(Error::from)?; let row_unit: String = row.try_get("unit").map_err(Error::from)?;
let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
@@ -699,16 +740,21 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
let row_state: String = row.try_get("state").map_err(Error::from)?; let row_state: String = row.try_get("state").map_err(Error::from)?;
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?; let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
let row_request_lookup: Option<String> =
row.try_get("request_lookup_id").map_err(Error::from)?;
Ok(MeltQuote { let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone());
Ok(mint::MeltQuote {
id: row_id, id: row_id,
amount: Amount::from(row_amount as u64), amount: Amount::from(row_amount as u64),
unit: CurrencyUnit::from(row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
request: row_request, request: row_request,
fee_reserve: Amount::from(row_fee_reserve as u64), fee_reserve: Amount::from(row_fee_reserve as u64),
state: QuoteState::from_str(&row_state)?, state: QuoteState::from_str(&row_state)?,
expiry: row_expiry as u64, expiry: row_expiry as u64,
payment_preimage: row_preimage, payment_preimage: row_preimage,
request_lookup_id,
}) })
} }

View File

@@ -14,6 +14,9 @@ pub enum Error {
/// Wallet Error /// Wallet Error
#[error(transparent)] #[error(transparent)]
CDKWallet(#[from] cdk::wallet::error::Error), CDKWallet(#[from] cdk::wallet::error::Error),
/// NUT00 Error
#[error(transparent)]
CDKNUT00(#[from] cdk::nuts::nut00::Error),
/// NUT01 Error /// NUT01 Error
#[error(transparent)] #[error(transparent)]
CDKNUT01(#[from] cdk::nuts::nut01::Error), CDKNUT01(#[from] cdk::nuts::nut01::Error),

View File

@@ -5,15 +5,17 @@ use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::amount::Amount;
use cdk::cdk_database::{self, WalletDatabase}; use cdk::cdk_database::{self, WalletDatabase};
use cdk::nuts::{ use cdk::nuts::{
CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs, CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs,
PublicKey, SpendingConditions, State, PublicKey, SpendingConditions, State,
}; };
use cdk::secret::Secret; use cdk::secret::Secret;
use cdk::types::{MeltQuote, MintQuote, ProofInfo}; use cdk::types::ProofInfo;
use cdk::url::UncheckedUrl; use cdk::url::UncheckedUrl;
use cdk::Amount; use cdk::wallet;
use cdk::wallet::MintQuote;
use error::Error; use error::Error;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
use sqlx::{ConnectOptions, Row}; use sqlx::{ConnectOptions, Row};
@@ -350,7 +352,7 @@ WHERE id=?
Ok(()) Ok(())
} }
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
sqlx::query( sqlx::query(
r#" r#"
INSERT OR REPLACE INTO melt_quote INSERT OR REPLACE INTO melt_quote
@@ -371,7 +373,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
Ok(()) Ok(())
} }
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> { async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
let rec = sqlx::query( let rec = sqlx::query(
r#" r#"
SELECT * SELECT *
@@ -709,7 +711,7 @@ fn sqlite_row_to_keyset(row: &SqliteRow) -> Result<KeySetInfo, Error> {
Ok(KeySetInfo { Ok(KeySetInfo {
id: Id::from_str(&row_id)?, id: Id::from_str(&row_id)?,
unit: CurrencyUnit::from(row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
active, active,
}) })
} }
@@ -729,14 +731,14 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
id: row_id, id: row_id,
mint_url: row_mint_url.into(), mint_url: row_mint_url.into(),
amount: Amount::from(row_amount as u64), amount: Amount::from(row_amount as u64),
unit: CurrencyUnit::from(row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
request: row_request, request: row_request,
state, state,
expiry: row_expiry as u64, expiry: row_expiry as u64,
}) })
} }
fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<MeltQuote, Error> { fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<wallet::MeltQuote, Error> {
let row_id: String = row.try_get("id").map_err(Error::from)?; let row_id: String = row.try_get("id").map_err(Error::from)?;
let row_unit: String = row.try_get("unit").map_err(Error::from)?; let row_unit: String = row.try_get("unit").map_err(Error::from)?;
let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
@@ -747,10 +749,10 @@ fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<MeltQuote, Error> {
let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?; let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
let state = MeltQuoteState::from_str(&row_state)?; let state = MeltQuoteState::from_str(&row_state)?;
Ok(MeltQuote { Ok(wallet::MeltQuote {
id: row_id, id: row_id,
amount: Amount::from(row_amount as u64), amount: Amount::from(row_amount as u64),
unit: CurrencyUnit::from(row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
request: row_request, request: row_request,
fee_reserve: Amount::from(row_fee_reserve as u64), fee_reserve: Amount::from(row_fee_reserve as u64),
state, state,
@@ -788,6 +790,6 @@ fn sqlite_row_to_proof_info(row: &SqliteRow) -> Result<ProofInfo, Error> {
mint_url: row_mint_url.into(), mint_url: row_mint_url.into(),
state: State::from_str(&row_state)?, state: State::from_str(&row_state)?,
spending_condition: row_spending_condition.and_then(|r| serde_json::from_str(&r).ok()), spending_condition: row_spending_condition.and_then(|r| serde_json::from_str(&r).ok()),
unit: CurrencyUnit::from(row_unit), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
}) })
} }

View File

@@ -27,7 +27,7 @@ bitcoin = { workspace = true, features = [
] } # lightning-invoice uses v0.30 ] } # lightning-invoice uses v0.30
ciborium = { version = "0.2.2", default-features = false, features = ["std"] } ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
http = "1.0" http = "1.0"
lightning-invoice = { version = "0.31", features = ["serde"] } lightning-invoice.workspace = true
once_cell = "1.19" once_cell = "1.19"
reqwest = { version = "0.12", default-features = false, features = [ reqwest = { version = "0.12", default-features = false, features = [
"json", "json",

View File

@@ -8,12 +8,11 @@ use tokio::sync::RwLock;
use super::{Error, MintDatabase}; use super::{Error, MintDatabase};
use crate::dhke::hash_to_curve; use crate::dhke::hash_to_curve;
use crate::mint::MintKeySetInfo; use crate::mint::{self, MintKeySetInfo, MintQuote};
use crate::nuts::{ use crate::nuts::{
BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
}; };
use crate::secret::Secret; use crate::secret::Secret;
use crate::types::{MeltQuote, MintQuote};
/// Mint Memory Database /// Mint Memory Database
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -21,7 +20,7 @@ pub struct MintMemoryDatabase {
active_keysets: Arc<RwLock<HashMap<CurrencyUnit, Id>>>, active_keysets: Arc<RwLock<HashMap<CurrencyUnit, Id>>>,
keysets: Arc<RwLock<HashMap<Id, MintKeySetInfo>>>, keysets: Arc<RwLock<HashMap<Id, MintKeySetInfo>>>,
mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>, mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
melt_quotes: Arc<RwLock<HashMap<String, MeltQuote>>>, melt_quotes: Arc<RwLock<HashMap<String, mint::MeltQuote>>>,
pending_proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>, pending_proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>,
spent_proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>, spent_proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>,
blinded_signatures: Arc<RwLock<HashMap<[u8; 33], BlindSignature>>>, blinded_signatures: Arc<RwLock<HashMap<[u8; 33], BlindSignature>>>,
@@ -34,7 +33,7 @@ impl MintMemoryDatabase {
active_keysets: HashMap<CurrencyUnit, Id>, active_keysets: HashMap<CurrencyUnit, Id>,
keysets: Vec<MintKeySetInfo>, keysets: Vec<MintKeySetInfo>,
mint_quotes: Vec<MintQuote>, mint_quotes: Vec<MintQuote>,
melt_quotes: Vec<MeltQuote>, melt_quotes: Vec<mint::MeltQuote>,
pending_proofs: Proofs, pending_proofs: Proofs,
spent_proofs: Proofs, spent_proofs: Proofs,
blinded_signatures: HashMap<[u8; 33], BlindSignature>, blinded_signatures: HashMap<[u8; 33], BlindSignature>,
@@ -129,6 +128,21 @@ impl MintDatabase for MintMemoryDatabase {
Ok(current_state) Ok(current_state)
} }
async fn get_mint_quote_by_request(
&self,
request: &str,
) -> Result<Option<MintQuote>, Self::Err> {
let quotes = self.get_mint_quotes().await?;
let quote = quotes
.into_iter()
.filter(|q| q.request.eq(request))
.collect::<Vec<MintQuote>>()
.first()
.cloned();
Ok(quote)
}
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> { async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
Ok(self.mint_quotes.read().await.values().cloned().collect()) Ok(self.mint_quotes.read().await.values().cloned().collect())
@@ -140,7 +154,7 @@ impl MintDatabase for MintMemoryDatabase {
Ok(()) Ok(())
} }
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
self.melt_quotes self.melt_quotes
.write() .write()
.await .await
@@ -148,7 +162,7 @@ impl MintDatabase for MintMemoryDatabase {
Ok(()) Ok(())
} }
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> { async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err> {
Ok(self.melt_quotes.read().await.get(quote_id).cloned()) Ok(self.melt_quotes.read().await.get(quote_id).cloned())
} }
@@ -173,7 +187,7 @@ impl MintDatabase for MintMemoryDatabase {
Ok(current_state) Ok(current_state)
} }
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> { async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
Ok(self.melt_quotes.read().await.values().cloned().collect()) Ok(self.melt_quotes.read().await.values().cloned().collect())
} }

View File

@@ -8,8 +8,12 @@ use std::fmt::Debug;
use async_trait::async_trait; use async_trait::async_trait;
use thiserror::Error; use thiserror::Error;
#[cfg(feature = "mint")]
use crate::mint;
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
use crate::mint::MintKeySetInfo; use crate::mint::MintKeySetInfo;
#[cfg(feature = "mint")]
use crate::mint::MintQuote as MintMintQuote;
#[cfg(feature = "wallet")] #[cfg(feature = "wallet")]
use crate::nuts::State; use crate::nuts::State;
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
@@ -22,10 +26,12 @@ use crate::nuts::{KeySetInfo, Keys, MintInfo, SpendingConditions};
use crate::secret::Secret; use crate::secret::Secret;
#[cfg(feature = "wallet")] #[cfg(feature = "wallet")]
use crate::types::ProofInfo; use crate::types::ProofInfo;
#[cfg(any(feature = "wallet", feature = "mint"))]
use crate::types::{MeltQuote, MintQuote};
#[cfg(feature = "wallet")] #[cfg(feature = "wallet")]
use crate::url::UncheckedUrl; use crate::url::UncheckedUrl;
#[cfg(feature = "wallet")]
use crate::wallet;
#[cfg(feature = "wallet")]
use crate::wallet::MintQuote as WalletMintQuote;
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
pub mod mint_memory; pub mod mint_memory;
@@ -94,18 +100,18 @@ pub trait WalletDatabase: Debug {
async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err>; async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err>;
/// Add mint quote to storage /// Add mint quote to storage
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err>; async fn add_mint_quote(&self, quote: WalletMintQuote) -> Result<(), Self::Err>;
/// Get mint quote from storage /// Get mint quote from storage
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err>; async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<WalletMintQuote>, Self::Err>;
/// Get mint quotes from storage /// Get mint quotes from storage
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err>; async fn get_mint_quotes(&self) -> Result<Vec<WalletMintQuote>, Self::Err>;
/// Remove mint quote from storage /// Remove mint quote from storage
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>; async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
/// Add melt quote to storage /// Add melt quote to storage
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err>; async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err>;
/// Get melt quote from storage /// Get melt quote from storage
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err>; async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err>;
/// Remove melt quote from storage /// Remove melt quote from storage
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>; async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
@@ -164,34 +170,39 @@ pub trait MintDatabase {
/// Get all Active Keyset /// Get all Active Keyset
async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err>; async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err>;
/// Add [`MintQuote`] /// Add [`MintMintQuote`]
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err>; async fn add_mint_quote(&self, quote: MintMintQuote) -> Result<(), Self::Err>;
/// Get [`MintQuote`] /// Get [`MintMintQuote`]
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err>; async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintMintQuote>, Self::Err>;
/// Update state of [`MintQuote`] /// Update state of [`MintMintQuote`]
async fn update_mint_quote_state( async fn update_mint_quote_state(
&self, &self,
quote_id: &str, quote_id: &str,
state: MintQuoteState, state: MintQuoteState,
) -> Result<MintQuoteState, Self::Err>; ) -> Result<MintQuoteState, Self::Err>;
/// Get all [`MintQuote`]s /// Get all [`MintMintQuote`]s
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err>; async fn get_mint_quote_by_request(
/// Remove [`MintQuote`] &self,
request: &str,
) -> Result<Option<MintMintQuote>, Self::Err>;
/// Get Mint Quotes
async fn get_mint_quotes(&self) -> Result<Vec<MintMintQuote>, Self::Err>;
/// Remove [`MintMintQuote`]
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>; async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
/// Add [`MeltQuote`] /// Add [`mint::MeltQuote`]
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err>; async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err>;
/// Get [`MeltQuote`] /// Get [`mint::MeltQuote`]
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err>; async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err>;
/// Update [`MeltQuote`] state /// Update [`mint::MeltQuote`] state
async fn update_melt_quote_state( async fn update_melt_quote_state(
&self, &self,
quote_id: &str, quote_id: &str,
state: MeltQuoteState, state: MeltQuoteState,
) -> Result<MeltQuoteState, Self::Err>; ) -> Result<MeltQuoteState, Self::Err>;
/// Get all [`MeltQuote`]s /// Get all [`mint::MeltQuote`]s
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err>; async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err>;
/// Remove [`MeltQuote`] /// Remove [`mint::MeltQuote`]
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>; async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
/// Add [`MintKeySetInfo`] /// Add [`MintKeySetInfo`]

View File

@@ -11,9 +11,11 @@ use crate::cdk_database::Error;
use crate::nuts::{ use crate::nuts::{
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
}; };
use crate::types::{MeltQuote, MintQuote, ProofInfo}; use crate::types::ProofInfo;
use crate::url::UncheckedUrl; use crate::url::UncheckedUrl;
use crate::util::unix_time; use crate::util::unix_time;
use crate::wallet;
use crate::wallet::types::MintQuote;
/// Wallet in Memory Database /// Wallet in Memory Database
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@@ -22,7 +24,7 @@ pub struct WalletMemoryDatabase {
mint_keysets: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Id>>>>, mint_keysets: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Id>>>>,
keysets: Arc<RwLock<HashMap<Id, KeySetInfo>>>, keysets: Arc<RwLock<HashMap<Id, KeySetInfo>>>,
mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>, mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
melt_quotes: Arc<RwLock<HashMap<String, MeltQuote>>>, melt_quotes: Arc<RwLock<HashMap<String, wallet::MeltQuote>>>,
mint_keys: Arc<RwLock<HashMap<Id, Keys>>>, mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>, proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
keyset_counter: Arc<RwLock<HashMap<Id, u32>>>, keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
@@ -33,7 +35,7 @@ impl WalletMemoryDatabase {
/// Create new [`WalletMemoryDatabase`] /// Create new [`WalletMemoryDatabase`]
pub fn new( pub fn new(
mint_quotes: Vec<MintQuote>, mint_quotes: Vec<MintQuote>,
melt_quotes: Vec<MeltQuote>, melt_quotes: Vec<wallet::MeltQuote>,
mint_keys: Vec<Keys>, mint_keys: Vec<Keys>,
keyset_counter: HashMap<Id, u32>, keyset_counter: HashMap<Id, u32>,
nostr_last_checked: HashMap<PublicKey, u32>, nostr_last_checked: HashMap<PublicKey, u32>,
@@ -207,7 +209,7 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(()) Ok(())
} }
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> { async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Error> {
self.melt_quotes self.melt_quotes
.write() .write()
.await .await
@@ -215,7 +217,7 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(()) Ok(())
} }
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Error> { async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Error> {
Ok(self.melt_quotes.read().await.get(quote_id).cloned()) Ok(self.melt_quotes.read().await.get(quote_id).cloned())
} }

View File

@@ -8,7 +8,8 @@ use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::nuts::{MeltQuoteState, MintQuoteState}; use crate::mint;
use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
/// CDK Lightning Error /// CDK Lightning Error
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -25,6 +26,9 @@ pub enum Error {
/// Parse Error /// Parse Error
#[error(transparent)] #[error(transparent)]
Parse(#[from] ParseOrSemanticError), Parse(#[from] ParseOrSemanticError),
/// Cannot convert units
#[error("Cannot convert units")]
CannotConvertUnits,
} }
/// MintLighting Trait /// MintLighting Trait
@@ -33,18 +37,28 @@ pub trait MintLightning {
/// Mint Lightning Error /// Mint Lightning Error
type Err: Into<Error> + From<Error>; type Err: Into<Error> + From<Error>;
/// Base Unit
fn get_base_unit(&self) -> CurrencyUnit;
/// Create a new invoice /// Create a new invoice
async fn create_invoice( async fn create_invoice(
&self, &self,
msats: u64, amount: u64,
description: String, description: String,
unix_expiry: u64, unix_expiry: u64,
) -> Result<Bolt11Invoice, Self::Err>; ) -> Result<CreateInvoiceResponse, Self::Err>;
/// Get payment quote
/// Used to get fee and amount required for a payment request
async fn get_payment_quote(
&self,
melt_quote_request: &MeltQuoteBolt11Request,
) -> Result<PaymentQuoteResponse, Self::Err>;
/// Pay bolt11 invoice /// Pay bolt11 invoice
async fn pay_invoice( async fn pay_invoice(
&self, &self,
bolt11: Bolt11Invoice, melt_quote: mint::MeltQuote,
partial_msats: Option<u64>, partial_msats: Option<u64>,
max_fee_msats: Option<u64>, max_fee_msats: Option<u64>,
) -> Result<PayInvoiceResponse, Self::Err>; ) -> Result<PayInvoiceResponse, Self::Err>;
@@ -55,11 +69,23 @@ pub trait MintLightning {
) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err>; ) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err>;
/// Check the status of an incoming payment /// Check the status of an incoming payment
async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err>; async fn check_invoice_status(
&self,
request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err>;
}
/// Create invoice response
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateInvoiceResponse {
/// Id that is used to look up the invoice from the ln backend
pub request_lookup_id: String,
/// Bolt11 payment request
pub request: Bolt11Invoice,
} }
/// Pay invoice response /// Pay invoice response
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct PayInvoiceResponse { pub struct PayInvoiceResponse {
/// Payment hash /// Payment hash
pub payment_hash: String, pub payment_hash: String,
@@ -70,3 +96,36 @@ pub struct PayInvoiceResponse {
/// Totoal Amount Spent in msats /// Totoal Amount Spent in msats
pub total_spent_msats: u64, pub total_spent_msats: u64,
} }
/// Payment quote response
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaymentQuoteResponse {
/// Request look up id
pub request_lookup_id: String,
/// Amount
pub amount: u64,
/// Fee required for melt
pub fee: u64,
}
const MSAT_IN_SAT: u64 = 1000;
/// Helper function to convert units
pub fn to_unit<T>(
amount: T,
current_unit: &CurrencyUnit,
target_unit: &CurrencyUnit,
) -> Result<u64, Error>
where
T: Into<u64>,
{
let amount = amount.into();
match (current_unit, target_unit) {
(CurrencyUnit::Sat, CurrencyUnit::Sat) => Ok(amount),
(CurrencyUnit::Msat, CurrencyUnit::Msat) => Ok(amount),
(CurrencyUnit::Sat, CurrencyUnit::Msat) => Ok(amount * MSAT_IN_SAT),
(CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok(amount / MSAT_IN_SAT),
(CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount),
_ => Err(Error::CannotConvertUnits),
}
}

View File

@@ -15,6 +15,21 @@ pub enum Error {
/// Mint does not have a key for amount /// Mint does not have a key for amount
#[error("No Key for Amount")] #[error("No Key for Amount")]
AmountKey, AmountKey,
/// Not enough input proofs provided
#[error("Not enough input proofs spent")]
InsufficientInputProofs,
/// Database update failed
#[error("Database error")]
DatabaseError,
/// Unsupported unit
#[error("Unit unsupported")]
UnsupportedUnit,
/// Payment failed
#[error("Payment failed")]
PaymentFailed,
/// Melt Request is not valid
#[error("Melt request is not valid")]
MeltRequestInvalid,
/// Amount is not what expected /// Amount is not what expected
#[error("Amount miss match")] #[error("Amount miss match")]
Amount, Amount,
@@ -24,6 +39,9 @@ pub enum Error {
/// Token could not be validated /// Token could not be validated
#[error("Token not verified")] #[error("Token not verified")]
TokenNotVerified, TokenNotVerified,
/// Invalid payment request
#[error("Invalid payment request")]
InvalidPaymentRequest,
/// Bolt11 invoice does not have amount /// Bolt11 invoice does not have amount
#[error("Invoice Amount undefined")] #[error("Invoice Amount undefined")]
InvoiceAmountUndefined, InvoiceAmountUndefined,
@@ -104,6 +122,15 @@ impl fmt::Display for ErrorResponse {
} }
impl ErrorResponse { impl ErrorResponse {
/// Create new [`ErrorResponse`]
pub fn new(code: ErrorCode, error: Option<String>, detail: Option<String>) -> Self {
Self {
code,
error,
detail,
}
}
/// Error response from json /// Error response from json
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> { pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let value: Value = serde_json::from_str(json)?; let value: Value = serde_json::from_str(json)?;
@@ -124,6 +151,23 @@ impl ErrorResponse {
} }
} }
impl From<Error> for ErrorResponse {
fn from(err: Error) -> ErrorResponse {
match err {
Error::TokenSpent => ErrorResponse {
code: ErrorCode::TokenAlreadySpent,
error: Some(err.to_string()),
detail: None,
},
_ => ErrorResponse {
code: ErrorCode::Unknown(9999),
error: Some(err.to_string()),
detail: None,
},
}
}
}
/// Possible Error Codes /// Possible Error Codes
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum ErrorCode { pub enum ErrorCode {

View File

@@ -1,6 +1,5 @@
//! Mint Errors //! Mint Errors
use http::StatusCode;
use thiserror::Error; use thiserror::Error;
use crate::cdk_database; use crate::cdk_database;
@@ -51,6 +50,9 @@ pub enum Error {
/// Multiple units provided /// Multiple units provided
#[error("Cannot have multiple units")] #[error("Cannot have multiple units")]
MultipleUnits, MultipleUnits,
/// Unit not supported
#[error("Unit not supported")]
UnsupportedUnit,
/// BlindMessage is already signed /// BlindMessage is already signed
#[error("Blinded Message is already signed")] #[error("Blinded Message is already signed")]
BlindedMessageAlreadySigned, BlindedMessageAlreadySigned,
@@ -103,12 +105,6 @@ impl From<Error> for ErrorResponse {
} }
} }
impl From<Error> for (StatusCode, ErrorResponse) {
fn from(err: Error) -> (StatusCode, ErrorResponse) {
(StatusCode::NOT_FOUND, err.into())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -14,12 +14,14 @@ use crate::cdk_database::{self, MintDatabase};
use crate::dhke::{hash_to_curve, sign_message, verify_message}; use crate::dhke::{hash_to_curve, sign_message, verify_message};
use crate::nuts::nut11::enforce_sig_flag; use crate::nuts::nut11::enforce_sig_flag;
use crate::nuts::*; use crate::nuts::*;
use crate::types::{MeltQuote, MintQuote};
use crate::url::UncheckedUrl; use crate::url::UncheckedUrl;
use crate::util::unix_time; use crate::util::unix_time;
use crate::Amount; use crate::Amount;
pub mod error; pub mod error;
pub mod types;
pub use types::{MeltQuote, MintQuote};
/// Cashu Mint /// Cashu Mint
#[derive(Clone)] #[derive(Clone)]
@@ -120,8 +122,9 @@ impl Mint {
unit: CurrencyUnit, unit: CurrencyUnit,
amount: Amount, amount: Amount,
expiry: u64, expiry: u64,
ln_lookup: String,
) -> Result<MintQuote, Error> { ) -> Result<MintQuote, Error> {
let quote = MintQuote::new(mint_url, request, unit, amount, expiry); let quote = MintQuote::new(mint_url, request, unit, amount, expiry, ln_lookup);
self.localstore.add_mint_quote(quote.clone()).await?; self.localstore.add_mint_quote(quote.clone()).await?;
@@ -159,6 +162,16 @@ impl Mint {
Ok(quotes) Ok(quotes)
} }
/// Get pending mint quotes
pub async fn get_pending_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::Pending)
.collect())
}
/// Remove mint quote /// Remove mint quote
pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> { pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
self.localstore.remove_mint_quote(quote_id).await?; self.localstore.remove_mint_quote(quote_id).await?;
@@ -174,8 +187,16 @@ impl Mint {
amount: Amount, amount: Amount,
fee_reserve: Amount, fee_reserve: Amount,
expiry: u64, expiry: u64,
request_lookup_id: String,
) -> Result<MeltQuote, Error> { ) -> Result<MeltQuote, Error> {
let quote = MeltQuote::new(request, unit, amount, fee_reserve, expiry); let quote = MeltQuote::new(
request,
unit,
amount,
fee_reserve,
expiry,
request_lookup_id,
);
self.localstore.add_melt_quote(quote.clone()).await?; self.localstore.add_melt_quote(quote.clone()).await?;
@@ -699,12 +720,27 @@ impl Mint {
Ok(quote) Ok(quote)
} }
/// Process unpaid melt request
/// In the event that a melt request fails and the lighthing payment is not made
/// The [`Proofs`] should be returned to an unspent state and the quote should be unpaid
pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> {
self.localstore
.remove_pending_proofs(melt_request.inputs.iter().map(|p| &p.secret).collect())
.await?;
self.localstore
.update_melt_quote_state(&melt_request.quote, MeltQuoteState::Unpaid)
.await?;
Ok(())
}
/// Process melt request marking [`Proofs`] as spent /// Process melt request marking [`Proofs`] as spent
/// The melt request must be verifyed using [`Self::verify_melt_request`] before calling [`Self::process_melt_request`] /// The melt request must be verifyed using [`Self::verify_melt_request`] before calling [`Self::process_melt_request`]
pub async fn process_melt_request( pub async fn process_melt_request(
&self, &self,
melt_request: &MeltBolt11Request, melt_request: &MeltBolt11Request,
preimage: &str, payment_preimage: Option<String>,
total_spent: Amount, total_spent: Amount,
) -> Result<MeltQuoteBolt11Response, Error> { ) -> Result<MeltQuoteBolt11Response, Error> {
tracing::debug!("Processing melt quote: {}", melt_request.quote); tracing::debug!("Processing melt quote: {}", melt_request.quote);
@@ -788,7 +824,7 @@ impl Mint {
Ok(MeltQuoteBolt11Response { Ok(MeltQuoteBolt11Response {
amount: quote.amount, amount: quote.amount,
paid: Some(true), paid: Some(true),
payment_preimage: Some(preimage.to_string()), payment_preimage,
change, change,
quote: quote.id, quote: quote.id,
fee_reserve: quote.fee_reserve, fee_reserve: quote.fee_reserve,

View File

@@ -0,0 +1,103 @@
//! Mint Types
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::CurrencyUnit;
use crate::nuts::{MeltQuoteState, MintQuoteState};
use crate::{Amount, UncheckedUrl};
/// Mint Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintQuote {
/// Quote id
pub id: String,
/// Mint Url
pub mint_url: UncheckedUrl,
/// Amount of quote
pub amount: Amount,
/// Unit of quote
pub unit: CurrencyUnit,
/// Quote payment request e.g. bolt11
pub request: String,
/// Quote state
pub state: MintQuoteState,
/// Expiration time of quote
pub expiry: u64,
/// Value used by ln backend to look up state of request
pub request_lookup_id: String,
}
impl MintQuote {
/// Create new [`MintQuote`]
pub fn new(
mint_url: UncheckedUrl,
request: String,
unit: CurrencyUnit,
amount: Amount,
expiry: u64,
request_lookup_id: String,
) -> Self {
let id = Uuid::new_v4();
Self {
mint_url,
id: id.to_string(),
amount,
unit,
request,
state: MintQuoteState::Unpaid,
expiry,
request_lookup_id,
}
}
}
/// Melt Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeltQuote {
/// Quote id
pub id: String,
/// Quote unit
pub unit: CurrencyUnit,
/// Quote amount
pub amount: Amount,
/// Quote Payment request e.g. bolt11
pub request: String,
/// Quote fee reserve
pub fee_reserve: Amount,
/// Quote state
pub state: MeltQuoteState,
/// Expiration time of quote
pub expiry: u64,
/// Payment preimage
pub payment_preimage: Option<String>,
/// Value used by ln backend to look up state of request
pub request_lookup_id: String,
}
impl MeltQuote {
/// Create new [`MeltQuote`]
pub fn new(
request: String,
unit: CurrencyUnit,
amount: Amount,
fee_reserve: Amount,
expiry: u64,
request_lookup_id: String,
) -> Self {
let id = Uuid::new_v4();
Self {
id: id.to_string(),
amount,
unit,
request,
fee_reserve,
state: MeltQuoteState::Unpaid,
expiry,
payment_preimage: None,
request_lookup_id,
}
}
}

View File

@@ -5,6 +5,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt; use std::fmt;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
@@ -37,6 +38,9 @@ pub enum Error {
/// Unsupported token /// Unsupported token
#[error("Unsupported token")] #[error("Unsupported token")]
UnsupportedToken, UnsupportedToken,
/// Unsupported token
#[error("Unsupported unit")]
UnsupportedUnit,
/// Invalid Url /// Invalid Url
#[error("Invalid Url")] #[error("Invalid Url")]
InvalidUrl, InvalidUrl,
@@ -317,20 +321,19 @@ pub enum CurrencyUnit {
Msat, Msat,
/// Usd /// Usd
Usd, Usd,
/// Custom unit /// Euro
Custom(String), Eur,
} }
impl<S> From<S> for CurrencyUnit impl FromStr for CurrencyUnit {
where type Err = Error;
S: AsRef<str>, fn from_str(value: &str) -> Result<Self, Self::Err> {
{ match value {
fn from(currency: S) -> Self { "sat" => Ok(Self::Sat),
match currency.as_ref() { "msat" => Ok(Self::Msat),
"sat" => Self::Sat, "usd" => Ok(Self::Usd),
"usd" => Self::Usd, "eur" => Ok(Self::Eur),
"msat" => Self::Msat, _ => Err(Error::UnsupportedUnit),
o => Self::Custom(o.to_string()),
} }
} }
} }
@@ -341,7 +344,7 @@ impl fmt::Display for CurrencyUnit {
CurrencyUnit::Sat => write!(f, "sat"), CurrencyUnit::Sat => write!(f, "sat"),
CurrencyUnit::Msat => write!(f, "msat"), CurrencyUnit::Msat => write!(f, "msat"),
CurrencyUnit::Usd => write!(f, "usd"), CurrencyUnit::Usd => write!(f, "usd"),
CurrencyUnit::Custom(unit) => write!(f, "{unit}"), CurrencyUnit::Eur => write!(f, "eur"),
} }
} }
} }
@@ -361,7 +364,7 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let currency: String = String::deserialize(deserializer)?; let currency: String = String::deserialize(deserializer)?;
Ok(Self::from(currency)) Self::from_str(&currency).map_err(|_| serde::de::Error::custom("Unsupported unit"))
} }
} }

View File

@@ -11,7 +11,6 @@ use thiserror::Error;
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod}; use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
use super::MintQuoteState; use super::MintQuoteState;
use crate::types::MintQuote;
use crate::Amount; use crate::Amount;
/// NUT04 Error /// NUT04 Error
@@ -153,8 +152,9 @@ impl<'de> Deserialize<'de> for MintQuoteBolt11Response {
} }
} }
impl From<MintQuote> for MintQuoteBolt11Response { #[cfg(feature = "mint")]
fn from(mint_quote: MintQuote) -> MintQuoteBolt11Response { impl From<crate::mint::MintQuote> for MintQuoteBolt11Response {
fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response {
let paid = mint_quote.state == QuoteState::Paid; let paid = mint_quote.state == QuoteState::Paid;
MintQuoteBolt11Response { MintQuoteBolt11Response {
quote: mint_quote.id, quote: mint_quote.id,
@@ -208,8 +208,24 @@ pub struct MintMethodSettings {
} }
/// Mint Settings /// Mint Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Settings { pub struct Settings {
methods: Vec<MintMethodSettings>, methods: Vec<MintMethodSettings>,
disabled: bool, disabled: bool,
} }
impl Default for Settings {
fn default() -> Self {
let bolt11_mint = MintMethodSettings {
method: PaymentMethod::Bolt11,
unit: CurrencyUnit::Sat,
min_amount: Some(Amount::from(1)),
max_amount: Some(Amount::from(1000000)),
};
Settings {
methods: vec![bolt11_mint],
disabled: false,
}
}
}

View File

@@ -11,7 +11,8 @@ use thiserror::Error;
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs}; use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
use super::nut15::Mpp; use super::nut15::Mpp;
use crate::types::MeltQuote; #[cfg(feature = "mint")]
use crate::mint;
use crate::{Amount, Bolt11Invoice}; use crate::{Amount, Bolt11Invoice};
/// NUT05 Error /// NUT05 Error
@@ -178,8 +179,9 @@ impl<'de> Deserialize<'de> for MeltQuoteBolt11Response {
} }
} }
impl From<MeltQuote> for MeltQuoteBolt11Response { #[cfg(feature = "mint")]
fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response { impl From<mint::MeltQuote> for MeltQuoteBolt11Response {
fn from(melt_quote: mint::MeltQuote) -> MeltQuoteBolt11Response {
let paid = melt_quote.state == QuoteState::Paid; let paid = melt_quote.state == QuoteState::Paid;
MeltQuoteBolt11Response { MeltQuoteBolt11Response {
quote: melt_quote.id, quote: melt_quote.id,
@@ -251,8 +253,24 @@ pub struct MeltMethodSettings {
} }
/// Melt Settings /// Melt Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Settings { pub struct Settings {
methods: Vec<MeltMethodSettings>, methods: Vec<MeltMethodSettings>,
disabled: bool, disabled: bool,
} }
impl Default for Settings {
fn default() -> Self {
let bolt11_mint = MeltMethodSettings {
method: PaymentMethod::Bolt11,
unit: CurrencyUnit::Sat,
min_amount: Some(Amount::from(1)),
max_amount: Some(Amount::from(1000000)),
};
Settings {
methods: vec![bolt11_mint],
disabled: false,
}
}
}

View File

@@ -19,6 +19,13 @@ pub struct MintVersion {
pub version: String, pub version: String,
} }
impl MintVersion {
/// Create new [`MintVersion`]
pub fn new(name: String, version: String) -> Self {
Self { name, version }
}
}
impl Serialize for MintVersion { impl Serialize for MintVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@@ -46,7 +53,7 @@ impl<'de> Deserialize<'de> for MintVersion {
} }
} }
/// Mint Info [NIP-09] /// Mint Info [NIP-06]
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MintInfo { pub struct MintInfo {
/// name of the mint and should be recognizable /// name of the mint and should be recognizable
@@ -75,6 +82,32 @@ pub struct MintInfo {
pub motd: Option<String>, pub motd: Option<String>,
} }
impl MintInfo {
#![allow(clippy::too_many_arguments)]
/// Create new [`MintInfo`]
pub fn new(
name: Option<String>,
pubkey: Option<PublicKey>,
version: Option<MintVersion>,
description: Option<String>,
description_long: Option<String>,
contact: Option<Vec<ContactInfo>>,
nuts: Nuts,
motd: Option<String>,
) -> Self {
Self {
name,
pubkey,
version,
description,
description_long,
contact,
nuts,
motd,
}
}
}
/// Supported nuts and settings /// Supported nuts and settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Nuts { pub struct Nuts {
@@ -145,6 +178,13 @@ pub struct ContactInfo {
pub info: String, pub info: String,
} }
impl ContactInfo {
/// Create new [`ContactInfo`]
pub fn new(method: String, info: String) -> Self {
Self { method, info }
}
}
fn deserialize_contact_info<'de, D>(deserializer: D) -> Result<Option<Vec<ContactInfo>>, D::Error> fn deserialize_contact_info<'de, D>(deserializer: D) -> Result<Option<Vec<ContactInfo>>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,

View File

@@ -1,15 +1,12 @@
//! Types //! Types
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::Error; use crate::error::Error;
use crate::nuts::{ use crate::nuts::{
CurrencyUnit, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, SpendingConditions, CurrencyUnit, MeltQuoteState, Proof, Proofs, PublicKey, SpendingConditions, State,
State,
}; };
use crate::url::UncheckedUrl; use crate::url::UncheckedUrl;
use crate::Amount;
/// Melt response with proofs /// Melt response with proofs
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
@@ -22,94 +19,6 @@ pub struct Melted {
pub change: Option<Proofs>, pub change: Option<Proofs>,
} }
/// Mint Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintQuote {
/// Quote id
pub id: String,
/// Mint Url
pub mint_url: UncheckedUrl,
/// Amount of quote
pub amount: Amount,
/// Unit of quote
pub unit: CurrencyUnit,
/// Quote payment request e.g. bolt11
pub request: String,
/// Quote state
pub state: MintQuoteState,
/// Expiration time of quote
pub expiry: u64,
}
impl MintQuote {
/// Create new [`MintQuote`]
pub fn new(
mint_url: UncheckedUrl,
request: String,
unit: CurrencyUnit,
amount: Amount,
expiry: u64,
) -> Self {
let id = Uuid::new_v4();
Self {
mint_url,
id: id.to_string(),
amount,
unit,
request,
state: MintQuoteState::Unpaid,
expiry,
}
}
}
/// Melt Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeltQuote {
/// Quote id
pub id: String,
/// Quote unit
pub unit: CurrencyUnit,
/// Quote amount
pub amount: Amount,
/// Quote Payment request e.g. bolt11
pub request: String,
/// Quote fee reserve
pub fee_reserve: Amount,
/// Quote state
pub state: MeltQuoteState,
/// Expiration time of quote
pub expiry: u64,
/// Payment preimage
pub payment_preimage: Option<String>,
}
#[cfg(feature = "mint")]
impl MeltQuote {
/// Create new [`MeltQuote`]
pub fn new(
request: String,
unit: CurrencyUnit,
amount: Amount,
fee_reserve: Amount,
expiry: u64,
) -> Self {
let id = Uuid::new_v4();
Self {
id: id.to_string(),
amount,
unit,
request,
fee_reserve,
state: MeltQuoteState::Unpaid,
expiry,
payment_preimage: None,
}
}
}
/// Prooinfo /// Prooinfo
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofInfo { pub struct ProofInfo {

View File

@@ -25,7 +25,7 @@ use crate::nuts::{
PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey, PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey,
SigFlag, SpendingConditions, State, SwapRequest, SigFlag, SpendingConditions, State, SwapRequest,
}; };
use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo}; use crate::types::{Melted, ProofInfo};
use crate::url::UncheckedUrl; use crate::url::UncheckedUrl;
use crate::util::{hex, unix_time}; use crate::util::{hex, unix_time};
use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1}; use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
@@ -33,8 +33,11 @@ use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
pub mod client; pub mod client;
pub mod error; pub mod error;
pub mod multi_mint_wallet; pub mod multi_mint_wallet;
pub mod types;
pub mod util; pub mod util;
pub use types::{MeltQuote, MintQuote};
/// CDK Wallet /// CDK Wallet
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Wallet { pub struct Wallet {

View File

@@ -14,7 +14,8 @@ use tracing::instrument;
use super::Error; use super::Error;
use crate::amount::SplitTarget; use crate::amount::SplitTarget;
use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token}; use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
use crate::types::{Melted, MintQuote}; use crate::types::Melted;
use crate::wallet::types::MintQuote;
use crate::{Amount, UncheckedUrl, Wallet}; use crate::{Amount, UncheckedUrl, Wallet};
/// Multi Mint Wallet /// Multi Mint Wallet

View File

@@ -0,0 +1,46 @@
//! Wallet Types
use serde::{Deserialize, Serialize};
use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
use crate::{Amount, UncheckedUrl};
/// Mint Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintQuote {
/// Quote id
pub id: String,
/// Mint Url
pub mint_url: UncheckedUrl,
/// Amount of quote
pub amount: Amount,
/// Unit of quote
pub unit: CurrencyUnit,
/// Quote payment request e.g. bolt11
pub request: String,
/// Quote state
pub state: MintQuoteState,
/// Expiration time of quote
pub expiry: u64,
}
/// Melt Quote Info
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeltQuote {
/// Quote id
pub id: String,
/// Quote unit
pub unit: CurrencyUnit,
/// Quote amount
pub amount: Amount,
/// Quote Payment request e.g. bolt11
pub request: String,
/// Quote fee reserve
pub fee_reserve: Amount,
/// Quote state
pub state: MeltQuoteState,
/// Expiration time of quote
pub expiry: u64,
/// Payment preimage
pub payment_preimage: Option<String>,
}

View File

@@ -33,7 +33,9 @@ buildargs=(
"-p cdk-sqlite --no-default-features --features mint" "-p cdk-sqlite --no-default-features --features mint"
"-p cdk-sqlite --no-default-features --features wallet" "-p cdk-sqlite --no-default-features --features wallet"
"-p cdk-cln" "-p cdk-cln"
"-p cdk-axum"
"--bin cdk-cli" "--bin cdk-cli"
"--bin cdk-mintd"
"--examples" "--examples"
) )