mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 23:55:01 +01:00
feat: mintd axum server
feat: deafult NUT-04 and NUT-05 settings to enable bolt11 sats
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -5,3 +5,4 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*.redb
|
*.redb
|
||||||
*.sqlite*
|
*.sqlite*
|
||||||
|
config.toml
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
19
crates/cdk-axum/Cargo.toml
Normal file
19
crates/cdk-axum/Cargo.toml
Normal 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"
|
||||||
63
crates/cdk-axum/src/lib.rs
Normal file
63
crates/cdk-axum/src/lib.rs
Normal 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,
|
||||||
|
}
|
||||||
402
crates/cdk-axum/src/router_handlers.rs
Normal file
402
crates/cdk-axum/src/router_handlers.rs
Normal 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("e_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("e_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("e.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("e.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, "e.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, "e.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, "e.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(),
|
||||||
|
"e.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()
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
30
crates/cdk-mintd/Cargo.toml
Normal file
30
crates/cdk-mintd/Cargo.toml
Normal 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
|
||||||
40
crates/cdk-mintd/example.config.toml
Normal file
40
crates/cdk-mintd/example.config.toml
Normal 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
|
||||||
24
crates/cdk-mintd/src/cli.rs
Normal file
24
crates/cdk-mintd/src/cli.rs
Normal 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>,
|
||||||
|
}
|
||||||
123
crates/cdk-mintd/src/config.rs
Normal file
123
crates/cdk-mintd/src/config.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
253
crates/cdk-mintd/src/main.rs
Normal file
253
crates/cdk-mintd/src/main.rs
Normal 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("e.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"))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
99
crates/cdk-redb/src/mint/migrations.rs
Normal file
99
crates/cdk-redb/src/mint/migrations.rs
Normal 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("e.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("e)?;
|
||||||
|
|
||||||
|
table.insert(quote_id.as_str(), quote_str.as_str())?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
table.remove(quote_id.as_str())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE mint_quote ADD request_lookup_id TEXT;
|
||||||
|
ALTER TABLE melt_quote ADD request_lookup_id TEXT;
|
||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`]
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
103
crates/cdk/src/mint/types.rs
Normal file
103
crates/cdk/src/mint/types.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(¤cy).map_err(|_| serde::de::Error::custom("Unsupported unit"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
46
crates/cdk/src/wallet/types.rs
Normal file
46
crates/cdk/src/wallet/types.rs
Normal 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>,
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user