From b3f892f37d1cc44a0eb56669591389db50a46b5c Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 27 Sep 2024 14:04:58 +0200 Subject: [PATCH 01/35] feat: bolt12 feat: mint builder --- crates/cdk-axum/src/lib.rs | 34 +- crates/cdk-axum/src/router_handlers.rs | 63 ++- crates/cdk-cli/src/main.rs | 13 +- crates/cdk-cli/src/sub_commands/melt.rs | 49 +- crates/cdk-cli/src/sub_commands/mint.rs | 26 +- crates/cdk-cli/src/sub_commands/mod.rs | 1 + .../cdk-cli/src/sub_commands/remint_bolt12.rs | 54 +++ crates/cdk-cln/Cargo.toml | 1 + crates/cdk-cln/src/error.rs | 3 + crates/cdk-cln/src/lib.rs | 282 +++++++++-- crates/cdk-fake-wallet/src/error.rs | 3 + crates/cdk-fake-wallet/src/lib.rs | 57 ++- .../src/init_fake_wallet.rs | 18 +- .../cdk-integration-tests/src/init_regtest.rs | 11 +- crates/cdk-integration-tests/src/lib.rs | 10 +- .../tests/fake_wallet.rs | 18 +- crates/cdk-integration-tests/tests/mint.rs | 8 +- crates/cdk-lnbits/src/error.rs | 6 + crates/cdk-lnbits/src/lib.rs | 57 ++- crates/cdk-lnd/src/error.rs | 3 + crates/cdk-lnd/src/lib.rs | 57 ++- crates/cdk-mintd/Cargo.toml | 9 + crates/cdk-mintd/src/config.rs | 35 +- crates/cdk-mintd/src/lib.rs | 23 + crates/cdk-mintd/src/main.rs | 444 ++++++------------ crates/cdk-mintd/src/mint.rs | 234 +++++++++ crates/cdk-mintd/src/setup.rs | 229 +++++++++ crates/cdk-phoenixd/Cargo.toml | 3 +- crates/cdk-phoenixd/src/error.rs | 6 + crates/cdk-phoenixd/src/lib.rs | 147 +++++- crates/cdk-redb/Cargo.toml | 1 + crates/cdk-redb/src/error.rs | 3 + crates/cdk-redb/src/mint/migrations.rs | 124 ++++- crates/cdk-redb/src/mint/mod.rs | 8 +- crates/cdk-sqlite/src/mint/error.rs | 3 + ...0241002093700_unknown_status_for_quote.sql | 23 + crates/cdk-sqlite/src/mint/mod.rs | 23 +- ...241001184621_melt_quote_payment_method.sql | 1 + crates/cdk-sqlite/src/wallet/mod.rs | 13 +- crates/cdk-strike/src/error.rs | 3 + crates/cdk-strike/src/lib.rs | 48 +- crates/cdk/Cargo.toml | 1 + crates/cdk/src/amount.rs | 29 ++ crates/cdk/src/cdk_lightning/mod.rs | 77 ++- crates/cdk/src/error.rs | 15 + crates/cdk/src/mint/melt.rs | 388 ++++++++++----- crates/cdk/src/mint/mint_18.rs | 95 ++++ crates/cdk/src/mint/mint_nut04.rs | 110 +++-- crates/cdk/src/mint/mod.rs | 30 +- crates/cdk/src/mint/types.rs | 64 ++- crates/cdk/src/nuts/mod.rs | 3 + crates/cdk/src/nuts/nut00/mod.rs | 10 +- crates/cdk/src/nuts/nut04.rs | 6 + crates/cdk/src/nuts/nut05.rs | 51 +- crates/cdk/src/nuts/nut06.rs | 40 +- crates/cdk/src/nuts/nut19.rs | 66 +++ crates/cdk/src/nuts/nut20.rs | 77 +++ crates/cdk/src/wallet/client.rs | 99 +++- crates/cdk/src/wallet/melt.rs | 82 +++- crates/cdk/src/wallet/mint.rs | 69 ++- crates/cdk/src/wallet/multi_mint_wallet.rs | 3 +- crates/cdk/src/wallet/types.rs | 4 +- 62 files changed, 2735 insertions(+), 738 deletions(-) create mode 100644 crates/cdk-cli/src/sub_commands/remint_bolt12.rs create mode 100644 crates/cdk-mintd/src/lib.rs create mode 100644 crates/cdk-mintd/src/mint.rs create mode 100644 crates/cdk-mintd/src/setup.rs create mode 100644 crates/cdk-sqlite/src/mint/migrations/20241002093700_unknown_status_for_quote.sql create mode 100644 crates/cdk-sqlite/src/wallet/migrations/20241001184621_melt_quote_payment_method.sql create mode 100644 crates/cdk/src/mint/mint_18.rs create mode 100644 crates/cdk/src/nuts/nut19.rs create mode 100644 crates/cdk/src/nuts/nut20.rs diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index 9083163b0..e16752184 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -132,7 +132,12 @@ pub struct MintState { pub struct ApiDocV1; /// Create mint [`Router`] with required endpoints for cashu mint -pub async fn create_mint_router(mint: Arc, cache_ttl: u64, cache_tti: u64) -> Result { +pub async fn create_mint_router( + mint: Arc, + cache_ttl: u64, + cache_tti: u64, + include_bolt12: bool, +) -> Result { let state = MintState { mint, cache: Cache::builder() @@ -142,7 +147,7 @@ pub async fn create_mint_router(mint: Arc, cache_ttl: u64, cache_tti: u64) .build(), }; - let v1_router = Router::new() + let mut v1_router = Router::new() .route("/keys", get(get_keys)) .route("/keysets", get(get_keysets)) .route("/keys/:keyset_id", get(get_keyset_pubkeys)) @@ -163,7 +168,32 @@ pub async fn create_mint_router(mint: Arc, cache_ttl: u64, cache_tti: u64) .route("/info", get(get_mint_info)) .route("/restore", post(post_restore)); + // Conditionally create and merge bolt12_router + if include_bolt12 { + let bolt12_router = create_bolt12_router(state.clone()); + //v1_router = bolt12_router.merge(v1_router); + v1_router = v1_router.merge(bolt12_router); + } + + // Nest the combined router under "/v1" let mint_router = Router::new().nest("/v1", v1_router).with_state(state); Ok(mint_router) } + +fn create_bolt12_router(state: MintState) -> Router { + Router::new() + .route("/melt/quote/bolt12", post(get_melt_bolt12_quote)) + .route( + "/melt/quote/bolt12/:quote_id", + get(get_check_melt_bolt11_quote), + ) + .route("/melt/bolt12", post(post_melt_bolt12)) + .route("/mint/quote/bolt12", post(get_mint_bolt12_quote)) + .route( + "/mint/quote/bolt12/:quote_id", + get(get_check_mint_bolt11_quote), + ) + .route("/mint/bolt12", post(post_mint_bolt12)) + .with_state(state) +} diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index c4ba781ab..546f37838 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -3,11 +3,12 @@ use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use cdk::error::ErrorResponse; +use cdk::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use cdk::nuts::{ CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request, - MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response, - MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse, - SwapRequest, SwapResponse, + MeltBolt12Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request, + MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request, + MintQuoteBolt11Response, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, }; use cdk::util::unix_time; use cdk::Error; @@ -143,6 +144,20 @@ pub async fn get_mint_bolt11_quote( Ok(Json(quote)) } +/// Get mint bolt12 quote +pub async fn get_mint_bolt12_quote( + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let quote = state + .mint + .get_mint_bolt12_quote(payload) + .await + .map_err(into_response)?; + + Ok(Json(quote)) +} + #[cfg_attr(feature = "swagger", utoipa::path( get, context_path = "/v1", @@ -155,8 +170,6 @@ pub async fn get_mint_bolt11_quote( (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") ) ))] -/// Get mint quote by ID -/// /// Get mint quote state. pub async fn get_check_mint_bolt11_quote( State(state): State, @@ -205,6 +218,23 @@ pub async fn post_mint_bolt11( Ok(Json(res)) } +/// Request a quote for melting tokens +pub async fn post_mint_bolt12( + State(state): State, + Json(payload): Json, +) -> Result, 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)) +} + #[cfg_attr(feature = "swagger", utoipa::path( post, context_path = "/v1", @@ -215,7 +245,6 @@ pub async fn post_mint_bolt11( (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") ) ))] -/// Request a quote for melting tokens pub async fn get_melt_bolt11_quote( State(state): State, Json(payload): Json, @@ -277,12 +306,30 @@ pub async fn post_melt_bolt11( State(state): State, Json(payload): Json, ) -> Result, Response> { - let res = state + let res = state.mint.melt(&payload).await.map_err(into_response)?; + + Ok(Json(res)) +} + +pub async fn get_melt_bolt12_quote( + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let quote = state .mint - .melt_bolt11(&payload) + .get_melt_bolt12_quote(&payload) .await .map_err(into_response)?; + Ok(Json(quote)) +} + +pub async fn post_melt_bolt12( + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let res = state.mint.melt(&payload).await.map_err(into_response)?; + Ok(Json(res)) } diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 7ff70cda0..7d1a286e2 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -64,6 +64,8 @@ enum Commands { MintInfo(sub_commands::mint_info::MintInfoSubcommand), /// Mint proofs via bolt11 Mint(sub_commands::mint::MintSubCommand), + /// Remint + ReMint(sub_commands::remint_bolt12::ReMintSubCommand), /// Burn Spent tokens Burn(sub_commands::burn::BurnSubCommand), /// Restore proofs from seed @@ -83,7 +85,7 @@ enum Commands { #[tokio::main] async fn main() -> Result<()> { let args: Cli = Cli::parse(); - let default_filter = args.log_level; + let default_filter = "warn"; let sqlx_filter = "sqlx=warn"; @@ -219,5 +221,14 @@ async fn main() -> Result<()> { Commands::CreateRequest(sub_command_args) => { sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await } + Commands::ReMint(sub_command_args) => { + sub_commands::remint_bolt12::remint( + &multi_mint_wallet, + &mnemonic.to_seed_normalized(""), + localstore, + sub_command_args, + ) + .await + } } } diff --git a/crates/cdk-cli/src/sub_commands/melt.rs b/crates/cdk-cli/src/sub_commands/melt.rs index 1467ebc72..4ed5e09a6 100644 --- a/crates/cdk-cli/src/sub_commands/melt.rs +++ b/crates/cdk-cli/src/sub_commands/melt.rs @@ -3,9 +3,10 @@ use std::io::Write; use std::str::FromStr; use anyhow::{bail, Result}; -use cdk::nuts::CurrencyUnit; +use cdk::amount::Amount; +use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey}; -use cdk::Bolt11Invoice; +// use cdk::Bolt11Invoice; use clap::Args; use crate::sub_commands::balance::mint_balances; @@ -15,13 +16,19 @@ pub struct MeltSubCommand { /// Currency unit e.g. sat #[arg(default_value = "sat")] unit: String, + /// Payment method + #[arg(short, long, default_value = "bolt11")] + method: String, + /// Amount + #[arg(short, long)] + amount: Option, } pub async fn pay( multi_mint_wallet: &MultiMintWallet, sub_command_args: &MeltSubCommand, ) -> Result<()> { - let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; + let unit = CurrencyUnit::from_str(&sub_command_args.unit).unwrap(); let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?; println!("Enter mint number to melt from"); @@ -44,22 +51,36 @@ pub async fn pay( .await .expect("Known wallet"); - println!("Enter bolt11 invoice request"); + let method = PaymentMethod::from_str(&sub_command_args.method)?; + match method { + PaymentMethod::Bolt11 => { + println!("Enter bolt11 invoice request"); + } + PaymentMethod::Bolt12 => { + println!("Enter bolt12 invoice request"); + } + _ => panic!("Unknown payment method"), + } let mut user_input = String::new(); let stdin = io::stdin(); io::stdout().flush().unwrap(); stdin.read_line(&mut user_input)?; - let bolt11 = Bolt11Invoice::from_str(user_input.trim())?; - - if bolt11 - .amount_milli_satoshis() - .unwrap() - .gt(&(>::into(mints_amounts[mint_number].1) * 1000_u64)) - { - bail!("Not enough funds"); - } - let quote = wallet.melt_quote(bolt11.to_string(), None).await?; + + let quote = match method { + PaymentMethod::Bolt11 => { + wallet + .melt_quote(user_input.trim().to_string(), None) + .await? + } + PaymentMethod::Bolt12 => { + let amount = sub_command_args.amount.map(Amount::from); + wallet + .melt_bolt12_quote(user_input.trim().to_string(), amount) + .await? + } + _ => panic!("Unsupported payment methof"), + }; println!("{:?}", quote); diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index 46ce6a27c..4f250f8b6 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -6,7 +6,7 @@ use anyhow::Result; use cdk::amount::SplitTarget; use cdk::cdk_database::{Error, WalletDatabase}; use cdk::mint_url::MintUrl; -use cdk::nuts::{CurrencyUnit, MintQuoteState}; +use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod}; use cdk::wallet::multi_mint_wallet::WalletKey; use cdk::wallet::{MultiMintWallet, Wallet}; use cdk::Amount; @@ -21,8 +21,11 @@ pub struct MintSubCommand { /// Amount amount: u64, /// Currency unit e.g. sat - #[arg(default_value = "sat")] + #[arg(short, long, default_value = "sat")] unit: String, + /// Payment method + #[arg(long, default_value = "bolt11")] + method: String, /// Quote description #[serde(skip_serializing_if = "Option::is_none")] description: Option, @@ -51,9 +54,22 @@ pub async fn mint( } }; - let quote = wallet - .mint_quote(Amount::from(sub_command_args.amount), description) - .await?; + let method = PaymentMethod::from_str(&sub_command_args.method)?; + + let quote = match method { + PaymentMethod::Bolt11 => { + println!("Bolt11"); + wallet + .mint_quote(Amount::from(sub_command_args.amount), description) + .await? + } + PaymentMethod::Bolt12 => { + wallet + .mint_bolt12_quote(Amount::from(sub_command_args.amount), description) + .await? + } + _ => panic!("Unsupported unit"), + }; println!("Quote: {:#?}", quote); diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index 8256d0aea..61bce63ce 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -11,6 +11,7 @@ pub mod mint_info; pub mod pay_request; pub mod pending_mints; pub mod receive; +pub mod remint_bolt12; pub mod restore; pub mod send; pub mod update_mint_url; diff --git a/crates/cdk-cli/src/sub_commands/remint_bolt12.rs b/crates/cdk-cli/src/sub_commands/remint_bolt12.rs new file mode 100644 index 000000000..4d63c9f49 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/remint_bolt12.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use anyhow::Result; +use cdk::amount::SplitTarget; +use cdk::cdk_database::{Error, WalletDatabase}; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::multi_mint_wallet::WalletKey; +use cdk::wallet::{MultiMintWallet, Wallet}; +use clap::Args; +use serde::{Deserialize, Serialize}; + +#[derive(Args, Serialize, Deserialize)] +pub struct ReMintSubCommand { + /// Mint url + mint_url: MintUrl, + #[arg(long)] + quote_id: String, +} + +pub async fn remint( + multi_mint_wallet: &MultiMintWallet, + seed: &[u8], + localstore: Arc + Sync + Send>, + sub_command_args: &ReMintSubCommand, +) -> Result<()> { + let mint_url = sub_command_args.mint_url.clone(); + let quote_id = sub_command_args.quote_id.clone(); + + let wallet = match multi_mint_wallet + .get_wallet(&WalletKey::new(mint_url.clone(), CurrencyUnit::Sat)) + .await + { + Some(wallet) => wallet.clone(), + None => { + let wallet = Wallet::new( + &mint_url.to_string(), + CurrencyUnit::Sat, + localstore, + seed, + None, + )?; + + multi_mint_wallet.add_wallet(wallet.clone()).await; + wallet + } + }; + + let receive_amount = wallet.mint("e_id, SplitTarget::default(), None).await?; + + println!("Received {receive_amount} from mint {mint_url}"); + + Ok(()) +} diff --git a/crates/cdk-cln/Cargo.toml b/crates/cdk-cln/Cargo.toml index a5c505ede..374275ed5 100644 --- a/crates/cdk-cln/Cargo.toml +++ b/crates/cdk-cln/Cargo.toml @@ -14,6 +14,7 @@ async-trait = "0.1" bitcoin = { version = "0.32.2", default-features = false } cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } cln-rpc = "0.2.0" +lightning = { version = "0.0.125", default-features = false, features = ["std"]} futures = { version = "0.3.28", default-features = false } tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } diff --git a/crates/cdk-cln/src/error.rs b/crates/cdk-cln/src/error.rs index e97832fc4..fc76e0964 100644 --- a/crates/cdk-cln/src/error.rs +++ b/crates/cdk-cln/src/error.rs @@ -17,6 +17,9 @@ pub enum Error { /// Invalid payment hash #[error("Invalid hash")] InvalidHash, + /// Wrong payment type + #[error("Wrong payment type")] + WrongPaymentType, /// Cln Error #[error(transparent)] Cln(#[from] cln_rpc::Error), diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 1e8f609c4..f9287990b 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -11,19 +11,21 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use cdk::amount::{to_unit, Amount}; +use cdk::amount::{amount_for_offer, to_unit, Amount}; use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, + PayInvoiceResponse, PaymentQuoteResponse, Settings, }; +use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, + CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, }; use cdk::util::{hex, unix_time}; use cdk::{mint, Bolt11Invoice}; use cln_rpc::model::requests::{ - InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest, + FetchinvoiceRequest, InvoiceRequest, ListinvoicesRequest, ListpaysRequest, OfferRequest, + PayRequest, WaitanyinvoiceRequest, }; use cln_rpc::model::responses::{ ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus, @@ -33,6 +35,8 @@ use cln_rpc::model::Request; use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny}; use error::Error; use futures::{Stream, StreamExt}; +use lightning::offers::invoice::Bolt12Invoice; +use lightning::offers::offer::Offer; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use uuid::Uuid; @@ -45,8 +49,8 @@ pub struct Cln { rpc_socket: PathBuf, cln_client: Arc>, fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, + bolt12_mint: bool, + bolt12_melt: bool, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, } @@ -56,8 +60,8 @@ impl Cln { pub async fn new( rpc_socket: PathBuf, fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, + bolt12_mint: bool, + bolt12_melt: bool, ) -> Result { let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?; @@ -65,8 +69,8 @@ impl Cln { rpc_socket, cln_client: Arc::new(Mutex::new(cln_client)), fee_reserve, - mint_settings, - melt_settings, + bolt12_mint, + bolt12_melt, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), }) @@ -81,8 +85,8 @@ impl MintLightning for Cln { Settings { mpp: true, unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + bolt12_mint: self.bolt12_mint, + bolt12_melt: self.bolt12_melt, invoice_description: true, } } @@ -101,7 +105,7 @@ impl MintLightning for Cln { // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let last_pay_index = self.get_last_pay_index().await?; let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?; @@ -158,6 +162,11 @@ impl MintLightning for Cln { let payment_hash = wait_any_response.payment_hash.to_string(); + + // TODO: Handle unit conversion + let amount_msats = wait_any_response.amount_received_msat.expect("status is paid there should be an amount"); + let amount_sats = amount_msats.msat() / 1000; + let request_look_up = match wait_any_response.bolt12 { // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up. // Since this is not returned in the wait any response, @@ -188,7 +197,7 @@ impl MintLightning for Cln { None => payment_hash, }; - return Some((request_look_up, (cln_client, last_pay_idx, cancel_token, is_active))); + break Some(((request_look_up, amount_sats.into()), (cln_client, last_pay_idx, cancel_token, is_active))); } Err(e) => { tracing::warn!("Error fetching invoice: {e}"); @@ -245,7 +254,11 @@ impl MintLightning for Cln { partial_amount: Option, max_fee: Option, ) -> Result { - let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; + let bolt11 = &match melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => bolt11, + PaymentRequest::Bolt12 { .. } => return Err(Error::WrongPaymentType.into()), + }; + let pay_state = self .check_outgoing_payment(&bolt11.payment_hash().to_string()) .await?; @@ -265,7 +278,7 @@ impl MintLightning for Cln { let mut cln_client = self.cln_client.lock().await; let cln_response = cln_client .call(Request::Pay(PayRequest { - bolt11: melt_quote.request.to_string(), + bolt11: bolt11.to_string(), amount_msat: None, label: None, riskfactor: None, @@ -384,41 +397,19 @@ impl MintLightning for Cln { ) -> Result { let mut cln_client = self.cln_client.lock().await; - let cln_response = cln_client - .call(Request::ListInvoices(ListinvoicesRequest { - payment_hash: Some(payment_hash.to_string()), - label: None, - invstring: None, - offer_id: None, - index: None, - limit: None, - start: None, - })) - .await - .map_err(Error::from)?; - - let status = match cln_response { - cln_rpc::Response::ListInvoices(invoice_response) => { - match invoice_response.invoices.first() { - Some(invoice_response) => { - cln_invoice_status_to_mint_state(invoice_response.status) - } - None => { - tracing::info!( - "Check invoice called on unknown look up id: {}", - payment_hash - ); - return Err(Error::WrongClnResponse.into()); - } - } + match fetch_invoice_by_payment_hash(&mut cln_client, payment_hash).await? { + Some(invoice) => { + let status = cln_invoice_status_to_mint_state(invoice.status); + Ok(status) } - _ => { - tracing::warn!("CLN returned wrong response kind"); - return Err(Error::WrongClnResponse.into()); + None => { + tracing::info!( + "Check invoice called on unknown payment hash: {}", + payment_hash + ); + Err(Error::UnknownInvoice.into()) } - }; - - Ok(status) + } } async fn check_outgoing_payment( @@ -465,6 +456,197 @@ impl MintLightning for Cln { } } } + + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + let offer = + Offer::from_str(&melt_quote_request.request).map_err(|_| Error::UnknownInvoice)?; + + let amount = match melt_quote_request.amount { + Some(amount) => amount, + None => amount_for_offer(&offer, &CurrencyUnit::Msat)?, + }; + + let mut cln_client = self.cln_client.lock().await; + let cln_response = cln_client + .call(Request::FetchInvoice(FetchinvoiceRequest { + amount_msat: Some(CLN_Amount::from_msat(amount.into())), + offer: melt_quote_request.request.clone(), + payer_note: None, + quantity: None, + recurrence_counter: None, + recurrence_label: None, + recurrence_start: None, + timeout: None, + })) + .await; + + let amount = to_unit(amount, &CurrencyUnit::Msat, &melt_quote_request.unit)?; + + match cln_response { + Ok(cln_rpc::Response::FetchInvoice(invoice_response)) => { + let bolt12_invoice = + Bolt12Invoice::try_from(hex::decode(&invoice_response.invoice).unwrap()) + .unwrap(); + + Ok(Bolt12PaymentQuoteResponse { + request_lookup_id: bolt12_invoice.payment_hash().to_string(), + amount, + fee: Amount::ZERO, + state: MeltQuoteState::Unpaid, + invoice: Some(invoice_response.invoice), + }) + } + c => { + tracing::debug!("{:?}", c); + tracing::error!("Error attempting to pay invoice for offer",); + Err(Error::WrongClnResponse.into()) + } + } + } + + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + _amount: Option, + max_fee: Option, + ) -> Result { + let bolt12 = &match melt_quote.request { + PaymentRequest::Bolt12 { offer: _, invoice } => invoice.ok_or(Error::UnknownInvoice)?, + PaymentRequest::Bolt11 { .. } => return Err(Error::WrongPaymentType.into()), + }; + + let pay_state = self + .check_outgoing_payment(&melt_quote.request_lookup_id) + .await?; + + match pay_state.status { + MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (), + MeltQuoteState::Paid => { + tracing::debug!("Melt attempted on invoice already paid"); + return Err(Self::Err::InvoiceAlreadyPaid); + } + MeltQuoteState::Pending => { + tracing::debug!("Melt attempted on invoice already pending"); + return Err(Self::Err::InvoicePaymentPending); + } + } + + let mut cln_client = self.cln_client.lock().await; + let cln_response = cln_client + .call(Request::Pay(PayRequest { + bolt11: bolt12.to_string(), + amount_msat: None, + label: None, + riskfactor: None, + maxfeepercent: None, + retry_for: None, + maxdelay: None, + exemptfee: None, + localinvreqid: None, + exclude: None, + maxfee: max_fee + .map(|a| { + let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?; + Ok::(CLN_Amount::from_msat( + msat.into(), + )) + }) + .transpose()?, + description: None, + partial_msat: None, + })) + .await; + + let response = match cln_response { + Ok(cln_rpc::Response::Pay(pay_response)) => { + let status = match pay_response.status { + PayStatus::COMPLETE => MeltQuoteState::Paid, + PayStatus::PENDING => MeltQuoteState::Pending, + PayStatus::FAILED => MeltQuoteState::Failed, + }; + PayInvoiceResponse { + payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), + payment_lookup_id: pay_response.payment_hash.to_string(), + status, + total_spent: to_unit( + pay_response.amount_sent_msat.msat(), + &CurrencyUnit::Msat, + &melt_quote.unit, + )?, + unit: melt_quote.unit, + } + } + _ => { + tracing::error!("Error attempting to pay invoice: {}", bolt12); + return Err(Error::WrongClnResponse.into()); + } + }; + + Ok(response) + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + amount: Option, + unit: &CurrencyUnit, + description: String, + unix_expiry: u64, + single_use: bool, + ) -> Result { + let time_now = unix_time(); + assert!(unix_expiry > time_now); + let mut cln_client = self.cln_client.lock().await; + + let label = Uuid::new_v4().to_string(); + + let amount = match amount { + Some(amount) => { + let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; + + amount.to_string() + } + None => "any".to_string(), + }; + + let cln_response = cln_client + .call(cln_rpc::Request::Offer(OfferRequest { + absolute_expiry: Some(unix_expiry), + description: Some(description), + label: Some(label), + issuer: None, + quantity_max: None, + recurrence: None, + recurrence_base: None, + recurrence_limit: None, + recurrence_paywindow: None, + recurrence_start_any_period: None, + single_use: Some(single_use), + amount, + })) + .await + .map_err(Error::from)?; + + match cln_response { + cln_rpc::Response::Offer(offer_res) => { + let offer = Offer::from_str(&offer_res.bolt12).unwrap(); + let expiry = offer.absolute_expiry().map(|t| t.as_secs()); + + Ok(CreateOfferResponse { + request_lookup_id: offer_res.offer_id.to_string(), + request: offer, + expiry, + }) + } + _ => { + tracing::warn!("CLN returned wrong response kind"); + Err(Error::WrongClnResponse.into()) + } + } + } } impl Cln { diff --git a/crates/cdk-fake-wallet/src/error.rs b/crates/cdk-fake-wallet/src/error.rs index 036d1cab4..f943e7f5c 100644 --- a/crates/cdk-fake-wallet/src/error.rs +++ b/crates/cdk-fake-wallet/src/error.rs @@ -14,6 +14,9 @@ pub enum Error { /// Unknown invoice #[error("No channel receiver")] NoReceiver, + /// Wrong invoice type + #[error("Wrong invoice type")] + WrongRequestType, } impl From for cdk::cdk_lightning::Error { diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index e0fba0727..34af1c8d7 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -7,7 +7,6 @@ use std::collections::{HashMap, HashSet}; use std::pin::Pin; -use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -16,13 +15,14 @@ use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{Secp256k1, SecretKey}; use cdk::amount::{to_unit, Amount}; use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, + PayInvoiceResponse, PaymentQuoteResponse, Settings, }; use cdk::mint; +use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, + CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, }; use cdk::util::unix_time; use error::Error; @@ -44,8 +44,6 @@ pub struct FakeWallet { fee_reserve: FeeReserve, sender: tokio::sync::mpsc::Sender, receiver: Arc>>>, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, payment_states: Arc>>, failed_payment_check: Arc>>, payment_delay: u64, @@ -57,8 +55,6 @@ impl FakeWallet { /// Creat new [`FakeWallet`] pub fn new( fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, payment_states: HashMap, fail_payment_check: HashSet, payment_delay: u64, @@ -69,8 +65,6 @@ impl FakeWallet { fee_reserve, sender, receiver: Arc::new(Mutex::new(Some(receiver))), - mint_settings, - melt_settings, payment_states: Arc::new(Mutex::new(payment_states)), failed_payment_check: Arc::new(Mutex::new(fail_payment_check)), payment_delay, @@ -112,8 +106,8 @@ impl MintLightning for FakeWallet { Settings { mpp: true, unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + bolt12_mint: false, + bolt12_melt: false, invoice_description: true, } } @@ -128,10 +122,11 @@ impl MintLightning for FakeWallet { async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; let receiver_stream = ReceiverStream::new(receiver); - Ok(Box::pin(receiver_stream.map(|label| label))) + self.wait_invoice_is_active.store(true, Ordering::SeqCst); + Ok(Box::pin(receiver_stream.map(|label| (label, Amount::ZERO)))) } async fn get_payment_quote( @@ -173,7 +168,10 @@ impl MintLightning for FakeWallet { _partial_msats: Option, _max_fee_msats: Option, ) -> Result { - let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; + let bolt11 = &match melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => bolt11, + PaymentRequest::Bolt12 { .. } => return Err(Error::WrongRequestType.into()), + }; let payment_hash = bolt11.payment_hash().to_string(); @@ -286,6 +284,35 @@ impl MintLightning for FakeWallet { unit: self.get_settings().unit, }) } + + async fn get_bolt12_payment_quote( + &self, + _melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + todo!() + } + + /// Pay a bolt12 offer + async fn pay_bolt12_offer( + &self, + _melt_quote: mint::MeltQuote, + _amount: Option, + _max_fee_amount: Option, + ) -> Result { + todo!() + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + _amount: Option, + _unit: &CurrencyUnit, + _description: String, + _unix_expiry: u64, + _single_use: bool, + ) -> Result { + todo!() + } } /// Create fake invoice diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index 3eeaa37f9..b4c76c11f 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -9,7 +9,7 @@ use cdk::{ cdk_database::{self, MintDatabase}, cdk_lightning::MintLightning, mint::FeeReserve, - nuts::{CurrencyUnit, MeltMethodSettings, MintMethodSettings}, + nuts::CurrencyUnit, types::LnKey, }; use cdk_fake_wallet::FakeWallet; @@ -46,14 +46,7 @@ where percent_fee_reserve: 1.0, }; - let fake_wallet = FakeWallet::new( - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - HashMap::default(), - HashSet::default(), - 0, - ); + let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0); ln_backends.insert( LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11), @@ -65,9 +58,10 @@ where let cache_tti = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti) - .await - .unwrap(); + let v1_service = + cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti, false) + .await + .unwrap(); let mint_service = Router::new() .merge(v1_service) diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 769a33500..78074b26e 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -7,7 +7,7 @@ use cdk::{ cdk_database::{self, MintDatabase}, cdk_lightning::MintLightning, mint::{FeeReserve, Mint}, - nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings}, + nuts::{CurrencyUnit, MintInfo}, types::{LnKey, QuoteTTL}, }; use cdk_cln::Cln as CdkCln; @@ -131,13 +131,7 @@ pub async fn create_cln_backend(cln_client: &ClnClient) -> Result { percent_fee_reserve: 1.0, }; - Ok(CdkCln::new( - rpc_path, - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - ) - .await?) + Ok(CdkCln::new(rpc_path, fee_reserve, true, true).await?) } pub async fn create_mint( @@ -222,6 +216,7 @@ where Arc::clone(&mint_arc), cache_time_to_live, cache_time_to_idle, + false, ) .await .unwrap(); diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index eacc3b3a5..7a727d7b3 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -11,8 +11,8 @@ use cdk::cdk_lightning::MintLightning; use cdk::dhke::construct_proofs; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState, - Nuts, PaymentMethod, PreMintSecrets, Proofs, State, + CurrencyUnit, Id, KeySet, MintInfo, MintQuoteState, Nuts, PaymentMethod, PreMintSecrets, + Proofs, State, }; use cdk::types::{LnKey, QuoteTTL}; use cdk::wallet::client::HttpClient; @@ -39,9 +39,7 @@ pub fn create_backends_fake_wallet( let ln_key = LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11); let wallet = Arc::new(FakeWallet::new( - fee_reserve.clone(), - MintMethodSettings::default(), - MeltMethodSettings::default(), + fee_reserve, HashMap::default(), HashSet::default(), 0, @@ -88,11 +86,11 @@ pub async fn start_mint( let cache_time_to_idle = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router( Arc::clone(&mint_arc), cache_time_to_live, cache_time_to_idle, + false, ) .await?; diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index 9b6f3ddb7..ba8b3be93 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -367,15 +367,13 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { let check = wallet.melt_quote_status(&melt_quote.id).await?; - assert_eq!( - melt_response - .change - .unwrap() - .sort_by(|a, b| a.amount.cmp(&b.amount)), - check - .change - .unwrap() - .sort_by(|a, b| a.amount.cmp(&b.amount)) - ); + let mut melt_response_change = melt_response.change.unwrap(); + let mut check_change = check.change.unwrap(); + + melt_response_change.sort_by(|a, b| a.amount.cmp(&b.amount)); + + check_change.sort_by(|a, b| a.amount.cmp(&b.amount)); + + assert_eq!(melt_response_change, check_change); Ok(()) } diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index c86e2dd31..b1ff2642e 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -69,14 +69,18 @@ async fn mint_proofs( mint.mint_url.clone(), "".to_string(), CurrencyUnit::Sat, - amount, + Some(amount), unix_time() + 36000, request_lookup.to_string(), + Amount::ZERO, + Amount::ZERO, + None, ); mint.localstore.add_mint_quote(quote.clone()).await?; - mint.pay_mint_quote_for_request_id(&request_lookup).await?; + mint.pay_mint_quote_for_request_id(&request_lookup, amount) + .await?; let keyset_id = Id::from(&keys); let premint = PreMintSecrets::random(keyset_id, amount, split_target)?; diff --git a/crates/cdk-lnbits/src/error.rs b/crates/cdk-lnbits/src/error.rs index c968376d4..e5d1bcd48 100644 --- a/crates/cdk-lnbits/src/error.rs +++ b/crates/cdk-lnbits/src/error.rs @@ -11,6 +11,12 @@ pub enum Error { /// Unknown invoice #[error("Unknown invoice")] UnknownInvoice, + /// Wrong invoice type + #[error("Wrong invoice type")] + WrongRequestType, + /// Unsupported method + #[error("Unsupported method")] + UnsupportedMethod, /// Anyhow error #[error(transparent)] Anyhow(#[from] anyhow::Error), diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index 64e7bef7e..2248d3d3b 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -12,12 +12,13 @@ use async_trait::async_trait; use axum::Router; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, + PayInvoiceResponse, PaymentQuoteResponse, Settings, }; +use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, + CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, }; use cdk::util::unix_time; use cdk::{mint, Bolt11Invoice}; @@ -35,8 +36,6 @@ pub mod error; #[derive(Clone)] pub struct LNbits { lnbits_api: LNBitsClient, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, fee_reserve: FeeReserve, receiver: Arc>>>, webhook_url: String, @@ -51,8 +50,6 @@ impl LNbits { admin_api_key: String, invoice_api_key: String, api_url: String, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, fee_reserve: FeeReserve, receiver: Arc>>>, webhook_url: String, @@ -61,8 +58,6 @@ impl LNbits { Ok(Self { lnbits_api, - mint_settings, - melt_settings, receiver, fee_reserve, webhook_url, @@ -80,8 +75,8 @@ impl MintLightning for LNbits { Settings { mpp: false, unit: CurrencyUnit::Sat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + bolt12_mint: false, + bolt12_melt: false, invoice_description: true, } } @@ -97,7 +92,7 @@ impl MintLightning for LNbits { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self .receiver .lock() @@ -134,7 +129,7 @@ impl MintLightning for LNbits { match check { Ok(state) => { if state { - Some((msg, (receiver, lnbits_api, cancel_token, is_active))) + Some(((msg, Amount::ZERO), (receiver, lnbits_api, cancel_token, is_active))) } else { None } @@ -198,9 +193,14 @@ impl MintLightning for LNbits { _partial_msats: Option, _max_fee_msats: Option, ) -> Result { + let bolt11 = &match melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => bolt11, + PaymentRequest::Bolt12 { .. } => return Err(Error::WrongRequestType.into()), + }; + let pay_response = self .lnbits_api - .pay_invoice(&melt_quote.request) + .pay_invoice(&bolt11.to_string()) .await .map_err(|err| { tracing::error!("Could not pay invoice"); @@ -329,6 +329,35 @@ impl MintLightning for LNbits { Ok(pay_response) } + + async fn get_bolt12_payment_quote( + &self, + _melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } + + /// Pay a bolt12 offer + async fn pay_bolt12_offer( + &self, + _melt_quote: mint::MeltQuote, + _amount: Option, + _max_fee_amount: Option, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + _amount: Option, + _unit: &CurrencyUnit, + _description: String, + _unix_expiry: u64, + _single_use: bool, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } } fn lnbits_to_melt_status(status: &str, pending: bool) -> MeltQuoteState { diff --git a/crates/cdk-lnd/src/error.rs b/crates/cdk-lnd/src/error.rs index 3b6f427b2..8d7d7fda6 100644 --- a/crates/cdk-lnd/src/error.rs +++ b/crates/cdk-lnd/src/error.rs @@ -20,6 +20,9 @@ pub enum Error { /// Payment failed #[error("LND payment failed")] PaymentFailed, + /// Wrong invoice type + #[error("Wrong invoice type")] + WrongRequestType, /// Unknown payment status #[error("LND unknown payment status")] UnknownPaymentStatus, diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index c5ecbeb8f..5fdf7c9ac 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -15,12 +15,13 @@ use anyhow::anyhow; use async_trait::async_trait; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, + PayInvoiceResponse, PaymentQuoteResponse, Settings, }; +use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, + CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, }; use cdk::util::{hex, unix_time}; use cdk::{mint, Bolt11Invoice}; @@ -43,8 +44,6 @@ pub struct Lnd { macaroon_file: PathBuf, client: Arc>, fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, } @@ -56,8 +55,6 @@ impl Lnd { cert_file: PathBuf, macaroon_file: PathBuf, fee_reserve: FeeReserve, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, ) -> Result { let client = fedimint_tonic_lnd::connect(address.to_string(), &cert_file, &macaroon_file) .await @@ -72,8 +69,6 @@ impl Lnd { macaroon_file, client: Arc::new(Mutex::new(client)), fee_reserve, - mint_settings, - melt_settings, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), }) @@ -88,8 +83,8 @@ impl MintLightning for Lnd { Settings { mpp: true, unit: CurrencyUnit::Msat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + bolt12_mint: false, + bolt12_melt: false, invoice_description: true, } } @@ -104,7 +99,7 @@ impl MintLightning for Lnd { async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let mut client = fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file) .await @@ -146,7 +141,7 @@ impl MintLightning for Lnd { match msg { Ok(Some(msg)) => { if msg.state == 1 { - Some((hex::encode(msg.r_hash), (stream, cancel_token, is_active))) + Some(((hex::encode(msg.r_hash), Amount::ZERO), (stream, cancel_token, is_active))) } else { None } @@ -210,10 +205,13 @@ impl MintLightning for Lnd { partial_amount: Option, max_fee: Option, ) -> Result { - let payment_request = melt_quote.request; + let bolt11 = &match melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => bolt11, + PaymentRequest::Bolt12 { .. } => return Err(Error::WrongRequestType.into()), + }; let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest { - payment_request, + payment_request: bolt11.to_string(), fee_limit: max_fee.map(|f| { let limit = Limit::Fixed(u64::from(f) as i64); @@ -393,4 +391,33 @@ impl MintLightning for Lnd { // If the stream is exhausted without a final status Err(Error::UnknownPaymentStatus.into()) } + + async fn get_bolt12_payment_quote( + &self, + _melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + todo!() + } + + /// Pay a bolt12 offer + async fn pay_bolt12_offer( + &self, + _melt_quote: mint::MeltQuote, + _amount: Option, + _max_fee_amount: Option, + ) -> Result { + todo!() + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + _amount: Option, + _unit: &CurrencyUnit, + _description: String, + _unix_expiry: u64, + _single_use: bool, + ) -> Result { + todo!() + } } diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index 34640215e..0cc41fa41 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -9,6 +9,15 @@ repository = "https://github.com/cashubtc/cdk.git" rust-version = "1.63.0" # MSRV description = "CDK mint binary" +[lib] +name = "cdk_mintd" +path = "src/lib.rs" + +[[bin]] +name = "cdk-mintd" +path = "src/main.rs" + + [dependencies] anyhow = "1" axum = "0.6.20" diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 5e94f69bc..a4a1ac072 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -35,12 +35,27 @@ pub enum LnBackend { Lnd, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Ln { pub ln_backend: LnBackend, pub invoice_description: Option, - pub fee_percent: f32, - pub reserve_fee_min: Amount, + pub min_mint: Amount, + pub max_mint: Amount, + pub min_melt: Amount, + pub max_melt: Amount, +} + +impl Default for Ln { + fn default() -> Self { + Ln { + ln_backend: LnBackend::default(), + invoice_description: None, + min_mint: 1.into(), + max_mint: 500_000.into(), + min_melt: 1.into(), + max_melt: 500_000.into(), + } + } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -54,11 +69,16 @@ pub struct LNbits { pub admin_api_key: String, pub invoice_api_key: String, pub lnbits_api: String, + pub fee_percent: f32, + pub reserve_fee_min: Amount, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Cln { pub rpc_path: PathBuf, + pub bolt12: bool, + pub fee_percent: f32, + pub reserve_fee_min: Amount, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -66,23 +86,32 @@ pub struct Lnd { pub address: String, pub cert_file: PathBuf, pub macaroon_file: PathBuf, + pub fee_percent: f32, + pub reserve_fee_min: Amount, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Phoenixd { pub api_password: String, pub api_url: String, + pub bolt12: bool, + pub fee_percent: f32, + pub reserve_fee_min: Amount, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FakeWallet { pub supported_units: Vec, + pub fee_percent: f32, + pub reserve_fee_min: Amount, } impl Default for FakeWallet { fn default() -> Self { Self { supported_units: vec![CurrencyUnit::Sat], + fee_percent: 0.02, + reserve_fee_min: 2.into(), } } } diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs new file mode 100644 index 000000000..92ab8f49b --- /dev/null +++ b/crates/cdk-mintd/src/lib.rs @@ -0,0 +1,23 @@ +//! Cdk mintd lib + +use std::path::PathBuf; + +pub mod cli; +pub mod config; +pub mod mint; +pub mod setup; + +fn expand_path(path: &str) -> Option { + 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)) + } +} diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 4adc2809d..1f668c35e 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -3,7 +3,7 @@ #![warn(missing_docs)] #![warn(rustdoc::bare_urls)] -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -14,33 +14,22 @@ use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; -use cdk::mint::{FeeReserve, MeltQuote, Mint}; -use cdk::mint_url::MintUrl; -use cdk::nuts::{ - nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MeltQuoteState, MintInfo, - MintMethodSettings, MintVersion, MppMethodSettings, Nuts, PaymentMethod, -}; -use cdk::types::{LnKey, QuoteTTL}; -use cdk_cln::Cln; -use cdk_fake_wallet::FakeWallet; -use cdk_lnbits::LNbits; -use cdk_lnd::Lnd; -use cdk_phoenixd::Phoenixd; +use cdk::mint::{MeltQuote, Mint}; +use cdk::nuts::{ContactInfo, CurrencyUnit, MeltQuoteState, MintVersion, PaymentMethod}; +use cdk::types::LnKey; +use cdk_mintd::mint::{MintBuilder, MintMeltLimits}; +use cdk_mintd::setup::LnBackendSetup; use cdk_redb::MintRedbDatabase; use cdk_sqlite::MintSqliteDatabase; -use cdk_strike::Strike; use clap::Parser; -use cli::CLIArgs; -use config::{DatabaseEngine, LnBackend}; -use tokio::sync::{Mutex, Notify}; +use tokio::sync::Notify; use tower_http::cors::CorsLayer; use tracing_subscriber::EnvFilter; -use url::Url; #[cfg(feature = "swagger")] use utoipa::OpenApi; -mod cli; -mod config; +use cdk_mintd::cli::CLIArgs; +use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); const DEFAULT_QUOTE_TTL_SECS: u64 = 1800; @@ -74,6 +63,8 @@ async fn main() -> anyhow::Result<()> { None => work_dir.join("config.toml"), }; + let mut mint_builder = MintBuilder::new(); + let settings = config::Settings::new(&Some(config_file_arg)); let localstore: Arc + Send + Sync> = @@ -92,6 +83,8 @@ async fn main() -> anyhow::Result<()> { } }; + mint_builder = mint_builder.with_localstore(localstore); + let mut contact_info: Option> = None; if let Some(nostr_contact) = &settings.mint_info.contact_nostr_public_key { @@ -123,323 +116,185 @@ async fn main() -> anyhow::Result<()> { CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(), ); - let relative_ln_fee = settings.ln.fee_percent; - - let absolute_ln_fee_reserve = settings.ln.reserve_fee_min; - - let fee_reserve = FeeReserve { - min_fee_reserve: absolute_ln_fee_reserve, - percent_fee_reserve: relative_ln_fee, - }; - let mut ln_backends: HashMap< LnKey, Arc + Send + Sync>, > = HashMap::new(); + let mut ln_routers = vec![]; - let mut supported_units = HashMap::new(); - let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0); - - let mint_url: MintUrl = settings.info.url.parse()?; + let mint_melt_limits = MintMeltLimits { + mint_min: settings.ln.min_mint, + mint_max: settings.ln.max_mint, + melt_min: settings.ln.min_melt, + melt_max: settings.ln.max_melt, + }; - let ln_routers: Vec = match settings.ln.ln_backend { + match settings.ln.ln_backend { LnBackend::Cln => { - let cln_socket = expand_path( - settings - .cln - .expect("Config checked at load that cln is some") - .rpc_path - .to_str() - .ok_or(anyhow!("cln socket not defined"))?, - ) - .ok_or(anyhow!("cln socket not defined"))?; - let cln = Arc::new( - Cln::new( - cln_socket, - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - ) - .await?, + let cln_settings = settings + .cln + .clone() + .expect("Config checked at load that cln is some"); + + let cln = cln_settings + .setup(&mut ln_routers, &settings, CurrencyUnit::Msat) + .await?; + let cln = Arc::new(cln); + let ln_key = LnKey { + unit: CurrencyUnit::Sat, + method: PaymentMethod::Bolt11, + }; + ln_backends.insert(ln_key, cln.clone()); + + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, + cln.clone(), ); - ln_backends.insert(LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11), cln); - supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); - vec![] + let ln_key = LnKey { + unit: CurrencyUnit::Sat, + method: PaymentMethod::Bolt12, + }; + ln_backends.insert(ln_key, cln.clone()); + + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt12, + mint_melt_limits, + cln, + ) } LnBackend::Strike => { - let strike_settings = settings.strike.expect("Checked on config load"); - let api_key = strike_settings.api_key; + let strike_settings = settings.clone().strike.expect("Checked on config load"); - let units = strike_settings + for unit in strike_settings + .clone() .supported_units - .unwrap_or(vec![CurrencyUnit::Sat]); - - let mut routers = vec![]; - - for unit in units { - // Channel used for strike web hook - let (sender, receiver) = tokio::sync::mpsc::channel(8); - let webhook_endpoint = format!("/webhook/{}/invoice", unit); - - let webhook_url = mint_url.join(&webhook_endpoint)?; - - let strike = Strike::new( - api_key.clone(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - unit, - Arc::new(Mutex::new(Some(receiver))), - webhook_url.to_string(), - ) - .await?; - - let router = strike - .create_invoice_webhook(&webhook_endpoint, sender) + .unwrap_or(vec![CurrencyUnit::Sat]) + { + let strike = strike_settings + .setup(&mut ln_routers, &settings, unit) .await?; - routers.push(router); - - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); - - ln_backends.insert(ln_key, Arc::new(strike)); - supported_units.insert(unit, (input_fee_ppk, 64)); + mint_builder = mint_builder.add_ln_backend( + unit, + PaymentMethod::Bolt11, + mint_melt_limits, + Arc::new(strike), + ); } - - routers } LnBackend::LNbits => { - let lnbits_settings = settings.lnbits.expect("Checked on config load"); - let admin_api_key = lnbits_settings.admin_api_key; - let invoice_api_key = lnbits_settings.invoice_api_key; - - // Channel used for lnbits web hook - let (sender, receiver) = tokio::sync::mpsc::channel(8); - let webhook_endpoint = "/webhook/lnbits/sat/invoice"; - - let webhook_url = mint_url.join(webhook_endpoint)?; - - let lnbits = LNbits::new( - admin_api_key, - invoice_api_key, - lnbits_settings.lnbits_api, - MintMethodSettings::default(), - MeltMethodSettings::default(), - fee_reserve, - Arc::new(Mutex::new(Some(receiver))), - webhook_url.to_string(), - ) - .await?; - - let router = lnbits - .create_invoice_webhook_router(webhook_endpoint, sender) + let lnbits_settings = settings.clone().lnbits.expect("Checked on config load"); + let lnbits = lnbits_settings + .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) .await?; - let unit = CurrencyUnit::Sat; - - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); - - ln_backends.insert(ln_key, Arc::new(lnbits)); - - supported_units.insert(unit, (input_fee_ppk, 64)); - vec![router] + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, + Arc::new(lnbits), + ); } LnBackend::Phoenixd => { - let api_password = settings - .clone() - .phoenixd - .expect("Checked at config load") - .api_password; - - let api_url = settings - .clone() - .phoenixd - .expect("Checked at config load") - .api_url; - - if fee_reserve.percent_fee_reserve < 0.04 { - bail!("Fee reserve is too low needs to be at least 0.02"); - } - - let webhook_endpoint = "/webhook/phoenixd"; - - let mint_url = Url::parse(&settings.info.url)?; - - let webhook_url = mint_url.join(webhook_endpoint)?.to_string(); - - let (sender, receiver) = tokio::sync::mpsc::channel(8); - - let phoenixd = Phoenixd::new( - api_password.to_string(), - api_url.to_string(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - fee_reserve, - Arc::new(Mutex::new(Some(receiver))), - webhook_url, - )?; - - let router = phoenixd - .create_invoice_webhook(webhook_endpoint, sender) + let phd_settings = settings.clone().phoenixd.expect("Checked at config load"); + let phd = phd_settings + .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) .await?; - supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); - ln_backends.insert( - LnKey { - unit: CurrencyUnit::Sat, - method: PaymentMethod::Bolt11, - }, - Arc::new(phoenixd), + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, + Arc::new(phd), ); - - vec![router] } LnBackend::Lnd => { - let lnd_settings = settings.lnd.expect("Checked at config load"); - - let address = lnd_settings.address; - let cert_file = lnd_settings.cert_file; - let macaroon_file = lnd_settings.macaroon_file; - - let lnd = Lnd::new( - address, - cert_file, - macaroon_file, - fee_reserve, - MintMethodSettings::default(), - MeltMethodSettings::default(), - ) - .await?; + let lnd_settings = settings.clone().lnd.expect("Checked at config load"); + let lnd = lnd_settings + .setup(&mut ln_routers, &settings, CurrencyUnit::Msat) + .await?; - supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); - ln_backends.insert( - LnKey { - unit: CurrencyUnit::Sat, - method: PaymentMethod::Bolt11, - }, + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, Arc::new(lnd), ); - - vec![] } LnBackend::FakeWallet => { - let units = settings.fake_wallet.unwrap_or_default().supported_units; + let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); - for unit in units { - let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); + for unit in fake_wallet.clone().supported_units { + let fake = fake_wallet + .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) + .await?; - let wallet = Arc::new(FakeWallet::new( - fee_reserve.clone(), - MintMethodSettings::default(), - MeltMethodSettings::default(), - HashMap::default(), - HashSet::default(), - 0, - )); + let fake = Arc::new(fake); - ln_backends.insert(ln_key, wallet); + mint_builder = mint_builder.add_ln_backend( + unit, + PaymentMethod::Bolt11, + mint_melt_limits, + fake.clone(), + ); - supported_units.insert(unit, (input_fee_ppk, 64)); + mint_builder = mint_builder.add_ln_backend( + unit, + PaymentMethod::Bolt12, + mint_melt_limits, + fake.clone(), + ); } - - vec![] } }; - let (nut04_settings, nut05_settings, mpp_settings): ( - nut04::Settings, - nut05::Settings, - Vec, - ) = ln_backends.iter().fold( - ( - nut04::Settings::new(vec![], false), - nut05::Settings::new(vec![], false), - Vec::new(), - ), - |(mut nut_04, mut nut_05, mut mpp), (key, ln)| { - let settings = ln.get_settings(); - - let m = MppMethodSettings { - method: key.method, - unit: key.unit, - mpp: settings.mpp, - }; - - let n4 = MintMethodSettings { - method: key.method, - unit: key.unit, - min_amount: settings.mint_settings.min_amount, - max_amount: settings.mint_settings.max_amount, - description: settings.invoice_description, - }; - - let n5 = MeltMethodSettings { - method: key.method, - unit: key.unit, - min_amount: settings.melt_settings.min_amount, - max_amount: settings.melt_settings.max_amount, - }; - - nut_04.methods.push(n4); - nut_05.methods.push(n5); - mpp.push(m); - - (nut_04, nut_05, mpp) - }, - ); + let support_bolt12_mint = ln_backends.iter().any(|(_k, ln)| { + let settings = ln.get_settings(); + settings.bolt12_mint + }); - let nuts = Nuts::new() - .nut04(nut04_settings) - .nut05(nut05_settings) - .nut07(true) - .nut08(true) - .nut09(true) - .nut10(true) - .nut11(true) - .nut12(true) - .nut14(true) - .nut15(mpp_settings); - - let mut mint_info = MintInfo::new() - .name(settings.mint_info.name) - .version(mint_version) - .description(settings.mint_info.description) - .nuts(nuts); + let support_bolt12_melt = ln_backends.iter().any(|(_k, ln)| { + let settings = ln.get_settings(); + settings.bolt12_melt + }); if let Some(long_description) = &settings.mint_info.description_long { - mint_info = mint_info.long_description(long_description); + mint_builder = mint_builder.with_long_description(long_description.to_string()); } if let Some(contact_info) = contact_info { - mint_info = mint_info.contact_info(contact_info); + for info in contact_info { + mint_builder = mint_builder.add_contact_info(info); + } } if let Some(pubkey) = settings.mint_info.pubkey { - mint_info = mint_info.pubkey(pubkey); + mint_builder = mint_builder.with_pubkey(pubkey); } if let Some(icon_url) = &settings.mint_info.icon_url { - mint_info = mint_info.icon_url(icon_url); + mint_builder = mint_builder.with_icon_url(icon_url.to_string()); } if let Some(motd) = settings.mint_info.motd { - mint_info = mint_info.motd(motd); + mint_builder = mint_builder.with_motd(motd); } let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?; - let quote_ttl = QuoteTTL::new(10000, 10000); + mint_builder = mint_builder + .with_name(settings.mint_info.name) + .with_mint_url(settings.info.url) + .with_version(mint_version) + .with_description(settings.mint_info.description) + .with_quote_ttl(10000, 10000) + .with_seed(mnemonic.to_seed_normalized("").to_vec()); - let mint = Mint::new( - &settings.info.url, - &mnemonic.to_seed_normalized(""), - mint_info, - quote_ttl, - localstore, - ln_backends.clone(), - supported_units, - ) - .await?; + let mint = mint_builder.build().await?; let mint = Arc::new(mint); @@ -471,7 +326,11 @@ async fn main() -> anyhow::Result<()> { .seconds_to_extend_cache_by .unwrap_or(DEFAULT_CACHE_TTI_SECS); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti).await?; + let include_bolt12 = support_bolt12_mint || support_bolt12_melt; + + let v1_service = + cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti, include_bolt12) + .await?; let mut mint_service = Router::new() .merge(v1_service) @@ -529,14 +388,14 @@ async fn check_pending_mint_quotes( ln: Arc + Send + Sync>, ) -> Result<()> { let mut pending_quotes = mint.get_pending_mint_quotes().await?; - tracing::trace!("There are {} pending mint quotes.", pending_quotes.len()); + tracing::info!("There are {} pending mint quotes.", pending_quotes.len()); let mut unpaid_quotes = mint.get_unpaid_mint_quotes().await?; - tracing::trace!("There are {} unpaid mint quotes.", unpaid_quotes.len()); + tracing::info!("There are {} unpaid mint quotes.", unpaid_quotes.len()); unpaid_quotes.append(&mut pending_quotes); for quote in unpaid_quotes { - tracing::trace!("Checking status of mint quote: {}", quote.id); + tracing::debug!("Checking status of mint quote: {}", quote.id); let lookup_id = quote.request_lookup_id; match ln.check_incoming_invoice_status(&lookup_id).await { Ok(state) => { @@ -567,8 +426,10 @@ async fn check_pending_melt_quotes( .into_iter() .filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown) .collect(); + tracing::info!("There are {} pending melt quotes.", pending_quotes.len()); for pending_quote in pending_quotes { + tracing::debug!("Checking status for melt quote {}.", pending_quote.id); let melt_request_ln_key = mint.localstore.get_melt_request(&pending_quote.id).await?; let (melt_request, ln_key) = match melt_request_ln_key { @@ -599,11 +460,7 @@ async fn check_pending_melt_quotes( match pay_invoice_response.status { MeltQuoteState::Paid => { if let Err(err) = mint - .process_melt_request( - &melt_request, - pay_invoice_response.payment_preimage, - pay_invoice_response.total_spent, - ) + .process_melt_request(&melt_request, pay_invoice_response.total_spent) .await { tracing::error!( @@ -645,21 +502,6 @@ async fn check_pending_melt_quotes( Ok(()) } -fn expand_path(path: &str) -> Option { - 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 { let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; diff --git a/crates/cdk-mintd/src/mint.rs b/crates/cdk-mintd/src/mint.rs new file mode 100644 index 000000000..d4eb1ad05 --- /dev/null +++ b/crates/cdk-mintd/src/mint.rs @@ -0,0 +1,234 @@ +use core::panic; +use std::{collections::HashMap, sync::Arc}; + +use anyhow::anyhow; +use cdk::{ + amount::Amount, + cdk_database::{self, MintDatabase}, + cdk_lightning::{self, MintLightning}, + mint::Mint, + nuts::{ + ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, + MppMethodSettings, PaymentMethod, + }, + types::{LnKey, QuoteTTL}, +}; + +/// Cashu Mint +#[derive(Default)] +pub struct MintBuilder { + /// Mint Url + mint_url: Option, + /// Mint Info + mint_info: MintInfo, + /// Mint Storage backend + localstore: Option + Send + Sync>>, + /// Ln backends for mint + ln: Option + Send + Sync>>>, + seed: Option>, + quote_ttl: Option, + supported_units: HashMap, +} + +impl MintBuilder { + pub fn new() -> MintBuilder { + MintBuilder::default() + } + + /// Set localstore + pub fn with_localstore( + mut self, + localstore: Arc + Send + Sync>, + ) -> MintBuilder { + self.localstore = Some(localstore); + self + } + + // Set mint url + pub fn with_mint_url(mut self, mint_url: String) -> Self { + self.mint_url = Some(mint_url); + self + } + + /// Set seed + pub fn with_seed(mut self, seed: Vec) -> Self { + self.seed = Some(seed); + self + } + + /// Set name + pub fn with_name(mut self, name: String) -> Self { + self.mint_info.name = Some(name); + self + } + + /// Set icon url + pub fn with_icon_url(mut self, icon_url: String) -> Self { + self.mint_info.icon_url = Some(icon_url); + self + } + + /// Set icon url + pub fn with_motd(mut self, motd: String) -> Self { + self.mint_info.motd = Some(motd); + self + } + + /// Set description + pub fn with_description(mut self, description: String) -> Self { + self.mint_info.description = Some(description); + self + } + + /// Set long description + pub fn with_long_description(mut self, description: String) -> Self { + self.mint_info.description_long = Some(description); + self + } + + /// Set version + pub fn with_version(mut self, version: MintVersion) -> Self { + self.mint_info.version = Some(version); + self + } + + /// Set contact info + pub fn add_contact_info(mut self, contact_info: ContactInfo) -> Self { + let mut contacts = self.mint_info.contact.clone().unwrap_or_default(); + contacts.push(contact_info); + self.mint_info.contact = Some(contacts); + self + } + + /// Add ln backend + pub fn add_ln_backend( + mut self, + unit: CurrencyUnit, + method: PaymentMethod, + limits: MintMeltLimits, + ln_backend: Arc + Send + Sync>, + ) -> Self { + let ln_key = LnKey { unit, method }; + + let mut ln = self.ln.unwrap_or_default(); + + let settings = ln_backend.get_settings(); + + if settings.mpp { + let mpp_settings = MppMethodSettings { + method, + unit, + mpp: true, + }; + let mut mpp = self.mint_info.nuts.nut15.clone().unwrap_or_default(); + + mpp.methods.push(mpp_settings); + + self.mint_info.nuts.nut15 = Some(mpp); + } + + match method { + PaymentMethod::Bolt11 => { + let mint_method_settings = MintMethodSettings { + method, + unit, + min_amount: Some(limits.mint_min), + max_amount: Some(limits.mint_max), + description: settings.invoice_description, + }; + + self.mint_info.nuts.nut04.methods.push(mint_method_settings); + self.mint_info.nuts.nut04.disabled = false; + + let melt_method_settings = MeltMethodSettings { + method, + unit, + min_amount: Some(limits.melt_min), + max_amount: Some(limits.melt_max), + }; + self.mint_info.nuts.nut05.methods.push(melt_method_settings); + self.mint_info.nuts.nut05.disabled = false; + } + PaymentMethod::Bolt12 => { + let mint_method_settings = MintMethodSettings { + method, + unit, + min_amount: Some(limits.mint_min), + max_amount: Some(limits.mint_max), + description: settings.invoice_description, + }; + + let mut nut18_settings = self.mint_info.nuts.nut18.unwrap_or_default(); + + nut18_settings.methods.push(mint_method_settings); + nut18_settings.disabled = false; + + self.mint_info.nuts.nut18 = Some(nut18_settings); + + let melt_method_settings = MeltMethodSettings { + method, + unit, + min_amount: Some(limits.melt_min), + max_amount: Some(limits.melt_max), + }; + + let mut nut19_settings = self.mint_info.nuts.nut19.unwrap_or_default(); + nut19_settings.methods.push(melt_method_settings); + nut19_settings.disabled = false; + + self.mint_info.nuts.nut19 = Some(nut19_settings); + } + _ => panic!("Unsupported unit"), + } + + ln.insert(ln_key, ln_backend); + + let mut supported_units = self.supported_units.clone(); + + supported_units.insert(ln_key.unit, (0, 32)); + self.supported_units = supported_units; + + self.ln = Some(ln); + + self + } + + /// Set quote ttl + pub fn with_quote_ttl(mut self, mint_ttl: u64, melt_ttl: u64) -> Self { + let quote_ttl = QuoteTTL { mint_ttl, melt_ttl }; + + self.quote_ttl = Some(quote_ttl); + + self + } + + /// Set pubkey + pub fn with_pubkey(mut self, pubkey: cdk::nuts::PublicKey) -> Self { + self.mint_info.pubkey = Some(pubkey); + + self + } + + pub async fn build(&self) -> anyhow::Result { + Ok(Mint::new( + self.mint_url.as_ref().ok_or(anyhow!("Mint url not set"))?, + self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?, + self.mint_info.clone(), + self.quote_ttl.ok_or(anyhow!("Quote ttl not set"))?, + self.localstore + .clone() + .ok_or(anyhow!("Localstore not set"))?, + self.ln.clone().ok_or(anyhow!("Ln backends not set"))?, + self.supported_units.clone(), + ) + .await?) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct MintMeltLimits { + pub mint_min: Amount, + pub mint_max: Amount, + pub melt_min: Amount, + pub melt_max: Amount, +} diff --git a/crates/cdk-mintd/src/setup.rs b/crates/cdk-mintd/src/setup.rs new file mode 100644 index 000000000..a88e1ded3 --- /dev/null +++ b/crates/cdk-mintd/src/setup.rs @@ -0,0 +1,229 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use anyhow::{anyhow, bail}; +use axum::{async_trait, Router}; + +use cdk::{cdk_lightning::MintLightning, mint::FeeReserve, mint_url::MintUrl, nuts::CurrencyUnit}; +use tokio::sync::Mutex; +use url::Url; + +use crate::{ + config::{self, Settings}, + expand_path, +}; + +#[async_trait] +pub trait LnBackendSetup { + async fn setup( + &self, + routers: &mut Vec, + settings: &Settings, + unit: CurrencyUnit, + ) -> anyhow::Result; +} + +#[async_trait] +impl LnBackendSetup for config::Cln { + async fn setup( + &self, + _routers: &mut Vec, + _settings: &Settings, + _unit: CurrencyUnit, + ) -> anyhow::Result { + let cln_socket = expand_path( + self.rpc_path + .to_str() + .ok_or(anyhow!("cln socket not defined"))?, + ) + .ok_or(anyhow!("cln socket not defined"))?; + + let fee_reserve = FeeReserve { + min_fee_reserve: self.reserve_fee_min, + percent_fee_reserve: self.fee_percent, + }; + + let cln = cdk_cln::Cln::new(cln_socket, fee_reserve, true, true).await?; + + Ok(cln) + } +} + +#[async_trait] +impl LnBackendSetup for config::Strike { + async fn setup( + &self, + routers: &mut Vec, + settings: &Settings, + unit: CurrencyUnit, + ) -> anyhow::Result { + let api_key = &self.api_key; + + // Channel used for strike web hook + let (sender, receiver) = tokio::sync::mpsc::channel(8); + let webhook_endpoint = format!("/webhook/{}/invoice", unit); + + let mint_url: MintUrl = settings.info.url.parse()?; + let webhook_url = mint_url.join(&webhook_endpoint)?; + + let strike = cdk_strike::Strike::new( + api_key.clone(), + unit, + Arc::new(Mutex::new(Some(receiver))), + webhook_url.to_string(), + ) + .await?; + + let router = strike + .create_invoice_webhook(&webhook_endpoint, sender) + .await?; + routers.push(router); + + Ok(strike) + } +} + +#[async_trait] +impl LnBackendSetup for config::LNbits { + async fn setup( + &self, + routers: &mut Vec, + settings: &Settings, + _unit: CurrencyUnit, + ) -> anyhow::Result { + let admin_api_key = &self.admin_api_key; + let invoice_api_key = &self.invoice_api_key; + + // Channel used for lnbits web hook + let (sender, receiver) = tokio::sync::mpsc::channel(8); + let webhook_endpoint = "/webhook/lnbits/sat/invoice"; + + let mint_url: MintUrl = settings.info.url.parse()?; + let webhook_url = mint_url.join(webhook_endpoint)?; + + let fee_reserve = FeeReserve { + min_fee_reserve: self.reserve_fee_min, + percent_fee_reserve: self.fee_percent, + }; + + let lnbits = cdk_lnbits::LNbits::new( + admin_api_key.clone(), + invoice_api_key.clone(), + self.lnbits_api.clone(), + fee_reserve, + Arc::new(Mutex::new(Some(receiver))), + webhook_url.to_string(), + ) + .await?; + + let router = lnbits + .create_invoice_webhook_router(webhook_endpoint, sender) + .await?; + + routers.push(router); + + Ok(lnbits) + } +} + +#[async_trait] +impl LnBackendSetup for config::Phoenixd { + async fn setup( + &self, + routers: &mut Vec, + settings: &Settings, + _unit: CurrencyUnit, + ) -> anyhow::Result { + let api_password = &self.api_password; + + let api_url = &self.api_url; + + let fee_reserve = FeeReserve { + min_fee_reserve: self.reserve_fee_min, + percent_fee_reserve: self.fee_percent, + }; + + if fee_reserve.percent_fee_reserve < 0.04 { + bail!("Fee reserve is too low needs to be at least 0.02"); + } + + let webhook_endpoint = "/webhook/phoenixd"; + + let mint_url = Url::parse(&settings.info.url)?; + + let webhook_url = mint_url.join(webhook_endpoint)?.to_string(); + + let (sender, receiver) = tokio::sync::mpsc::channel(8); + + let phoenixd = cdk_phoenixd::Phoenixd::new( + api_password.to_string(), + api_url.to_string(), + fee_reserve, + Arc::new(Mutex::new(Some(receiver))), + webhook_url, + )?; + + let router = phoenixd + .create_invoice_webhook(webhook_endpoint, sender) + .await?; + + routers.push(router); + + Ok(phoenixd) + } +} + +#[async_trait] +impl LnBackendSetup for config::Lnd { + async fn setup( + &self, + _routers: &mut Vec, + _settings: &Settings, + _unit: CurrencyUnit, + ) -> anyhow::Result { + let address = &self.address; + let cert_file = &self.cert_file; + let macaroon_file = &self.macaroon_file; + + let fee_reserve = FeeReserve { + min_fee_reserve: self.reserve_fee_min, + percent_fee_reserve: self.fee_percent, + }; + + let lnd = cdk_lnd::Lnd::new( + address.to_string(), + cert_file.clone(), + macaroon_file.clone(), + fee_reserve, + ) + .await?; + + Ok(lnd) + } +} + +#[async_trait] +impl LnBackendSetup for config::FakeWallet { + async fn setup( + &self, + _router: &mut Vec, + _settings: &Settings, + _unit: CurrencyUnit, + ) -> anyhow::Result { + let fee_reserve = FeeReserve { + min_fee_reserve: self.reserve_fee_min, + percent_fee_reserve: self.fee_percent, + }; + + let fake_wallet = cdk_fake_wallet::FakeWallet::new( + fee_reserve, + HashMap::default(), + HashSet::default(), + 0, + ); + + Ok(fake_wallet) + } +} diff --git a/crates/cdk-phoenixd/Cargo.toml b/crates/cdk-phoenixd/Cargo.toml index cfeb98b81..a8eecade6 100644 --- a/crates/cdk-phoenixd/Cargo.toml +++ b/crates/cdk-phoenixd/Cargo.toml @@ -20,6 +20,7 @@ tokio = { version = "1", default-features = false } tokio-util = { version = "0.7.11", default-features = false } tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } thiserror = "1" +lightning = { version = "0.0.125", default-features = false, features = ["std"]} # phoenixd-rs = "0.3.0" -phoenixd-rs = { git = "https://github.com/thesimplekid/phoenixd-rs", rev = "22a44f0"} +phoenixd-rs = { git = "https://github.com/thesimplekid/phoenixd-rs", rev = "91dc766f"} uuid = { version = "1", features = ["v4"] } diff --git a/crates/cdk-phoenixd/src/error.rs b/crates/cdk-phoenixd/src/error.rs index 85e56c4eb..955343940 100644 --- a/crates/cdk-phoenixd/src/error.rs +++ b/crates/cdk-phoenixd/src/error.rs @@ -17,6 +17,12 @@ pub enum Error { /// phd error #[error(transparent)] Phd(#[from] phoenixd_rs::Error), + /// Wrong invoice type + #[error("Wrong invoice type")] + WrongRequestType, + /// Unsupported method + #[error("Unsupported method")] + UnsupportedMethod, /// Anyhow error #[error(transparent)] Anyhow(#[from] anyhow::Error), diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index 4aecfac6b..e5a8a9052 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -4,24 +4,28 @@ #![warn(rustdoc::bare_urls)] use std::pin::Pin; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use anyhow::anyhow; use async_trait::async_trait; use axum::Router; -use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; +use cdk::amount::{amount_for_offer, to_unit, Amount}; use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, + PayInvoiceResponse, PaymentQuoteResponse, Settings, }; +use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, + CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, }; +use cdk::util::hex; use cdk::{mint, Bolt11Invoice}; use error::Error; use futures::{Stream, StreamExt}; +use lightning::offers::offer::Offer; use phoenixd_rs::webhooks::WebhookResponse; use phoenixd_rs::{InvoiceRequest, Phoenixd as PhoenixdApi}; use tokio::sync::Mutex; @@ -32,8 +36,6 @@ pub mod error; /// Phoenixd #[derive(Clone)] pub struct Phoenixd { - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, phoenixd_api: PhoenixdApi, fee_reserve: FeeReserve, receiver: Arc>>>, @@ -47,16 +49,12 @@ impl Phoenixd { pub fn new( api_password: String, api_url: String, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, fee_reserve: FeeReserve, receiver: Arc>>>, webhook_url: String, ) -> Result { let phoenixd = PhoenixdApi::new(&api_password, &api_url)?; Ok(Self { - mint_settings, - melt_settings, phoenixd_api: phoenixd, fee_reserve, receiver, @@ -86,8 +84,8 @@ impl MintLightning for Phoenixd { Settings { mpp: false, unit: CurrencyUnit::Sat, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + bolt12_mint: false, + bolt12_melt: true, invoice_description: true, } } @@ -103,7 +101,7 @@ impl MintLightning for Phoenixd { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self .receiver .lock() @@ -138,7 +136,7 @@ impl MintLightning for Phoenixd { Ok(state) => { if state.is_paid { // Yield the payment hash and continue the stream - Some((msg.payment_hash, (receiver, phoenixd_api, cancel_token, is_active))) + Some(((msg.payment_hash, Amount::ZERO), (receiver, phoenixd_api, cancel_token, is_active))) } else { // Invoice not paid yet, continue waiting // We need to continue the stream, so we return the same state @@ -210,9 +208,14 @@ impl MintLightning for Phoenixd { partial_amount: Option, _max_fee_msats: Option, ) -> Result { + let bolt11 = &match melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => bolt11, + PaymentRequest::Bolt12 { .. } => return Err(Error::WrongRequestType.into()), + }; + let pay_response = self .phoenixd_api - .pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into())) + .pay_bolt11_invoice(&bolt11.to_string(), partial_amount.map(|a| a.into())) .await?; // The pay invoice response does not give the needed fee info so we have to check. @@ -220,12 +223,10 @@ impl MintLightning for Phoenixd { .check_outgoing_payment(&pay_response.payment_id) .await?; - let bolt11: Bolt11Invoice = melt_quote.request.parse()?; - Ok(PayInvoiceResponse { payment_lookup_id: bolt11.payment_hash().to_string(), payment_preimage: Some(pay_response.payment_preimage), - status: MeltQuoteState::Paid, + status: check_outgoing_response.status, total_spent: check_outgoing_response.total_spent, unit: CurrencyUnit::Sat, }) @@ -279,6 +280,19 @@ impl MintLightning for Phoenixd { &self, payment_id: &str, ) -> Result { + // We can only check the status of the payment if we have the payment id not if we only have a payment hash. + // In phd this is a uuid, that we get after getting a response from the pay invoice + if let Err(_err) = uuid::Uuid::from_str(payment_id) { + tracing::warn!("Could not check status of payment, no payment id"); + return Ok(PayInvoiceResponse { + payment_lookup_id: payment_id.to_string(), + payment_preimage: None, + status: MeltQuoteState::Unknown, + total_spent: Amount::ZERO, + unit: CurrencyUnit::Sat, + }); + } + let res = self.phoenixd_api.get_outgoing_invoice(payment_id).await; let state = match res { @@ -288,13 +302,11 @@ impl MintLightning for Phoenixd { false => MeltQuoteState::Unpaid, }; - let total_spent = res.sent + (res.fees + 999) / MSAT_IN_SAT; - PayInvoiceResponse { payment_lookup_id: res.payment_hash, payment_preimage: Some(res.preimage), status, - total_spent: total_spent.into(), + total_spent: res.sent.into(), unit: CurrencyUnit::Sat, } } @@ -314,4 +326,97 @@ impl MintLightning for Phoenixd { Ok(state) } + + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + if CurrencyUnit::Sat != melt_quote_request.unit { + return Err(Error::UnsupportedUnit.into()); + } + + let offer = Offer::from_str(&melt_quote_request.request) + .map_err(|_| Error::Anyhow(anyhow!("Invalid offer")))?; + + let amount = match melt_quote_request.amount { + Some(amount) => amount, + None => amount_for_offer(&offer, &CurrencyUnit::Sat)?, + }; + + let relative_fee_reserve = + (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; + + let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); + + let mut fee = match relative_fee_reserve > absolute_fee_reserve { + true => relative_fee_reserve, + false => absolute_fee_reserve, + }; + + // Fee in phoenixd is always 0.04 + 4 sat + fee += 4; + + Ok(Bolt12PaymentQuoteResponse { + request_lookup_id: hex::encode(offer.id().0), + amount, + fee: fee.into(), + state: MeltQuoteState::Unpaid, + invoice: None, + }) + } + + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + amount: Option, + _max_fee_amount: Option, + ) -> Result { + let offer = &match melt_quote.request { + PaymentRequest::Bolt12 { offer, invoice: _ } => offer, + PaymentRequest::Bolt11 { .. } => return Err(Error::WrongRequestType.into()), + }; + + let amount = match amount { + Some(amount) => amount, + None => amount_for_offer(offer, &CurrencyUnit::Sat)?, + }; + + let pay_response = self + .phoenixd_api + .pay_bolt12_offer(offer.to_string(), amount.into(), None) + .await?; + + // The pay invoice response does not give the needed fee info so we have to check. + let check_outgoing_response = self + .check_outgoing_payment(&pay_response.payment_id) + .await?; + + tracing::debug!( + "Phd offer {} with amount {} with fee {} total spent {}", + check_outgoing_response.status, + amount, + check_outgoing_response.total_spent - amount, + check_outgoing_response.total_spent + ); + + Ok(PayInvoiceResponse { + payment_lookup_id: pay_response.payment_id, + payment_preimage: Some(pay_response.payment_preimage), + status: check_outgoing_response.status, + total_spent: check_outgoing_response.total_spent, + unit: CurrencyUnit::Sat, + }) + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + _amount: Option, + _unit: &CurrencyUnit, + _description: String, + _unix_expiry: u64, + _single_use: bool, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } } diff --git a/crates/cdk-redb/Cargo.toml b/crates/cdk-redb/Cargo.toml index d7ec6b509..a1a5908b7 100644 --- a/crates/cdk-redb/Cargo.toml +++ b/crates/cdk-redb/Cargo.toml @@ -17,6 +17,7 @@ wallet = ["cdk/wallet"] [dependencies] async-trait = "0.1" +anyhow = "1" cdk = { path = "../cdk", version = "0.4.0", default-features = false } redb = "2.1.0" thiserror = "1" diff --git a/crates/cdk-redb/src/error.rs b/crates/cdk-redb/src/error.rs index 556adb78f..d2a08283d 100644 --- a/crates/cdk-redb/src/error.rs +++ b/crates/cdk-redb/src/error.rs @@ -58,6 +58,9 @@ pub enum Error { /// Unknown Database Version #[error("Unknown database version")] UnknownDatabaseVersion, + /// Anyhow error + #[error(transparent)] + Anyhow(#[from] anyhow::Error), } impl From for cdk::cdk_database::Error { diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index 90feaeffb..0f8101406 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -3,15 +3,16 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; +use cdk::mint::types::PaymentRequest; use cdk::mint::MintQuote; use cdk::mint_url::MintUrl; -use cdk::nuts::{CurrencyUnit, MintQuoteState, Proof, State}; +use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, Proof, State}; use cdk::Amount; use lightning_invoice::Bolt11Invoice; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use serde::{Deserialize, Serialize}; -use super::{Error, PROOFS_STATE_TABLE, PROOFS_TABLE, QUOTE_SIGNATURES_TABLE}; +use super::{Error, MELT_QUOTES_TABLE, PROOFS_STATE_TABLE, PROOFS_TABLE, QUOTE_SIGNATURES_TABLE}; const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes"); const PENDING_PROOFS_TABLE: TableDefinition<[u8; 33], &str> = @@ -29,6 +30,7 @@ pub fn migrate_02_to_03(db: Arc) -> Result { migrate_mint_proofs_02_to_03(db)?; Ok(3) } + pub fn migrate_03_to_04(db: Arc) -> Result { let write_txn = db.begin_write()?; let _ = write_txn.open_multimap_table(QUOTE_PROOFS_TABLE)?; @@ -36,6 +38,118 @@ pub fn migrate_03_to_04(db: Arc) -> Result { Ok(4) } +/// Melt Quote Info +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct V04MeltQuote { + /// 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, + /// Value used by ln backend to look up state of request + pub request_lookup_id: String, +} + +/// Melt Quote Info +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct V05MeltQuote { + /// Quote id + pub id: String, + /// Quote unit + pub unit: CurrencyUnit, + /// Quote amount + pub amount: Amount, + /// Quote Payment request e.g. bolt11 + pub request: PaymentRequest, + /// 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, + /// Value used by ln backend to look up state of request + pub request_lookup_id: String, +} + +impl TryFrom for V05MeltQuote { + type Error = anyhow::Error; + fn try_from(melt_quote: V04MeltQuote) -> anyhow::Result { + let V04MeltQuote { + id, + unit, + amount, + request, + fee_reserve, + state, + expiry, + payment_preimage, + request_lookup_id, + } = melt_quote; + + let bolt11 = Bolt11Invoice::from_str(&request)?; + + let payment_request = PaymentRequest::Bolt11 { bolt11 }; + + Ok(V05MeltQuote { + id, + unit, + amount, + request: payment_request, + fee_reserve, + state, + expiry, + payment_preimage, + request_lookup_id, + }) + } +} + +pub fn migrate_04_to_05(db: Arc) -> anyhow::Result { + let quotes: Vec<_>; + { + let read_txn = db.begin_write()?; + let table = read_txn.open_table(MELT_QUOTES_TABLE)?; + + quotes = table + .iter()? + .flatten() + .map(|(k, v)| (k.value().to_string(), v.value().to_string())) + .collect(); + } + + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(MELT_QUOTES_TABLE)?; + + for (quote_id, quote) in quotes { + let melt_quote: V04MeltQuote = serde_json::from_str("e)?; + + let v05_melt_quote: V05MeltQuote = melt_quote.try_into()?; + + table.insert( + quote_id.as_str(), + serde_json::to_string(&v05_melt_quote)?.as_str(), + )?; + } + } + write_txn.commit()?; + + Ok(5) +} + /// Mint Quote Info #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] struct V1MintQuote { @@ -53,12 +167,16 @@ impl From for MintQuote { MintQuote { id: quote.id, mint_url: quote.mint_url, - amount: quote.amount, + amount: Some(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(), + // TODO: Create real migrations + amount_paid: Amount::ZERO, + amount_issued: Amount::ZERO, + single_use: None, } } } diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index 0ca822e9f..291215314 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -22,7 +22,7 @@ use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use super::error::Error; use crate::migrations::migrate_00_to_01; -use crate::mint::migrations::{migrate_02_to_03, migrate_03_to_04}; +use crate::mint::migrations::{migrate_02_to_03, migrate_03_to_04, migrate_04_to_05}; mod migrations; @@ -43,7 +43,7 @@ const QUOTE_SIGNATURES_TABLE: MultimapTableDefinition<&str, [u8; 33]> = const MELT_REQUESTS: TableDefinition<&str, (&str, &str)> = TableDefinition::new("melt_requests"); -const DATABASE_VERSION: u32 = 4; +const DATABASE_VERSION: u32 = 5; /// Mint Redbdatabase #[derive(Debug, Clone)] @@ -93,6 +93,10 @@ impl MintRedbDatabase { current_file_version = migrate_03_to_04(Arc::clone(&db))?; } + if current_file_version == 4 { + current_file_version = migrate_04_to_05(Arc::clone(&db))?; + } + if current_file_version != DATABASE_VERSION { tracing::warn!( "Database upgrade did not complete at {} current is {}", diff --git a/crates/cdk-sqlite/src/mint/error.rs b/crates/cdk-sqlite/src/mint/error.rs index 5f2be0907..10dc7a5bb 100644 --- a/crates/cdk-sqlite/src/mint/error.rs +++ b/crates/cdk-sqlite/src/mint/error.rs @@ -41,6 +41,9 @@ pub enum Error { /// Invalid Database Path #[error("Invalid database path")] InvalidDbPath, + /// Invalid bolt11 + #[error("Invalid bolt11")] + InvalidBolt11, /// Serde Error #[error(transparent)] Serde(#[from] serde_json::Error), diff --git a/crates/cdk-sqlite/src/mint/migrations/20241002093700_unknown_status_for_quote.sql b/crates/cdk-sqlite/src/mint/migrations/20241002093700_unknown_status_for_quote.sql new file mode 100644 index 000000000..1f9242361 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20241002093700_unknown_status_for_quote.sql @@ -0,0 +1,23 @@ +-- Create a new table with the updated CHECK constraint +CREATE TABLE melt_quote_new ( + id TEXT PRIMARY KEY, + unit TEXT NOT NULL, + amount INTEGER NOT NULL, + request TEXT NOT NULL, + fee_reserve INTEGER NOT NULL, + expiry INTEGER NOT NULL, + state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'UNKNOWN') ) NOT NULL DEFAULT 'UNPAID', + payment_preimage TEXT, + request_lookup_id TEXT +); + +-- Copy the data from the old table to the new table +INSERT INTO melt_quote_new (id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id) +SELECT id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id +FROM melt_quote; + +-- Drop the old table +DROP TABLE melt_quote; + +-- Rename the new table to the original table name +ALTER TABLE melt_quote_new RENAME TO melt_quote; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index c72e5220a..121d43dde 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -8,6 +8,7 @@ use std::time::Duration; use async_trait::async_trait; use bitcoin::bip32::DerivationPath; use cdk::cdk_database::{self, MintDatabase}; +use cdk::mint::types::PaymentRequest; use cdk::mint::{MintKeySetInfo, MintQuote}; use cdk::mint_url::MintUrl; use cdk::nuts::nut00::ProofsMethods; @@ -211,7 +212,8 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?); ) .bind(quote.id.to_string()) .bind(quote.mint_url.to_string()) - .bind(u64::from(quote.amount) as i64) + // REVIEW: Should this be 0 + .bind(u64::from(quote.amount.unwrap_or(Amount::ZERO)) as i64) .bind(quote.unit.to_string()) .bind(quote.request) .bind(quote.state.to_string()) @@ -472,7 +474,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); .bind(quote.id.to_string()) .bind(quote.unit.to_string()) .bind(u64::from(quote.amount) as i64) - .bind(quote.request) + .bind(serde_json::to_string("e.request)?) .bind(u64::from(quote.fee_reserve) as i64) .bind(quote.state.to_string()) .bind(quote.expiry as i64) @@ -1289,12 +1291,16 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, - amount: Amount::from(row_amount as u64), + amount: Some(Amount::from(row_amount as u64)), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, request: row_request, state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, expiry: row_expiry as u64, request_lookup_id, + // TODO: Get these values + amount_paid: Amount::ZERO, + amount_issued: Amount::ZERO, + single_use: None, }) } @@ -1312,11 +1318,20 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result { let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone()); + let request: PaymentRequest = match serde_json::from_str(&row_request) { + Ok(request) => request, + Err(_) => { + let bolt11 = Bolt11Invoice::from_str(&row_request).map_err(|_| Error::InvalidBolt11)?; + + PaymentRequest::Bolt11 { bolt11 } + } + }; + Ok(mint::MeltQuote { id: row_id, amount: Amount::from(row_amount as u64), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, - request: row_request, + request, fee_reserve: Amount::from(row_fee_reserve as u64), state: QuoteState::from_str(&row_state)?, expiry: row_expiry as u64, diff --git a/crates/cdk-sqlite/src/wallet/migrations/20241001184621_melt_quote_payment_method.sql b/crates/cdk-sqlite/src/wallet/migrations/20241001184621_melt_quote_payment_method.sql new file mode 100644 index 000000000..b0b6a5b3d --- /dev/null +++ b/crates/cdk-sqlite/src/wallet/migrations/20241001184621_melt_quote_payment_method.sql @@ -0,0 +1 @@ +ALTER TABLE melt_quote ADD payment_method TEXT; diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index f45c6bb44..bb740eca3 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -9,8 +9,8 @@ use cdk::amount::Amount; use cdk::cdk_database::{self, WalletDatabase}; use cdk::mint_url::MintUrl; use cdk::nuts::{ - CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, PublicKey, - SpendingConditions, State, + CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, PaymentMethod, + Proof, PublicKey, SpendingConditions, State, }; use cdk::secret::Secret; use cdk::types::ProofInfo; @@ -419,8 +419,8 @@ WHERE id=? sqlx::query( r#" INSERT OR REPLACE INTO melt_quote -(id, unit, amount, request, fee_reserve, state, expiry) -VALUES (?, ?, ?, ?, ?, ?, ?); +(id, unit, amount, request, fee_reserve, state, expiry, payment_method) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -430,6 +430,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?); .bind(u64::from(quote.fee_reserve) as i64) .bind(quote.state.to_string()) .bind(quote.expiry as i64) + .bind(quote.payment_method.to_string()) .execute(&self.pool) .await .map_err(Error::from)?; @@ -838,6 +839,9 @@ fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result 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_preimage: Option = row.try_get("payment_preimage").map_err(Error::from)?; + let row_payment_method: Option = row.try_get("payment_method").map_err(Error::from)?; + + let payment_method = row_payment_method.and_then(|p| PaymentMethod::from_str(&p).ok()); let state = MeltQuoteState::from_str(&row_state)?; Ok(wallet::MeltQuote { @@ -845,6 +849,7 @@ fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result amount: Amount::from(row_amount as u64), unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?, request: row_request, + payment_method: payment_method.unwrap_or_default(), fee_reserve: Amount::from(row_fee_reserve as u64), state, expiry: row_expiry as u64, diff --git a/crates/cdk-strike/src/error.rs b/crates/cdk-strike/src/error.rs index b9915d8d8..a7316e95d 100644 --- a/crates/cdk-strike/src/error.rs +++ b/crates/cdk-strike/src/error.rs @@ -14,6 +14,9 @@ pub enum Error { /// Strikers error #[error(transparent)] StrikeRs(#[from] strike_rs::Error), + /// Unsupported method + #[error("Unsupported method")] + UnsupportedMethod, /// Anyhow error #[error(transparent)] Anyhow(#[from] anyhow::Error), diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 76eb0f256..7a0251f78 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -12,11 +12,11 @@ use async_trait::async_trait; use axum::Router; use cdk::amount::Amount; use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, + PayInvoiceResponse, PaymentQuoteResponse, Settings, }; use cdk::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, + CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, }; use cdk::util::unix_time; use cdk::{mint, Bolt11Invoice}; @@ -37,8 +37,6 @@ pub mod error; #[derive(Clone)] pub struct Strike { strike_api: StrikeApi, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, unit: CurrencyUnit, receiver: Arc>>>, webhook_url: String, @@ -50,8 +48,6 @@ impl Strike { /// Create new [`Strike`] wallet pub async fn new( api_key: String, - mint_settings: MintMethodSettings, - melt_settings: MeltMethodSettings, unit: CurrencyUnit, receiver: Arc>>>, webhook_url: String, @@ -59,8 +55,6 @@ impl Strike { let strike = StrikeApi::new(&api_key, None)?; Ok(Self { strike_api: strike, - mint_settings, - melt_settings, receiver, unit, webhook_url, @@ -78,8 +72,8 @@ impl MintLightning for Strike { Settings { mpp: false, unit: self.unit, - mint_settings: self.mint_settings, - melt_settings: self.melt_settings, + bolt12_mint: false, + bolt12_melt: false, invoice_description: true, } } @@ -95,7 +89,7 @@ impl MintLightning for Strike { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { self.strike_api .subscribe_to_invoice_webhook(self.webhook_url.clone()) .await?; @@ -135,7 +129,7 @@ impl MintLightning for Strike { match check { Ok(state) => { if state.state == InvoiceState::Paid { - Some((msg, (receiver, strike_api, cancel_token, is_active))) + Some(((msg, Amount::ZERO), (receiver, strike_api, cancel_token, is_active))) } else { None } @@ -307,6 +301,34 @@ impl MintLightning for Strike { Ok(pay_invoice_response) } + + async fn get_bolt12_payment_quote( + &self, + _melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } + + async fn pay_bolt12_offer( + &self, + _melt_quote: mint::MeltQuote, + _amount: Option, + _max_fee_amount: Option, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + _amount: Option, + _unit: &CurrencyUnit, + _description: String, + _unix_expiry: u64, + _single_use: bool, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } } impl Strike { diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index e89311a53..22dbb1e4a 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -25,6 +25,7 @@ bitcoin = { version= "0.32.2", features = ["base64", "serde", "rand", "rand-std" ciborium = { version = "0.2.2", default-features = false, features = ["std"] } cbor-diag = "0.1.12" lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } +lightning = { version = "0.0.125", default-features = false, features = ["std"]} once_cell = "1.19" regex = "1" reqwest = { version = "0.12", default-features = false, features = [ diff --git a/crates/cdk/src/amount.rs b/crates/cdk/src/amount.rs index d2c599d04..087d01784 100644 --- a/crates/cdk/src/amount.rs +++ b/crates/cdk/src/amount.rs @@ -7,6 +7,8 @@ use std::fmt; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use lightning::offers::offer::Offer; use thiserror::Error; use crate::nuts::CurrencyUnit; @@ -23,6 +25,12 @@ pub enum Error { /// Cannot convert units #[error("Cannot convert units")] CannotConvertUnits, + /// Amount undefinded + #[error("Amount undefined")] + AmountUndefinded, + /// Utf8 parse error + #[error(transparent)] + Utf8ParseError(#[from] std::string::FromUtf8Error), } /// Amount can be any unit @@ -300,6 +308,27 @@ where } } +/// Convert offer to amount in unit +pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result { + let offer_amount = offer.amount().ok_or(Error::AmountUndefinded)?; + + let (amount, currency) = match offer_amount { + lightning::offers::offer::Amount::Bitcoin { amount_msats } => { + (amount_msats, CurrencyUnit::Msat) + } + lightning::offers::offer::Amount::Currency { + iso4217_code, + amount, + } => ( + amount, + CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?) + .map_err(|_| Error::CannotConvertUnits)?, + ), + }; + + to_unit(amount, ¤cy, unit).map_err(|_err| Error::CannotConvertUnits) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index eb94fb4b2..1c0d58de9 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -4,14 +4,13 @@ use std::pin::Pin; use async_trait::async_trait; use futures::Stream; +use lightning::offers::offer::Offer; use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::nuts::{ - CurrencyUnit, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteState, MintMethodSettings, - MintQuoteState, -}; +use crate::nuts::nut20::MeltQuoteBolt12Request; +use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use crate::{mint, Amount}; /// CDK Lightning Error @@ -23,12 +22,18 @@ pub enum Error { /// Invoice pay pending #[error("Invoice pay is pending")] InvoicePaymentPending, + /// Invoice amount unknown + #[error("Invoice amount unknown")] + InvoiceAmountUnknown, /// Unsupported unit #[error("Unsupported unit")] UnsupportedUnit, /// Payment state is unknown #[error("Payment state is unknown")] UnknownPaymentState, + /// Utf8 parse error + #[error(transparent)] + Utf8ParseError(#[from] std::string::FromUtf8Error), /// Lightning Error #[error(transparent)] Lightning(Box), @@ -83,7 +88,7 @@ pub trait MintLightning { /// Returns a stream of request_lookup_id once invoices are paid async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err>; + ) -> Result + Send>>, Self::Err>; /// Is wait invoice active fn is_wait_invoice_active(&self) -> bool; @@ -102,10 +107,34 @@ pub trait MintLightning { &self, request_lookup_id: &str, ) -> Result; + + /// Bolt12 Payment quote + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result; + + /// Pay a bolt12 offer + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + amount: Option, + max_fee_amount: Option, + ) -> Result; + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + amount: Option, + unit: &CurrencyUnit, + description: String, + unix_expiry: u64, + single_use: bool, + ) -> Result; } /// Create invoice response -#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct CreateInvoiceResponse { /// Id that is used to look up the invoice from the ln backend pub request_lookup_id: String, @@ -115,6 +144,17 @@ pub struct CreateInvoiceResponse { pub expiry: Option, } +/// Create offer response +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct CreateOfferResponse { + /// Id that is used to look up the invoice from the ln backend + pub request_lookup_id: String, + /// Bolt11 payment request + pub request: Offer, + /// Unix Expiry of Invoice + pub expiry: Option, +} + /// Pay invoice response #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct PayInvoiceResponse { @@ -143,15 +183,30 @@ pub struct PaymentQuoteResponse { pub state: MeltQuoteState, } -/// Ln backend settings +/// Payment quote response #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bolt12PaymentQuoteResponse { + /// Request look up id + pub request_lookup_id: String, + /// Amount + pub amount: Amount, + /// Fee required for melt + pub fee: Amount, + /// Status + pub state: MeltQuoteState, + /// Bolt12 invoice + pub invoice: Option, +} + +/// Ln backend settings +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Settings { /// MPP supported pub mpp: bool, - /// Min amount to mint - pub mint_settings: MintMethodSettings, - /// Max amount to mint - pub melt_settings: MeltMethodSettings, + /// Supports bolt12 mint + pub bolt12_mint: bool, + /// Supports bolt12 melt + pub bolt12_melt: bool, /// Base unit of backend pub unit: CurrencyUnit, /// Invoice Description supported diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 1695a58c7..61c2aaadf 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -193,6 +193,9 @@ pub enum Error { /// From hex error #[error(transparent)] ReqwestError(#[from] reqwest::Error), + /// Bolt12 parse error + #[error("BOLT12 Parse error")] + Bolt12Parse, // Crate error conversions /// Cashu Url Error @@ -237,12 +240,24 @@ pub enum Error { /// NUT18 Error #[error(transparent)] NUT18(#[from] crate::nuts::nut18::Error), + /// NUT19 Error + #[error(transparent)] + NUT19(#[from] crate::nuts::nut19::Error), + /// NUT18 Error + #[error(transparent)] + NUT20(#[from] crate::nuts::nut20::Error), /// Database Error #[cfg(any(feature = "wallet", feature = "mint"))] #[error(transparent)] Database(#[from] crate::cdk_database::Error), } +impl From for Error { + fn from(_err: lightning::offers::parse::Bolt12ParseError) -> Error { + Error::Bolt12Parse + } +} + /// CDK Error Response /// /// See NUT definition in [00](https://github.com/cashubtc/nuts/blob/main/00.md) diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 53268b4d2..d94263a90 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -1,10 +1,12 @@ use std::collections::HashSet; use std::str::FromStr; +use std::sync::Arc; use anyhow::bail; -use lightning_invoice::Bolt11Invoice; +use lightning::offers::offer::Offer; use tracing::instrument; +use crate::amount::amount_for_offer; use crate::cdk_lightning; use crate::cdk_lightning::MintLightning; use crate::cdk_lightning::PayInvoiceResponse; @@ -16,9 +18,12 @@ use crate::{ Amount, Error, }; +use super::nut05::MeltRequestTrait; +use super::BlindSignature; +use super::MeltQuoteBolt12Request; use super::{ - CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, - Mint, PaymentMethod, PublicKey, State, + CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, Mint, PaymentMethod, + PaymentRequest, PublicKey, State, }; impl Mint { @@ -95,8 +100,84 @@ impl Mint { Error::UnitUnsupported })?; + let request = PaymentRequest::Bolt11 { + bolt11: request.clone(), + }; + let quote = MeltQuote::new( - request.to_string(), + request, + *unit, + payment_quote.amount, + payment_quote.fee, + unix_time() + self.quote_ttl.melt_ttl, + payment_quote.request_lookup_id.clone(), + ); + + tracing::debug!( + "New melt quote {} for {} {} with request id {}", + quote.id, + amount, + unit, + payment_quote.request_lookup_id + ); + + self.localstore.add_melt_quote(quote.clone()).await?; + + Ok(quote.into()) + } + + /// Get melt bolt12 quote + #[instrument(skip_all)] + pub async fn get_melt_bolt12_quote( + &self, + melt_request: &MeltQuoteBolt12Request, + ) -> Result { + let MeltQuoteBolt12Request { + request, + unit, + amount, + } = melt_request; + + let offer = Offer::from_str(request).unwrap(); + + let amount = match amount { + Some(amount) => *amount, + None => amount_for_offer(&offer, unit).map_err(|_| Error::UnsupportedUnit)?, + }; + + self.check_melt_request_acceptable(amount, *unit, PaymentMethod::Bolt12)?; + + let ln = self + .ln + .get(&LnKey::new(*unit, PaymentMethod::Bolt12)) + .ok_or_else(|| { + tracing::info!("Could not get ln backend for {}, bolt11 ", unit); + + Error::UnitUnsupported + })?; + + let payment_quote = ln + .get_bolt12_payment_quote(melt_request) + .await + .map_err(|err| { + tracing::error!( + "Could not get payment quote for mint quote, {} bolt11, {}", + unit, + err + ); + + Error::UnitUnsupported + })?; + + let offer = Offer::from_str(request)?; + + let payment_request = PaymentRequest::Bolt12 { + offer: Box::new(offer), + invoice: payment_quote.invoice, + }; + + let quote = MeltQuote::new( + payment_request, *unit, payment_quote.amount, payment_quote.fee, @@ -169,70 +250,81 @@ impl Mint { /// Check melt has expected fees #[instrument(skip_all)] - pub async fn check_melt_expected_ln_fees( + pub async fn check_melt_expected_ln_fees( &self, melt_quote: &MeltQuote, - melt_request: &MeltBolt11Request, - ) -> Result, Error> { - let invoice = Bolt11Invoice::from_str(&melt_quote.request)?; - - let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat) - .expect("Quote unit is checked above that it can convert to msat"); - - let invoice_amount_msats: Amount = invoice - .amount_milli_satoshis() - .ok_or(Error::InvoiceAmountUndefined)? - .into(); - - let partial_amount = match invoice_amount_msats > quote_msats { - true => { - let partial_msats = invoice_amount_msats - quote_msats; - - Some( - to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit) + melt_request: &R, + ) -> Result, Error> + where + R: MeltRequestTrait, + { + let quote_amount = melt_quote.amount; + + let request_amount = match &melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => match bolt11.amount_milli_satoshis() { + Some(amount) => Some( + to_unit(amount, &CurrencyUnit::Msat, &melt_quote.unit) .map_err(|_| Error::UnitUnsupported)?, - ) - } - false => None, + ), + None => None, + }, + PaymentRequest::Bolt12 { offer, invoice: _ } => match offer.amount() { + Some(amount) => { + let (amount, currency) = match amount { + lightning::offers::offer::Amount::Bitcoin { amount_msats } => { + (amount_msats, CurrencyUnit::Msat) + } + lightning::offers::offer::Amount::Currency { + iso4217_code, + amount, + } => ( + amount, + CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?)?, + ), + }; + + Some( + to_unit(amount, ¤cy, &melt_quote.unit) + .map_err(|_err| Error::UnsupportedUnit)?, + ) + } + None => None, + }, }; - let amount_to_pay = match partial_amount { - Some(amount_to_pay) => amount_to_pay, - None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &melt_quote.unit) - .map_err(|_| Error::UnitUnsupported)?, - }; + let amount_to_pay = request_amount.unwrap_or(quote_amount); - let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| { - tracing::error!("Proof inputs in melt quote overflowed"); - Error::AmountOverflow - })?; + let inputs_amount = melt_request + .inputs_amount() + .map_err(|_| Error::AmountOverflow)?; - if amount_to_pay + melt_quote.fee_reserve > inputs_amount_quote_unit { + if amount_to_pay + melt_quote.fee_reserve > inputs_amount { tracing::debug!( "Not enough inputs provided: {} msats needed {} msats", - inputs_amount_quote_unit, + inputs_amount, amount_to_pay ); return Err(Error::TransactionUnbalanced( - inputs_amount_quote_unit.into(), + inputs_amount.into(), amount_to_pay.into(), melt_quote.fee_reserve.into(), )); } - Ok(partial_amount) + Ok(Some(amount_to_pay)) } /// Verify melt request is valid #[instrument(skip_all)] - pub async fn verify_melt_request( - &self, - melt_request: &MeltBolt11Request, - ) -> Result { + pub async fn verify_melt_request(&self, melt_request: &R) -> Result + where + R: MeltRequestTrait, + { + let quote_id = melt_request.get_quote_id(); let state = self .localstore - .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Pending) + .update_melt_quote_state(quote_id, MeltQuoteState::Pending) .await?; match state { @@ -242,34 +334,33 @@ impl Mint { MeltQuoteState::Unknown => Err(Error::UnknownPaymentState), }?; - let ys = melt_request.inputs.ys()?; + let inputs = melt_request.get_inputs(); + + let ys = inputs.ys()?; // Ensure proofs are unique and not being double spent - if melt_request.inputs.len() != ys.iter().collect::>().len() { + if inputs.len() != ys.iter().collect::>().len() { return Err(Error::DuplicateProofs); } self.localstore - .add_proofs( - melt_request.inputs.clone(), - Some(melt_request.quote.clone()), - ) + .add_proofs(inputs.clone(), Some(quote_id.to_string())) .await?; self.check_ys_spendable(&ys, State::Pending).await?; - for proof in &melt_request.inputs { + for proof in inputs.iter() { self.verify_proof(proof).await?; } let quote = self .localstore - .get_melt_quote(&melt_request.quote) + .get_melt_quote(quote_id) .await? .ok_or(Error::UnknownQuote)?; - let proofs_total = melt_request.proofs_amount()?; + let proofs_total = Amount::try_sum(inputs.iter().map(|p| p.amount))?; - let fee = self.get_proofs_fee(&melt_request.inputs).await?; + let fee = self.get_proofs_fee(inputs).await?; let required_total = quote.amount + quote.fee_reserve + fee; @@ -289,8 +380,7 @@ impl Mint { )); } - let input_keyset_ids: HashSet = - melt_request.inputs.iter().map(|p| p.keyset_id).collect(); + let input_keyset_ids: HashSet = inputs.iter().map(|p| p.keyset_id).collect(); let mut keyset_units = HashSet::with_capacity(input_keyset_ids.capacity()); @@ -303,13 +393,15 @@ impl Mint { keyset_units.insert(keyset.unit); } - let EnforceSigFlag { sig_flag, .. } = enforce_sig_flag(melt_request.inputs.clone()); + let EnforceSigFlag { sig_flag, .. } = enforce_sig_flag(inputs.clone()); if sig_flag.eq(&SigFlag::SigAll) { return Err(Error::SigAllUsedInMelt); } - if let Some(outputs) = &melt_request.outputs { + let outputs = melt_request.get_outputs(); + + if let Some(outputs) = outputs { let output_keysets_ids: HashSet = outputs.iter().map(|b| b.keyset_id).collect(); for id in output_keysets_ids { let keyset = self @@ -338,7 +430,7 @@ impl Mint { return Err(Error::MultipleUnits); } - tracing::debug!("Verified melt quote: {}", melt_request.quote); + tracing::debug!("Verified melt quote: {}", quote_id); Ok(quote) } @@ -347,48 +439,47 @@ impl Mint { /// made The [`Proofs`] should be returned to an unspent state and the /// quote should be unpaid #[instrument(skip_all)] - pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> { - let input_ys = melt_request.inputs.ys()?; - + pub async fn process_unpaid_melt(&self, melt_request: &R) -> Result<(), Error> + where + R: MeltRequestTrait, + { + let inputs = melt_request.get_inputs(); + let input_ys = inputs.ys()?; self.localstore .update_proofs_states(&input_ys, State::Unspent) .await?; self.localstore - .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Unpaid) + .update_melt_quote_state(melt_request.get_quote_id(), MeltQuoteState::Unpaid) .await?; Ok(()) } - /// Melt Bolt11 - #[instrument(skip_all)] - pub async fn melt_bolt11( - &self, - melt_request: &MeltBolt11Request, - ) -> Result { - use std::sync::Arc; - async fn check_payment_state( - ln: Arc + Send + Sync>, - melt_quote: &MeltQuote, - ) -> anyhow::Result { - match ln - .check_outgoing_payment(&melt_quote.request_lookup_id) - .await - { - Ok(response) => Ok(response), - Err(check_err) => { - // If we cannot check the status of the payment we keep the proofs stuck as pending. - tracing::error!( - "Could not check the status of payment for {},. Proofs stuck as pending", - melt_quote.id - ); - tracing::error!("Checking payment error: {}", check_err); - bail!("Could not check payment status") - } + async fn check_payment_state( + ln: Arc + Send + Sync>, + request_lookup_id: &str, + ) -> anyhow::Result { + match ln.check_outgoing_payment(request_lookup_id).await { + Ok(response) => Ok(response), + Err(check_err) => { + // If we cannot check the status of the payment we keep the proofs stuck as pending. + tracing::error!( + "Could not check the status of payment for {},. Proofs stuck as pending", + request_lookup_id + ); + tracing::error!("Checking payment error: {}", check_err); + bail!("Could not check payment status") } } + } + /// Melt Bolt11 + #[instrument(skip_all)] + pub async fn melt(&self, melt_request: &R) -> Result + where + R: MeltRequestTrait, + { let quote = match self.verify_melt_request(melt_request).await { Ok(quote) => quote, Err(err) => { @@ -397,7 +488,7 @@ impl Mint { if let Err(err) = self.process_unpaid_melt(melt_request).await { tracing::error!( "Could not reset melt quote {} state: {}", - melt_request.quote, + melt_request.get_quote_id(), err ); } @@ -405,15 +496,19 @@ impl Mint { } }; + let inputs_amount = melt_request + .inputs_amount() + .map_err(|_err| Error::AmountOverflow)?; + let settled_internally_amount = - match self.handle_internal_melt_mint("e, melt_request).await { + match self.handle_internal_melt_mint("e, inputs_amount).await { Ok(amount) => amount, Err(err) => { tracing::error!("Attempting to settle internally failed"); if let Err(err) = self.process_unpaid_melt(melt_request).await { tracing::error!( "Could not reset melt quote {} state: {}", - melt_request.quote, + melt_request.get_quote_id(), err ); } @@ -447,6 +542,7 @@ impl Mint { } _ => None, }; + let ln = match self.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) { Some(ln) => ln, None => { @@ -459,17 +555,26 @@ impl Mint { } }; - let pre = match ln - .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve)) - .await - { + let attempt_to_pay = match melt_request.get_payment_method() { + PaymentMethod::Bolt11 => { + ln.pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve)) + .await + } + PaymentMethod::Bolt12 => { + ln.pay_bolt12_offer(quote.clone(), partial_amount, Some(quote.fee_reserve)) + .await + } + }; + + let pre = match attempt_to_pay { Ok(pay) if pay.status == MeltQuoteState::Unknown || pay.status == MeltQuoteState::Failed => { - let check_response = check_payment_state(Arc::clone(ln), "e) - .await - .map_err(|_| Error::Internal)?; + let check_response = + Self::check_payment_state(Arc::clone(ln), "e.request_lookup_id) + .await + .map_err(|_| Error::Internal)?; if check_response.status == MeltQuoteState::Paid { tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string()); @@ -493,9 +598,10 @@ impl Mint { tracing::error!("Error returned attempting to pay: {} {}", quote.id, err); - let check_response = check_payment_state(Arc::clone(ln), "e) - .await - .map_err(|_| Error::Internal)?; + let check_response = + Self::check_payment_state(Arc::clone(ln), "e.request_lookup_id) + .await + .map_err(|_| Error::Internal)?; // If there error is something else we want to check the status of the payment ensure it is not pending or has been made. if check_response.status == MeltQuoteState::Paid { tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string()); @@ -511,7 +617,7 @@ impl Mint { MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => { tracing::info!( "Lightning payment for quote {} failed.", - melt_request.quote + melt_request.get_quote_id() ); if let Err(err) = self.process_unpaid_melt(melt_request).await { tracing::error!("Could not reset melt quote state: {}", err); @@ -521,7 +627,7 @@ impl Mint { MeltQuoteState::Pending => { tracing::warn!( "LN payment pending, proofs are stuck as pending for quote: {}", - melt_request.quote + melt_request.get_quote_id() ); return Err(Error::PendingQuote); } @@ -542,7 +648,7 @@ impl Mint { payment_lookup_id ); - let mut melt_quote = quote; + let mut melt_quote = quote.clone(); melt_quote.request_lookup_id = payment_lookup_id; if let Err(err) = self.localstore.add_melt_quote(melt_quote).await { @@ -556,51 +662,84 @@ impl Mint { // If we made it here the payment has been made. // We process the melt burning the inputs and returning change - let res = self - .process_melt_request(melt_request, preimage, amount_spent_quote_unit) + let change = self + .process_melt_request(melt_request, amount_spent_quote_unit) .await .map_err(|err| { tracing::error!("Could not process melt request: {}", err); err })?; - Ok(res) + let change_amount: u64 = change + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|b| u64::from(b.amount)) + .sum(); + + tracing::debug!( + "Quote {} paid, quote amount: {}, total paid: {}, change amount: {}", + quote.id, + quote.amount, + amount_spent_quote_unit, + change_amount + ); + + Ok(MeltQuoteBolt11Response { + paid: Some(true), + payment_preimage: preimage, + change, + quote: quote.id, + amount: quote.amount, + fee_reserve: quote.fee_reserve, + state: MeltQuoteState::Paid, + expiry: quote.expiry, + }) } /// Process melt request marking [`Proofs`] as spent /// The melt request must be verifyed using [`Self::verify_melt_request`] /// before calling [`Self::process_melt_request`] #[instrument(skip_all)] - pub async fn process_melt_request( + pub async fn process_melt_request( &self, - melt_request: &MeltBolt11Request, - payment_preimage: Option, + melt_request: &R, total_spent: Amount, - ) -> Result { - tracing::debug!("Processing melt quote: {}", melt_request.quote); + ) -> Result>, Error> + where + R: MeltRequestTrait, + { + let quote_id = melt_request.get_quote_id(); + tracing::debug!("Processing melt quote: {}", quote_id); let quote = self .localstore - .get_melt_quote(&melt_request.quote) + .get_melt_quote(quote_id) .await? .ok_or(Error::UnknownQuote)?; - let input_ys = melt_request.inputs.ys()?; + let inputs = melt_request.get_inputs(); + + let input_ys = inputs.ys()?; self.localstore .update_proofs_states(&input_ys, State::Spent) .await?; self.localstore - .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Paid) + .update_melt_quote_state(quote_id, MeltQuoteState::Paid) .await?; let mut change = None; + let inputs_amount = Amount::try_sum(inputs.iter().map(|p| p.amount))?; + + let outputs = melt_request.get_outputs(); + // Check if there is change to return - if melt_request.proofs_amount()? > total_spent { + if inputs_amount > total_spent { // Check if wallet provided change outputs - if let Some(outputs) = melt_request.outputs.clone() { + if let Some(outputs) = outputs { let blinded_messages: Vec = outputs.iter().map(|b| b.blinded_secret).collect(); @@ -618,7 +757,7 @@ impl Mint { return Err(Error::BlindedMessageAlreadySigned); } - let change_target = melt_request.proofs_amount()? - total_spent; + let change_target = inputs_amount - total_spent; let mut amounts = change_target.split(); let mut change_sigs = Vec::with_capacity(amounts.len()); @@ -635,7 +774,7 @@ impl Mint { amounts.sort_by(|a, b| b.cmp(a)); } - let mut outputs = outputs; + let mut outputs = outputs.clone(); for (amount, blinded_message) in amounts.iter().zip(&mut outputs) { blinded_message.amount = *amount; @@ -659,15 +798,6 @@ impl Mint { } } - Ok(MeltQuoteBolt11Response { - amount: quote.amount, - paid: Some(true), - payment_preimage, - change, - quote: quote.id, - fee_reserve: quote.fee_reserve, - state: MeltQuoteState::Paid, - expiry: quote.expiry, - }) + Ok(change) } } diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs new file mode 100644 index 000000000..ddda8d8ae --- /dev/null +++ b/crates/cdk/src/mint/mint_18.rs @@ -0,0 +1,95 @@ +use tracing::instrument; + +use crate::{types::LnKey, util::unix_time, Amount, Error}; + +use super::{ + nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}, + Mint, MintQuote, PaymentMethod, +}; + +impl Mint { + /// Create new mint bolt11 quote + #[instrument(skip_all)] + pub async fn get_mint_bolt12_quote( + &self, + mint_quote_request: MintQuoteBolt12Request, + ) -> Result { + let MintQuoteBolt12Request { + amount, + unit, + description, + single_use, + expiry, + } = mint_quote_request; + + let nut18 = &self + .mint_info + .nuts + .nut18 + .as_ref() + .ok_or(Error::UnsupportedUnit)?; + + if nut18.disabled { + return Err(Error::MintingDisabled); + } + + let ln = self + .ln + .get(&LnKey::new(unit, PaymentMethod::Bolt12)) + .ok_or_else(|| { + tracing::info!("Bolt11 mint request for unsupported unit"); + + Error::UnitUnsupported + })?; + + let quote_expiry = match expiry { + Some(expiry) => expiry, + None => unix_time() + self.quote_ttl.mint_ttl, + }; + + if description.is_some() && !ln.get_settings().invoice_description { + tracing::error!("Backend does not support invoice description"); + return Err(Error::InvoiceDescriptionUnsupported); + } + + let single_use = single_use.unwrap_or(true); + + let create_invoice_response = ln + .create_bolt12_offer( + amount, + &unit, + description.unwrap_or("".to_string()), + quote_expiry, + single_use, + ) + .await + .map_err(|err| { + tracing::error!("Could not create invoice: {}", err); + Error::InvalidPaymentRequest + })?; + + let quote = MintQuote::new( + self.mint_url.clone(), + create_invoice_response.request.to_string(), + unit, + amount, + create_invoice_response.expiry.unwrap_or(0), + create_invoice_response.request_lookup_id.clone(), + Amount::ZERO, + Amount::ZERO, + Some(single_use), + ); + + tracing::debug!( + "New bolt12 mint quote {} for {} {} with request id {}", + quote.id, + amount.unwrap_or_default(), + unit, + create_invoice_response.request_lookup_id, + ); + + self.localstore.add_mint_quote(quote.clone()).await?; + + Ok(quote.into()) + } +} diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 7d46263db..b2d71aa15 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -12,7 +12,7 @@ impl Mint { fn check_mint_request_acceptable( &self, amount: Amount, - unit: CurrencyUnit, + unit: &CurrencyUnit, ) -> Result<(), Error> { let nut04 = &self.mint_info.nuts.nut04; @@ -20,7 +20,7 @@ impl Mint { return Err(Error::MintingDisabled); } - match nut04.get_settings(&unit, &PaymentMethod::Bolt11) { + match nut04.get_settings(unit, &PaymentMethod::Bolt11) { Some(settings) => { if settings .max_amount @@ -64,7 +64,7 @@ impl Mint { description, } = mint_quote_request; - self.check_mint_request_acceptable(amount, unit)?; + self.check_mint_request_acceptable(amount, &unit)?; let ln = self .ln @@ -99,13 +99,16 @@ impl Mint { self.mint_url.clone(), create_invoice_response.request.to_string(), unit, - amount, + Some(amount), create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), + Amount::ZERO, + Amount::ZERO, + None, ); tracing::debug!( - "New mint quote {} for {} {} with request id {}", + "New bolt11 mint quote {} for {} {} with request id {}", quote.id, amount, unit, @@ -139,6 +142,8 @@ impl Mint { request: quote.request, state, expiry: Some(quote.expiry), + amount_paid: quote.amount_paid, + amount_issued: quote.amount_issued, }) } @@ -191,6 +196,7 @@ impl Mint { pub async fn pay_mint_quote_for_request_id( &self, request_lookup_id: &str, + amount: Amount, ) -> Result<(), Error> { if let Ok(Some(mint_quote)) = self .localstore @@ -201,38 +207,39 @@ impl Mint { "Received payment notification for mint quote {}", mint_quote.id ); + self.localstore + .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) + .await?; + let quote = self + .localstore + .get_mint_quote(&mint_quote.id) + .await? + .unwrap(); - if mint_quote.state != MintQuoteState::Issued - && mint_quote.state != MintQuoteState::Paid - { - let unix_time = unix_time(); - - if mint_quote.expiry < unix_time { - tracing::warn!( - "Mint quote {} paid at {} expired at {}, leaving current state", - mint_quote.id, - mint_quote.expiry, - unix_time, - ); - return Err(Error::ExpiredQuote(mint_quote.expiry, unix_time)); - } + let amount_paid = quote.amount_paid + amount; + + let quote = MintQuote { + id: quote.id, + mint_url: quote.mint_url, + amount: quote.amount, + unit: quote.unit, + request: quote.request, + state: MintQuoteState::Paid, + expiry: quote.expiry, + request_lookup_id: quote.request_lookup_id, + amount_paid, + amount_issued: quote.amount_issued, + single_use: None, + }; - tracing::debug!( - "Marking quote {} paid by lookup id {}", - mint_quote.id, - request_lookup_id - ); - - self.localstore - .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) - .await?; - } else { - tracing::debug!( - "{} Quote already {} continuing", - mint_quote.id, - mint_quote.state - ); - } + tracing::debug!( + "Quote: {}, Amount paid: {}, amount issued: {}", + quote.id, + amount_paid, + quote.amount_issued + ); + + self.localstore.add_mint_quote(quote).await?; } Ok(()) } @@ -256,6 +263,11 @@ impl Mint { .localstore .update_mint_quote_state(&mint_request.quote, MintQuoteState::Pending) .await?; + let quote = self + .localstore + .get_mint_quote(&mint_request.quote) + .await? + .unwrap(); match state { MintQuoteState::Unpaid => { @@ -265,11 +277,21 @@ impl Mint { return Err(Error::PendingQuote); } MintQuoteState::Issued => { - return Err(Error::IssuedQuote); + if quote.amount_issued >= quote.amount_paid { + return Err(Error::IssuedQuote); + } } MintQuoteState::Paid => (), } + let amount_can_issue = quote.amount_paid - quote.amount_issued; + + let messages_amount = mint_request.total_amount().unwrap(); + + if amount_can_issue < messages_amount { + return Err(Error::IssuedQuote); + } + let blinded_messages: Vec = mint_request .outputs .iter() @@ -321,6 +343,22 @@ impl Mint { .update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued) .await?; + let mint_quote = MintQuote { + id: quote.id, + mint_url: quote.mint_url, + amount: quote.amount, + unit: quote.unit, + request: quote.request, + state: MintQuoteState::Issued, + expiry: quote.expiry, + amount_paid: quote.amount_paid, + amount_issued: quote.amount_issued + messages_amount, + request_lookup_id: quote.request_lookup_id, + single_use: None, + }; + + self.localstore.add_mint_quote(mint_quote).await?; + Ok(nut04::MintBolt11Response { signatures: blind_signatures, }) diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 0699cf7f7..30a6b02cf 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -12,6 +12,7 @@ use tokio::sync::{Notify, RwLock}; use tokio::task::JoinSet; use tracing::instrument; +use self::types::PaymentRequest; use crate::cdk_database::{self, MintDatabase}; use crate::cdk_lightning::{self, MintLightning}; use crate::dhke::{sign_message, verify_message}; @@ -27,6 +28,7 @@ mod check_spendable; mod info; mod keysets; mod melt; +mod mint_18; mod mint_nut04; mod swap; pub mod types; @@ -208,7 +210,7 @@ impl Mint { match result { Ok(mut stream) => { while let Some(request_lookup_id) = stream.next().await { - if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id).await { + if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id.0, request_lookup_id.1).await { tracing::warn!("{:?}", err); } } @@ -363,13 +365,14 @@ impl Mint { pub async fn handle_internal_melt_mint( &self, melt_quote: &MeltQuote, - melt_request: &MeltBolt11Request, + inputs_amount: Amount, ) -> Result, Error> { - let mint_quote = match self - .localstore - .get_mint_quote_by_request(&melt_quote.request) - .await - { + let request = match &melt_quote.request { + PaymentRequest::Bolt11 { bolt11 } => bolt11.to_string(), + PaymentRequest::Bolt12 { offer, invoice: _ } => offer.to_string(), + }; + + let mint_quote = match self.localstore.get_mint_quote_by_request(&request).await { Ok(Some(mint_quote)) => mint_quote, // Not an internal melt -> mint Ok(None) => return Ok(None), @@ -384,18 +387,13 @@ impl Mint { return Err(Error::RequestAlreadyPaid); } - let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| { - tracing::error!("Proof inputs in melt quote overflowed"); - Error::AmountOverflow - })?; - let mut mint_quote = mint_quote; - if mint_quote.amount > inputs_amount_quote_unit { + if mint_quote.amount.unwrap_or_default() > inputs_amount { tracing::debug!( "Not enough inuts provided: {} needed {}", - inputs_amount_quote_unit, - mint_quote.amount + inputs_amount, + mint_quote.amount.unwrap_or_default() ); return Err(Error::InsufficientFunds); } @@ -489,7 +487,7 @@ impl Mint { } /// Mint Fee Reserve -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct FeeReserve { /// Absolute expected min fee pub min_fee_reserve: Amount, diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index 44047fd9b..a2f3413a1 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -1,5 +1,7 @@ //! Mint Types +use lightning::offers::offer::Offer; +use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -16,7 +18,7 @@ pub struct MintQuote { /// Mint Url pub mint_url: MintUrl, /// Amount of quote - pub amount: Amount, + pub amount: Option, /// Unit of quote pub unit: CurrencyUnit, /// Quote payment request e.g. bolt11 @@ -27,6 +29,12 @@ pub struct MintQuote { pub expiry: u64, /// Value used by ln backend to look up state of request pub request_lookup_id: String, + /// Amount paid + pub amount_paid: Amount, + /// Amount issued + pub amount_issued: Amount, + /// Single use + pub single_use: Option, } impl MintQuote { @@ -35,9 +43,12 @@ impl MintQuote { mint_url: MintUrl, request: String, unit: CurrencyUnit, - amount: Amount, + amount: Option, expiry: u64, request_lookup_id: String, + amount_paid: Amount, + amount_issued: Amount, + single_use: Option, ) -> Self { let id = Uuid::new_v4(); @@ -50,6 +61,9 @@ impl MintQuote { state: MintQuoteState::Unpaid, expiry, request_lookup_id, + amount_paid, + amount_issued, + single_use, } } } @@ -64,7 +78,7 @@ pub struct MeltQuote { /// Quote amount pub amount: Amount, /// Quote Payment request e.g. bolt11 - pub request: String, + pub request: PaymentRequest, /// Quote fee reserve pub fee_reserve: Amount, /// Quote state @@ -80,7 +94,7 @@ pub struct MeltQuote { impl MeltQuote { /// Create new [`MeltQuote`] pub fn new( - request: String, + request: PaymentRequest, unit: CurrencyUnit, amount: Amount, fee_reserve: Amount, @@ -102,3 +116,45 @@ impl MeltQuote { } } } + +/// Payment request +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum PaymentRequest { + /// Bolt11 Payment + Bolt11 { + /// Bolt11 invoice + bolt11: Bolt11Invoice, + }, + /// Bolt12 Payment + Bolt12 { + /// Offer + #[serde(with = "offer_serde")] + offer: Box, + /// Invoice + invoice: Option, + }, +} + +mod offer_serde { + use super::Offer; + use serde::{self, Deserialize, Deserializer, Serializer}; + use std::str::FromStr; + + pub fn serialize(offer: &Offer, serializer: S) -> Result + where + S: Serializer, + { + let s = offer.to_string(); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(Box::new(Offer::from_str(&s).map_err(|_| { + serde::de::Error::custom("Invalid Bolt12 Offer") + })?)) + } +} diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index 07518bff1..9efa6acbd 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -19,6 +19,8 @@ pub mod nut13; pub mod nut14; pub mod nut15; pub mod nut18; +pub mod nut19; +pub mod nut20; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, @@ -48,3 +50,4 @@ pub use nut12::{BlindSignatureDleq, ProofDleq}; pub use nut14::HTLCWitness; pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings}; pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport}; +pub use nut20::{MeltBolt12Request, MeltQuoteBolt12Request}; diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index d6c3dffdb..0ec8b6b48 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -391,7 +391,8 @@ impl CurrencyUnit { impl FromStr for CurrencyUnit { type Err = Error; fn from_str(value: &str) -> Result { - match value { + let value = value.to_lowercase(); + match value.as_str() { "sat" => Ok(Self::Sat), "msat" => Ok(Self::Msat), "usd" => Ok(Self::Usd), @@ -444,13 +445,17 @@ pub enum PaymentMethod { /// Bolt11 payment type #[default] Bolt11, + /// Bolt12 offer + Bolt12, } impl FromStr for PaymentMethod { type Err = Error; fn from_str(value: &str) -> Result { - match value { + let value = value.to_lowercase(); + match value.as_str() { "bolt11" => Ok(Self::Bolt11), + "bolt12" => Ok(Self::Bolt12), _ => Err(Error::UnsupportedPaymentMethod), } } @@ -460,6 +465,7 @@ impl fmt::Display for PaymentMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PaymentMethod::Bolt11 => write!(f, "bolt11"), + PaymentMethod::Bolt12 => write!(f, "bolt12"), } } } diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 207a7a9a3..32485d0f9 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -90,6 +90,10 @@ pub struct MintQuoteBolt11Response { pub state: MintQuoteState, /// Unix timestamp until the quote is valid pub expiry: Option, + /// Amount that has been paid + pub amount_paid: Amount, + /// Amount that has been issued + pub amount_issued: Amount, } #[cfg(feature = "mint")] @@ -100,6 +104,8 @@ impl From for MintQuoteBolt11Response { request: mint_quote.request, state: mint_quote.state, expiry: Some(mint_quote.expiry), + amount_paid: mint_quote.amount_paid, + amount_issued: mint_quote.amount_issued, } } } diff --git a/crates/cdk/src/nuts/nut05.rs b/crates/cdk/src/nuts/nut05.rs index e7ac3153a..2c6d292b7 100644 --- a/crates/cdk/src/nuts/nut05.rs +++ b/crates/cdk/src/nuts/nut05.rs @@ -226,12 +226,59 @@ pub struct MeltBolt11Request { pub outputs: Option>, } -impl MeltBolt11Request { +/// MeltRequest trait +pub trait MeltRequestTrait { + /// Error for MeltRequest trait + type Err: Into; + // async fn verify(&self, service: &MyService) -> Result; + /// Get id for [`MeltRequest`] + fn get_quote_id(&self) -> &str; + /// Get inputs for [`MeltRequest`] + fn get_inputs(&self) -> &Proofs; + /// Get outputs for [`MeltRequest`] + fn get_outputs(&self) -> &Option>; /// Total [`Amount`] of [`Proofs`] - pub fn proofs_amount(&self) -> Result { + fn inputs_amount(&self) -> Result; + /// Total [`Amount`] of outputs + fn outputs_amount(&self) -> Result; + /// [`PaymentMethod`] of request + fn get_payment_method(&self) -> PaymentMethod; +} + +impl MeltRequestTrait for MeltBolt11Request { + type Err = Error; + + fn get_quote_id(&self) -> &str { + &self.quote + } + + fn get_inputs(&self) -> &Proofs { + &self.inputs + } + + fn get_outputs(&self) -> &Option> { + &self.outputs + } + + fn inputs_amount(&self) -> Result { Amount::try_sum(self.inputs.iter().map(|proof| proof.amount)) .map_err(|_| Error::AmountOverflow) } + + fn outputs_amount(&self) -> Result { + Amount::try_sum( + self.outputs + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|proof| proof.amount), + ) + .map_err(|_| Error::AmountOverflow) + } + + fn get_payment_method(&self) -> PaymentMethod { + PaymentMethod::Bolt11 + } } /// Melt Method Settings diff --git a/crates/cdk/src/nuts/nut06.rs b/crates/cdk/src/nuts/nut06.rs index 358cc4ff4..f7869c17b 100644 --- a/crates/cdk/src/nuts/nut06.rs +++ b/crates/cdk/src/nuts/nut06.rs @@ -229,9 +229,17 @@ pub struct Nuts { #[serde(rename = "14")] pub nut14: SupportedSettings, /// NUT15 Settings - #[serde(default)] #[serde(rename = "15")] - pub nut15: nut15::Settings, + #[serde(skip_serializing_if = "Option::is_none")] + pub nut15: Option, + /// NUT04 Settings + #[serde(rename = "18")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nut18: Option, + /// NUT05 Settings + #[serde(rename = "19")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nut19: Option, } impl Nuts { @@ -315,21 +323,43 @@ impl Nuts { /// Nut15 settings pub fn nut15(self, mpp_settings: Vec) -> Self { Self { - nut15: nut15::Settings { + nut15: Some(nut15::Settings { methods: mpp_settings, - }, + }), + ..self + } + } + + /// Nut18 settings + pub fn nut18(self, nut04_settings: nut04::Settings) -> Self { + Self { + nut18: Some(nut04_settings), + ..self + } + } + + /// Nut19 settings + pub fn nut19(self, nut05_settings: nut05::Settings) -> Self { + Self { + nut19: Some(nut05_settings), ..self } } } /// Check state Settings -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct SupportedSettings { supported: bool, } +impl Default for SupportedSettings { + fn default() -> Self { + Self { supported: true } + } +} + /// Contact Info #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs new file mode 100644 index 000000000..8fdafb32d --- /dev/null +++ b/crates/cdk/src/nuts/nut19.rs @@ -0,0 +1,66 @@ +//! NUT-17: Mint Tokens via Bolt11 +//! +//! + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::nut00::CurrencyUnit; +use crate::Amount; + +/// NUT04 Error +#[derive(Debug, Error)] +pub enum Error { + /// Unknown Quote State + #[error("Unknown Quote State")] + UnknownState, + /// Amount overflow + #[error("Amount overflow")] + AmountOverflow, +} + +/// Mint quote request [NUT-17] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintQuoteBolt12Request { + /// Amount + pub amount: Option, + /// Unit wallet would like to pay with + pub unit: CurrencyUnit, + /// Memo to create the invoice with + pub description: Option, + /// Single use + pub single_use: Option, + /// Expiry + pub expiry: Option, +} + +/// Mint quote response [NUT-04] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct MintQuoteBolt12Response { + /// Quote Id + pub quote: String, + /// Payment request to fulfil + pub request: String, + /// Single use + pub single_use: Option, + /// Unix timestamp until the quote is valid + pub expiry: Option, + /// Amount that has been paid + pub amount_paid: Amount, + /// Amount that has been issued + pub amount_issued: Amount, +} + +#[cfg(feature = "mint")] +impl From for MintQuoteBolt12Response { + fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt12Response { + MintQuoteBolt12Response { + quote: mint_quote.id, + request: mint_quote.request, + expiry: Some(mint_quote.expiry), + amount_paid: mint_quote.amount_paid, + amount_issued: mint_quote.amount_issued, + single_use: mint_quote.single_use, + } + } +} diff --git a/crates/cdk/src/nuts/nut20.rs b/crates/cdk/src/nuts/nut20.rs new file mode 100644 index 000000000..dbb498084 --- /dev/null +++ b/crates/cdk/src/nuts/nut20.rs @@ -0,0 +1,77 @@ +//! Bolt12 +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::Amount; + +use super::{nut05::MeltRequestTrait, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs}; + +/// NUT18 Error +#[derive(Debug, Error)] +pub enum Error { + /// Unknown Quote State + #[error("Unknown quote state")] + UnknownState, + /// Amount overflow + #[error("Amount Overflow")] + AmountOverflow, +} + +/// Melt quote request [NUT-18] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltQuoteBolt12Request { + /// Bolt12 invoice to be paid + pub request: String, + /// Unit wallet would like to pay with + pub unit: CurrencyUnit, + /// Payment Options + pub amount: Option, +} + +/// Melt Bolt12 Request [NUT-18] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltBolt12Request { + /// Quote ID + pub quote: String, + /// Proofs + pub inputs: Proofs, + /// Blinded Message that can be used to return change [NUT-08] + /// Amount field of BlindedMessages `SHOULD` be set to zero + pub outputs: Option>, +} + +impl MeltRequestTrait for MeltBolt12Request { + type Err = Error; + + fn get_quote_id(&self) -> &str { + &self.quote + } + + fn get_inputs(&self) -> &Proofs { + &self.inputs + } + + fn get_outputs(&self) -> &Option> { + &self.outputs + } + + fn inputs_amount(&self) -> Result { + Amount::try_sum(self.inputs.iter().map(|proof| proof.amount)) + .map_err(|_| Error::AmountOverflow) + } + + fn outputs_amount(&self) -> Result { + Amount::try_sum( + self.outputs + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|proof| proof.amount), + ) + .map_err(|_| Error::AmountOverflow) + } + + fn get_payment_method(&self) -> PaymentMethod { + PaymentMethod::Bolt12 + } +} diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index d5b413ce4..cdd9726c5 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -11,10 +11,10 @@ use crate::mint_url::MintUrl; use crate::nuts::nut15::Mpp; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, - KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, - MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request, - MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey, RestoreRequest, RestoreResponse, - SwapRequest, SwapResponse, + KeysetResponse, MeltBolt11Request, MeltBolt12Request, MeltQuoteBolt11Request, + MeltQuoteBolt11Response, MeltQuoteBolt12Request, MintBolt11Request, MintBolt11Response, + MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey, + RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, }; use crate::{Amount, Bolt11Invoice}; @@ -139,6 +139,37 @@ impl HttpClient { } } + /// Mint Quote [NUT-04] + #[instrument(skip(self), fields(mint_url = %mint_url))] + pub async fn post_mint_bolt12_quote( + &self, + mint_url: MintUrl, + amount: Amount, + unit: CurrencyUnit, + description: Option, + ) -> Result { + let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt12"])?; + + let request = MintQuoteBolt11Request { + amount, + unit, + description, + }; + + let res = self.inner.post(url).json(&request).send().await?; + println!("{:?}", res); + + let res = res.json::().await?; + + match serde_json::from_value::(res.clone()) { + Ok(mint_quote_response) => Ok(mint_quote_response), + Err(err) => { + tracing::warn!("{}", err); + Err(ErrorResponse::from_value(res)?.into()) + } + } + } + /// Mint Quote status #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_mint_quote_status( @@ -223,6 +254,33 @@ impl HttpClient { } } + /// Melt Bol12 + #[instrument(skip(self, request), fields(mint_url = %mint_url))] + pub async fn post_melt_bolt12_quote( + &self, + mint_url: MintUrl, + unit: CurrencyUnit, + request: String, + amount: Option, + ) -> Result { + let url = mint_url.join_paths(&["v1", "melt", "quote", "bolt12"])?; + + let request = MeltQuoteBolt12Request { + request, + unit, + amount, + }; + + let res = self.inner.post(url).json(&request).send().await?; + + let res = res.json::().await?; + + match serde_json::from_value::(res.clone()) { + Ok(melt_quote_response) => Ok(melt_quote_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } + /// Melt Quote Status #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn get_melt_quote_status( @@ -278,6 +336,39 @@ impl HttpClient { } } + /// Melt Bolt12 [NUT-05] + /// [Nut-08] Lightning fee return if outputs defined + #[instrument(skip(self, quote, inputs, outputs), fields(mint_url = %mint_url))] + pub async fn post_melt_bolt12( + &self, + mint_url: MintUrl, + quote: String, + inputs: Vec, + outputs: Option>, + ) -> Result { + let url = mint_url.join_paths(&["v1", "melt", "bolt12"])?; + + let request = MeltBolt12Request { + quote, + inputs, + outputs, + }; + + let res = self + .inner + .post(url) + .json(&request) + .send() + .await? + .json::() + .await?; + + match serde_json::from_value::(res.clone()) { + Ok(melt_quote_response) => Ok(melt_quote_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } + /// Split Token [NUT-06] #[instrument(skip(self, swap_request), fields(mint_url = %mint_url))] pub async fn post_swap( diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index c31f2ff49..d3ca05d30 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -1,12 +1,14 @@ use std::str::FromStr; +use lightning::offers::offer::Offer; use lightning_invoice::Bolt11Invoice; use tracing::instrument; use crate::nuts::nut00::ProofsMethods; use crate::{ + amount::amount_for_offer, dhke::construct_proofs, - nuts::{CurrencyUnit, MeltQuoteBolt11Response, PreMintSecrets, Proofs, State}, + nuts::{CurrencyUnit, MeltQuoteBolt11Response, PaymentMethod, PreMintSecrets, Proofs, State}, types::{Melted, ProofInfo}, util::unix_time, Amount, Error, Wallet, @@ -70,6 +72,53 @@ impl Wallet { id: quote_res.quote, amount, request, + payment_method: PaymentMethod::Bolt11, + unit: self.unit, + fee_reserve: quote_res.fee_reserve, + state: quote_res.state, + expiry: quote_res.expiry, + payment_preimage: quote_res.payment_preimage, + }; + + self.localstore.add_melt_quote(quote.clone()).await?; + + Ok(quote) + } + + /// Melt Quote bolt12 + #[instrument(skip(self, request))] + pub async fn melt_bolt12_quote( + &self, + request: String, + amount: Option, + ) -> Result { + let offer = Offer::from_str(&request)?; + + let amount = match amount { + Some(amount) => amount, + None => amount_for_offer(&offer, &self.unit).unwrap(), + }; + + let quote_res = self + .client + .post_melt_bolt12_quote( + self.mint_url.clone(), + self.unit, + request.to_string(), + Some(amount), + ) + .await + .unwrap(); + + if quote_res.amount != amount { + return Err(Error::IncorrectQuoteAmount); + } + + let quote = MeltQuote { + id: quote_res.quote, + amount, + request, + payment_method: PaymentMethod::Bolt12, unit: self.unit, fee_reserve: quote_res.fee_reserve, state: quote_res.state, @@ -146,15 +195,28 @@ impl Wallet { proofs_total - quote_info.amount, )?; - let melt_response = self - .client - .post_melt( - self.mint_url.clone(), - quote_id.to_string(), - proofs.clone(), - Some(premint_secrets.blinded_messages()), - ) - .await; + let melt_response = match quote_info.payment_method { + PaymentMethod::Bolt11 => { + self.client + .post_melt( + self.mint_url.clone(), + quote_id.to_string(), + proofs.clone(), + Some(premint_secrets.blinded_messages()), + ) + .await + } + PaymentMethod::Bolt12 => { + self.client + .post_melt_bolt12( + self.mint_url.clone(), + quote_id.to_string(), + proofs.clone(), + Some(premint_secrets.blinded_messages()), + ) + .await + } + }; let melt_response = match melt_response { Ok(melt_response) => melt_response, diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index b429daaf8..8c48aa901 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -84,6 +84,54 @@ impl Wallet { Ok(quote) } + /// Mint Bolt12 + #[instrument(skip(self))] + pub async fn mint_bolt12_quote( + &self, + amount: Amount, + description: Option, + ) -> Result { + let mint_url = self.mint_url.clone(); + let unit = self.unit; + + // If we have a description, we check that the mint supports it. + // If we have a description, we check that the mint supports it. + if description.is_some() { + let mint_method_settings = self + .localstore + .get_mint(mint_url.clone()) + .await? + .ok_or(Error::IncorrectMint)? + .nuts + .nut04 + .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11) + .ok_or(Error::UnsupportedUnit)?; + + if !mint_method_settings.description { + return Err(Error::InvoiceDescriptionUnsupported); + } + } + + let quote_res = self + .client + .post_mint_bolt12_quote(mint_url.clone(), amount, unit, description) + .await?; + + let quote = MintQuote { + mint_url, + id: quote_res.quote.clone(), + amount, + unit, + request: quote_res.request, + state: quote_res.state, + expiry: quote_res.expiry.unwrap_or(0), + }; + + self.localstore.add_mint_quote(quote.clone()).await?; + + Ok(quote) + } + /// Check mint quote status #[instrument(skip(self, quote_id))] pub async fn mint_quote_state(&self, quote_id: &str) -> Result { @@ -196,10 +244,25 @@ impl Wallet { let count = count.map_or(0, |c| c + 1); + let status = self.mint_quote_state(quote_id).await?; + + println!( + "Amount paid: {}, Amount issued: {}", + status.amount_paid, status.amount_issued + ); + + let amount = status.amount_paid - status.amount_issued; + + let amount = if amount == Amount::ZERO { + quote_info.amount + } else { + amount + }; + let premint_secrets = match &spending_conditions { Some(spending_conditions) => PreMintSecrets::with_conditions( active_keyset_id, - quote_info.amount, + amount, &amount_split_target, spending_conditions, )?, @@ -207,7 +270,7 @@ impl Wallet { active_keyset_id, count, self.xpriv, - quote_info.amount, + amount, &amount_split_target, )?, }; @@ -241,7 +304,7 @@ impl Wallet { let minted_amount = proofs.total_amount()?; // Remove filled quote from store - self.localstore.remove_mint_quote("e_info.id).await?; + //self.localstore.remove_mint_quote("e_info.id).await?; if spending_conditions.is_none() { // Update counter for keyset diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index 8d26e9e46..16e3936e6 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -16,7 +16,7 @@ use super::types::SendKind; use super::Error; use crate::amount::SplitTarget; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, Proof, SecretKey, SpendingConditions, Token}; +use crate::nuts::{CurrencyUnit, PaymentMethod, Proof, SecretKey, SpendingConditions, Token}; use crate::types::Melted; use crate::wallet::types::MintQuote; use crate::{Amount, Wallet}; @@ -166,6 +166,7 @@ impl MultiMintWallet { wallet_key: &WalletKey, amount: Amount, description: Option, + payment_method: PaymentMethod, ) -> Result { let wallet = self .get_wallet(wallet_key) diff --git a/crates/cdk/src/wallet/types.rs b/crates/cdk/src/wallet/types.rs index 309a4c1cf..a5df013ee 100644 --- a/crates/cdk/src/wallet/types.rs +++ b/crates/cdk/src/wallet/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState}; +use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PaymentMethod}; use crate::Amount; /// Mint Quote Info @@ -36,6 +36,8 @@ pub struct MeltQuote { pub amount: Amount, /// Quote Payment request e.g. bolt11 pub request: String, + /// Payment Method + pub payment_method: PaymentMethod, /// Quote fee reserve pub fee_reserve: Amount, /// Quote state From 8558e94dbe7c472dd0a54d7f8160a681c93e6999 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 25 Oct 2024 19:12:16 +0100 Subject: [PATCH 02/35] feat: bolt12 mints --- .helix/languages.toml | 2 +- crates/cdk-axum/src/bolt12_router.rs | 63 +++++ crates/cdk-axum/src/lib.rs | 4 + crates/cdk-axum/src/router_handlers.rs | 60 +---- ...75105_wallet_mint_quote_payment_method.sql | 3 + crates/cdk-sqlite/src/wallet/mod.rs | 18 +- crates/cdk/src/mint/mint_nut04.rs | 2 - crates/cdk/src/nuts/nut04.rs | 6 - crates/cdk/src/nuts/nut19.rs | 8 +- crates/cdk/src/wallet/client.rs | 53 ++++- crates/cdk/src/wallet/mint.rs | 67 +----- crates/cdk/src/wallet/mint_bolt12.rs | 216 ++++++++++++++++++ crates/cdk/src/wallet/mod.rs | 1 + crates/cdk/src/wallet/types.rs | 7 + 14 files changed, 375 insertions(+), 135 deletions(-) create mode 100644 crates/cdk-axum/src/bolt12_router.rs create mode 100644 crates/cdk-sqlite/src/wallet/migrations/20241025175105_wallet_mint_quote_payment_method.sql create mode 100644 crates/cdk/src/wallet/mint_bolt12.rs diff --git a/.helix/languages.toml b/.helix/languages.toml index 222103597..f86f944a1 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,2 +1,2 @@ [language-server.rust-analyzer.config] -cargo = { features = ["wallet", "mint"] } +cargo = { features = ["wallet", "mint", "swagger"] } diff --git a/crates/cdk-axum/src/bolt12_router.rs b/crates/cdk-axum/src/bolt12_router.rs new file mode 100644 index 000000000..db2c83586 --- /dev/null +++ b/crates/cdk-axum/src/bolt12_router.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use axum::extract::{Json, State}; +use axum::response::Response; +use cdk::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; +use cdk::nuts::{ + MeltBolt12Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request, MintBolt11Request, + MintBolt11Response, +}; + +use crate::{into_response, MintState}; + +/// Get mint bolt12 quote +pub async fn get_mint_bolt12_quote( + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let quote = state + .mint + .get_mint_bolt12_quote(payload) + .await + .map_err(into_response)?; + + Ok(Json(quote)) +} + +/// Request a quote for melting tokens +pub async fn post_mint_bolt12( + State(state): State, + Json(payload): Json, +) -> Result, 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_bolt12_quote( + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let quote = state + .mint + .get_melt_bolt12_quote(&payload) + .await + .map_err(into_response)?; + + Ok(Json(quote)) +} + +pub async fn post_melt_bolt12( + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let res = state.mint.melt(&payload).await.map_err(into_response)?; + + Ok(Json(res)) +} diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index e16752184..9244b0a34 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -9,10 +9,14 @@ use std::time::Duration; use anyhow::Result; use axum::routing::{get, post}; use axum::Router; +use bolt12_router::{ + get_melt_bolt12_quote, get_mint_bolt12_quote, post_melt_bolt12, post_mint_bolt12, +}; use cdk::mint::Mint; use moka::future::Cache; use router_handlers::*; +mod bolt12_router; mod router_handlers; #[cfg(feature = "swagger")] diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 546f37838..6120488af 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -3,12 +3,11 @@ use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use cdk::error::ErrorResponse; -use cdk::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use cdk::nuts::{ CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request, - MeltBolt12Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request, - MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request, - MintQuoteBolt11Response, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, + MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response, + MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse, + SwapRequest, SwapResponse, }; use cdk::util::unix_time; use cdk::Error; @@ -144,20 +143,6 @@ pub async fn get_mint_bolt11_quote( Ok(Json(quote)) } -/// Get mint bolt12 quote -pub async fn get_mint_bolt12_quote( - State(state): State, - Json(payload): Json, -) -> Result, Response> { - let quote = state - .mint - .get_mint_bolt12_quote(payload) - .await - .map_err(into_response)?; - - Ok(Json(quote)) -} - #[cfg_attr(feature = "swagger", utoipa::path( get, context_path = "/v1", @@ -218,23 +203,6 @@ pub async fn post_mint_bolt11( Ok(Json(res)) } -/// Request a quote for melting tokens -pub async fn post_mint_bolt12( - State(state): State, - Json(payload): Json, -) -> Result, 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)) -} - #[cfg_attr(feature = "swagger", utoipa::path( post, context_path = "/v1", @@ -311,28 +279,6 @@ pub async fn post_melt_bolt11( Ok(Json(res)) } -pub async fn get_melt_bolt12_quote( - State(state): State, - Json(payload): Json, -) -> Result, Response> { - let quote = state - .mint - .get_melt_bolt12_quote(&payload) - .await - .map_err(into_response)?; - - Ok(Json(quote)) -} - -pub async fn post_melt_bolt12( - State(state): State, - Json(payload): Json, -) -> Result, Response> { - let res = state.mint.melt(&payload).await.map_err(into_response)?; - - Ok(Json(res)) -} - #[cfg_attr(feature = "swagger", utoipa::path( post, context_path = "/v1", diff --git a/crates/cdk-sqlite/src/wallet/migrations/20241025175105_wallet_mint_quote_payment_method.sql b/crates/cdk-sqlite/src/wallet/migrations/20241025175105_wallet_mint_quote_payment_method.sql new file mode 100644 index 000000000..0cd6fe5ac --- /dev/null +++ b/crates/cdk-sqlite/src/wallet/migrations/20241025175105_wallet_mint_quote_payment_method.sql @@ -0,0 +1,3 @@ +ALTER TABLE mint_quote ADD payment_method TEXT; +ALTER TABLE mint_quote ADD amount_paid INTEGER; +ALTER TABLE mint_quote ADD amount_minted INTEGER; diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index bb740eca3..a6ef4cc77 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -336,8 +336,8 @@ WHERE id=? sqlx::query( r#" INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry) -VALUES (?, ?, ?, ?, ?, ?, ?); +(id, mint_url, amount, unit, request, state, expiry, payment_method, amount_paid, amount_minted) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -347,6 +347,9 @@ VALUES (?, ?, ?, ?, ?, ?, ?); .bind(quote.request) .bind(quote.state.to_string()) .bind(quote.expiry as i64) + .bind(quote.payment_method.to_string()) + .bind(u64::from(quote.amount_paid) as i64) + .bind(u64::from(quote.amount_minted) as i64) .execute(&self.pool) .await .map_err(Error::from)?; @@ -816,9 +819,17 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { 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_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; + let row_method: Option = row.try_get("payment_method").map_err(Error::from)?; + let row_amount_paid: Option = row.try_get("amount_paid").map_err(Error::from)?; + let row_amount_minted: Option = row.try_get("amount_minted").map_err(Error::from)?; let state = MintQuoteState::from_str(&row_state)?; + let payment_method = row_method.and_then(|m| PaymentMethod::from_str(&m).ok()); + + let amount_paid = row_amount_paid.unwrap_or(0) as u64; + let amount_minted = row_amount_minted.unwrap_or(0) as u64; + Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, @@ -827,6 +838,9 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { request: row_request, state, expiry: row_expiry as u64, + payment_method: payment_method.unwrap_or_default(), + amount_minted: amount_minted.into(), + amount_paid: amount_paid.into(), }) } diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index b2d71aa15..855dd72b5 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -142,8 +142,6 @@ impl Mint { request: quote.request, state, expiry: Some(quote.expiry), - amount_paid: quote.amount_paid, - amount_issued: quote.amount_issued, }) } diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 32485d0f9..207a7a9a3 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -90,10 +90,6 @@ pub struct MintQuoteBolt11Response { pub state: MintQuoteState, /// Unix timestamp until the quote is valid pub expiry: Option, - /// Amount that has been paid - pub amount_paid: Amount, - /// Amount that has been issued - pub amount_issued: Amount, } #[cfg(feature = "mint")] @@ -104,8 +100,6 @@ impl From for MintQuoteBolt11Response { request: mint_quote.request, state: mint_quote.state, expiry: Some(mint_quote.expiry), - amount_paid: mint_quote.amount_paid, - amount_issued: mint_quote.amount_issued, } } } diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs index 8fdafb32d..992fb0572 100644 --- a/crates/cdk/src/nuts/nut19.rs +++ b/crates/cdk/src/nuts/nut19.rs @@ -19,8 +19,8 @@ pub enum Error { AmountOverflow, } -/// Mint quote request [NUT-17] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// Mint quote request [NUT-19] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct MintQuoteBolt12Request { /// Amount pub amount: Option, @@ -34,8 +34,8 @@ pub struct MintQuoteBolt12Request { pub expiry: Option, } -/// Mint quote response [NUT-04] -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +/// Mint quote response [NUT-19] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct MintQuoteBolt12Response { /// Quote Id pub quote: String, diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index cdd9726c5..16b5bd110 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -9,6 +9,7 @@ use super::Error; use crate::error::ErrorResponse; use crate::mint_url::MintUrl; use crate::nuts::nut15::Mpp; +use crate::nuts::nut19::MintQuoteBolt12Response; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, MeltBolt11Request, MeltBolt12Request, MeltQuoteBolt11Request, @@ -139,7 +140,7 @@ impl HttpClient { } } - /// Mint Quote [NUT-04] + /// Mint Quote [NUT-19] #[instrument(skip(self), fields(mint_url = %mint_url))] pub async fn post_mint_bolt12_quote( &self, @@ -220,6 +221,56 @@ impl HttpClient { } } + /// Mint Quote status + #[instrument(skip(self), fields(mint_url = %mint_url))] + pub async fn get_mint_bolt12_quote_status( + &self, + mint_url: MintUrl, + quote_id: &str, + ) -> Result { + let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt12", quote_id])?; + + let res = self.inner.get(url).send().await?.json::().await?; + + match serde_json::from_value::(res.clone()) { + Ok(mint_quote_response) => Ok(mint_quote_response), + Err(err) => { + tracing::warn!("{}", err); + Err(ErrorResponse::from_value(res)?.into()) + } + } + } + + /// Mint Tokens [NUT-19] + #[instrument(skip(self, quote, premint_secrets), fields(mint_url = %mint_url))] + pub async fn post_mint_bolt12( + &self, + mint_url: MintUrl, + quote: &str, + premint_secrets: PreMintSecrets, + ) -> Result { + let url = mint_url.join_paths(&["v1", "mint", "bolt12"])?; + + let request = MintBolt11Request { + quote: quote.to_string(), + outputs: premint_secrets.blinded_messages(), + }; + + let res = self + .inner + .post(url) + .json(&request) + .send() + .await? + .json::() + .await?; + + match serde_json::from_value::(res.clone()) { + Ok(mint_quote_response) => Ok(mint_quote_response), + Err(_) => Err(ErrorResponse::from_value(res)?.into()), + } + } + /// Melt Quote [NUT-05] #[instrument(skip(self, request), fields(mint_url = %mint_url))] pub async fn post_melt_quote( diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 8c48aa901..eb5051022 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -2,6 +2,7 @@ use tracing::instrument; use super::MintQuote; use crate::nuts::nut00::ProofsMethods; +use crate::nuts::PaymentMethod; use crate::{ amount::SplitTarget, dhke::construct_proofs, @@ -72,59 +73,14 @@ impl Wallet { let quote = MintQuote { mint_url, id: quote_res.quote.clone(), + payment_method: PaymentMethod::Bolt11, amount, unit, request: quote_res.request, state: quote_res.state, expiry: quote_res.expiry.unwrap_or(0), - }; - - self.localstore.add_mint_quote(quote.clone()).await?; - - Ok(quote) - } - - /// Mint Bolt12 - #[instrument(skip(self))] - pub async fn mint_bolt12_quote( - &self, - amount: Amount, - description: Option, - ) -> Result { - let mint_url = self.mint_url.clone(); - let unit = self.unit; - - // If we have a description, we check that the mint supports it. - // If we have a description, we check that the mint supports it. - if description.is_some() { - let mint_method_settings = self - .localstore - .get_mint(mint_url.clone()) - .await? - .ok_or(Error::IncorrectMint)? - .nuts - .nut04 - .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11) - .ok_or(Error::UnsupportedUnit)?; - - if !mint_method_settings.description { - return Err(Error::InvoiceDescriptionUnsupported); - } - } - - let quote_res = self - .client - .post_mint_bolt12_quote(mint_url.clone(), amount, unit, description) - .await?; - - let quote = MintQuote { - mint_url, - id: quote_res.quote.clone(), - amount, - unit, - request: quote_res.request, - state: quote_res.state, - expiry: quote_res.expiry.unwrap_or(0), + amount_minted: Amount::ZERO, + amount_paid: Amount::ZERO, }; self.localstore.add_mint_quote(quote.clone()).await?; @@ -244,20 +200,7 @@ impl Wallet { let count = count.map_or(0, |c| c + 1); - let status = self.mint_quote_state(quote_id).await?; - - println!( - "Amount paid: {}, Amount issued: {}", - status.amount_paid, status.amount_issued - ); - - let amount = status.amount_paid - status.amount_issued; - - let amount = if amount == Amount::ZERO { - quote_info.amount - } else { - amount - }; + let amount = quote_info.amount; let premint_secrets = match &spending_conditions { Some(spending_conditions) => PreMintSecrets::with_conditions( diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs new file mode 100644 index 000000000..bfc87ec0c --- /dev/null +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -0,0 +1,216 @@ +use tracing::instrument; + +use super::MintQuote; +use crate::nuts::nut00::ProofsMethods; +use crate::nuts::nut19::MintQuoteBolt12Response; +use crate::nuts::PaymentMethod; +use crate::{ + amount::SplitTarget, + dhke::construct_proofs, + nuts::{nut12, PreMintSecrets, SpendingConditions, State}, + types::ProofInfo, + util::unix_time, + Amount, Error, Wallet, +}; + +impl Wallet { + /// Mint Bolt12 + #[instrument(skip(self))] + pub async fn mint_bolt12_quote( + &self, + amount: Amount, + description: Option, + ) -> Result { + let mint_url = self.mint_url.clone(); + let unit = self.unit; + + // If we have a description, we check that the mint supports it. + if description.is_some() { + let mint_method_settings = self + .localstore + .get_mint(mint_url.clone()) + .await? + .ok_or(Error::IncorrectMint)? + .nuts + .nut04 + .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11) + .ok_or(Error::UnsupportedUnit)?; + + if !mint_method_settings.description { + return Err(Error::InvoiceDescriptionUnsupported); + } + } + + let quote_res = self + .client + .post_mint_bolt12_quote(mint_url.clone(), amount, unit, description) + .await?; + + let quote = MintQuote { + mint_url, + id: quote_res.quote.clone(), + payment_method: PaymentMethod::Bolt12, + amount, + unit, + request: quote_res.request, + state: quote_res.state, + expiry: quote_res.expiry.unwrap_or(0), + amount_minted: Amount::ZERO, + amount_paid: Amount::ZERO, + }; + + self.localstore.add_mint_quote(quote.clone()).await?; + + Ok(quote) + } + + /// Mint bolt12 + #[instrument(skip(self))] + pub async fn mint_bolt12( + &self, + quote_id: &str, + amount: Option, + amount_split_target: SplitTarget, + spending_conditions: Option, + ) -> Result { + // Check that mint is in store of mints + if self + .localstore + .get_mint(self.mint_url.clone()) + .await? + .is_none() + { + self.get_mint_info().await?; + } + + let quote_info = self.localstore.get_mint_quote(quote_id).await?; + + let quote_info = if let Some(quote) = quote_info { + if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) { + return Err(Error::ExpiredQuote(quote.expiry, unix_time())); + } + + quote.clone() + } else { + return Err(Error::UnknownQuote); + }; + + let active_keyset_id = self.get_active_mint_keyset().await?.id; + + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; + + let count = count.map_or(0, |c| c + 1); + + let amount = match amount { + Some(amount) => amount, + None => { + // If an amount it not supplied with check the status of the quote + // The mint will tell us how much can be minted + let state = self.mint_bolt12_quote_state(quote_id).await?; + + state.amount_paid - state.amount_issued + } + }; + + let premint_secrets = match &spending_conditions { + Some(spending_conditions) => PreMintSecrets::with_conditions( + active_keyset_id, + amount, + &amount_split_target, + spending_conditions, + )?, + None => PreMintSecrets::from_xpriv( + active_keyset_id, + count, + self.xpriv, + amount, + &amount_split_target, + )?, + }; + + let mint_res = self + .client + .post_mint(self.mint_url.clone(), quote_id, premint_secrets.clone()) + .await?; + + let keys = self.get_keyset_keys(active_keyset_id).await?; + + // Verify the signature DLEQ is valid + { + for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { + let keys = self.get_keyset_keys(sig.keyset_id).await?; + let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; + match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { + Ok(_) | Err(nut12::Error::MissingDleqProof) => (), + Err(_) => return Err(Error::CouldNotVerifyDleq), + } + } + } + + let proofs = construct_proofs( + mint_res.signatures, + premint_secrets.rs(), + premint_secrets.secrets(), + &keys, + )?; + + let minted_amount = proofs.total_amount()?; + + // Remove filled quote from store + //self.localstore.remove_mint_quote("e_info.id).await?; + + if spending_conditions.is_none() { + // Update counter for keyset + self.localstore + .increment_keyset_counter(&active_keyset_id, proofs.len() as u32) + .await?; + } + + let proofs = proofs + .into_iter() + .map(|proof| { + ProofInfo::new( + proof, + self.mint_url.clone(), + State::Unspent, + quote_info.unit, + ) + }) + .collect::, _>>()?; + + // Add new proofs to store + self.localstore.update_proofs(proofs, vec![]).await?; + + Ok(minted_amount) + } + + /// Check mint quote status + #[instrument(skip(self, quote_id))] + pub async fn mint_bolt12_quote_state( + &self, + quote_id: &str, + ) -> Result { + let response = self + .client + .get_mint_bolt12_quote_status(self.mint_url.clone(), quote_id) + .await?; + + match self.localstore.get_mint_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + quote.amount_minted = response.amount_issued; + quote.amount_paid = response.amount_paid; + + self.localstore.add_mint_quote(quote).await?; + } + None => { + tracing::info!("Quote mint {} unknown", quote_id); + } + } + + Ok(response) + } +} diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 85045eaa8..6e704c89f 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -27,6 +27,7 @@ pub mod client; mod keysets; mod melt; mod mint; +mod mint_bolt12; pub mod multi_mint_wallet; mod proofs; mod receive; diff --git a/crates/cdk/src/wallet/types.rs b/crates/cdk/src/wallet/types.rs index a5df013ee..536dfc6ec 100644 --- a/crates/cdk/src/wallet/types.rs +++ b/crates/cdk/src/wallet/types.rs @@ -13,6 +13,9 @@ pub struct MintQuote { pub id: String, /// Mint Url pub mint_url: MintUrl, + /// Payment method + #[serde(default)] + pub payment_method: PaymentMethod, /// Amount of quote pub amount: Amount, /// Unit of quote @@ -23,6 +26,10 @@ pub struct MintQuote { pub state: MintQuoteState, /// Expiration time of quote pub expiry: u64, + /// Amount minted + pub amount_minted: Amount, + /// Amount paid to the mint for the quote + pub amount_paid: Amount, } /// Melt Quote Info From c9cfb030e752a113c554bc4a24f9591536ec2a16 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 26 Oct 2024 08:49:09 +0100 Subject: [PATCH 03/35] chore: clippy --- crates/cdk/src/mint/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index a2f3413a1..d764e2ac3 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -39,6 +39,7 @@ pub struct MintQuote { impl MintQuote { /// Create new [`MintQuote`] + #[allow(clippy::too_many_arguments)] pub fn new( mint_url: MintUrl, request: String, From cf0b4ce3acdc8314f496c320bee096e626851493 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 26 Oct 2024 10:51:17 +0100 Subject: [PATCH 04/35] feat: check mint bolt12 --- crates/cdk-axum/src/bolt12_router.rs | 16 +++++++++++++++- crates/cdk-axum/src/lib.rs | 5 +++-- crates/cdk/src/mint/mint_18.rs | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/cdk-axum/src/bolt12_router.rs b/crates/cdk-axum/src/bolt12_router.rs index db2c83586..cf9d7cb26 100644 --- a/crates/cdk-axum/src/bolt12_router.rs +++ b/crates/cdk-axum/src/bolt12_router.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use axum::extract::{Json, State}; +use axum::extract::{Json, Path, State}; use axum::response::Response; use cdk::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use cdk::nuts::{ @@ -23,6 +23,20 @@ pub async fn get_mint_bolt12_quote( Ok(Json(quote)) } +/// Get mint bolt12 quote +pub async fn get_check_mint_bolt12_quote( + State(state): State, + Path(quote_id): Path, +) -> Result, Response> { + let quote = state + .mint + .check_mint_bolt12_quote("e_id) + .await + .map_err(into_response)?; + + Ok(Json(quote)) +} + /// Request a quote for melting tokens pub async fn post_mint_bolt12( State(state): State, diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index 9244b0a34..f0e22959e 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -10,7 +10,8 @@ use anyhow::Result; use axum::routing::{get, post}; use axum::Router; use bolt12_router::{ - get_melt_bolt12_quote, get_mint_bolt12_quote, post_melt_bolt12, post_mint_bolt12, + get_check_mint_bolt12_quote, get_melt_bolt12_quote, get_mint_bolt12_quote, post_melt_bolt12, + post_mint_bolt12, }; use cdk::mint::Mint; use moka::future::Cache; @@ -196,7 +197,7 @@ fn create_bolt12_router(state: MintState) -> Router { .route("/mint/quote/bolt12", post(get_mint_bolt12_quote)) .route( "/mint/quote/bolt12/:quote_id", - get(get_check_mint_bolt11_quote), + get(get_check_mint_bolt12_quote), ) .route("/mint/bolt12", post(post_mint_bolt12)) .with_state(state) diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index ddda8d8ae..cdc9c8183 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -92,4 +92,19 @@ impl Mint { Ok(quote.into()) } + + /// Check mint quote + #[instrument(skip(self))] + pub async fn check_mint_bolt12_quote( + &self, + quote_id: &str, + ) -> Result { + let quote = self + .localstore + .get_mint_quote(quote_id) + .await? + .ok_or(Error::UnknownQuote)?; + + Ok(quote.into()) + } } From 9ded476e6f04f8b5a05d899fdf40912f59d1fa6c Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 26 Oct 2024 11:24:56 +0100 Subject: [PATCH 05/35] fix: mint nut04 --- crates/cdk-integration-tests/tests/regtest.rs | 2 +- crates/cdk/src/mint/mint_nut04.rs | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index c10d5ffef..e66d38acd 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -186,7 +186,7 @@ async fn test_pay_invoice_twice() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_internal_payment() -> Result<()> { +async fn test_regtest_internal_payment() -> Result<()> { let lnd_client = init_lnd_client().await?; let seed = Mnemonic::generate(12)?.to_seed_normalized(""); diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 855dd72b5..0b532e585 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -261,6 +261,7 @@ impl Mint { .localstore .update_mint_quote_state(&mint_request.quote, MintQuoteState::Pending) .await?; + let quote = self .localstore .get_mint_quote(&mint_request.quote) @@ -282,14 +283,6 @@ impl Mint { MintQuoteState::Paid => (), } - let amount_can_issue = quote.amount_paid - quote.amount_issued; - - let messages_amount = mint_request.total_amount().unwrap(); - - if amount_can_issue < messages_amount { - return Err(Error::IssuedQuote); - } - let blinded_messages: Vec = mint_request .outputs .iter() @@ -350,7 +343,10 @@ impl Mint { state: MintQuoteState::Issued, expiry: quote.expiry, amount_paid: quote.amount_paid, - amount_issued: quote.amount_issued + messages_amount, + amount_issued: quote.amount_issued + + mint_request + .total_amount() + .map_err(|_| Error::AmountOverflow)?, request_lookup_id: quote.request_lookup_id, single_use: None, }; From c322bb655a0aa6a3cdae87b99876b88fd4f97625 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 26 Oct 2024 12:22:20 +0100 Subject: [PATCH 06/35] feat: wait invoice response --- crates/cdk-cln/src/lib.rs | 12 ++++++++--- crates/cdk-fake-wallet/src/lib.rs | 11 +++++++--- crates/cdk-integration-tests/tests/mint.rs | 10 +++++++-- crates/cdk-lnbits/src/lib.rs | 12 ++++++++--- crates/cdk-lnd/src/lib.rs | 13 +++++++++--- crates/cdk-phoenixd/src/lib.rs | 11 +++++++--- crates/cdk-strike/src/lib.rs | 15 ++++++++++---- crates/cdk/src/cdk_lightning/mod.rs | 13 +++++++++++- crates/cdk/src/mint/mint_nut04.rs | 24 +++++++++++++++------- crates/cdk/src/mint/mod.rs | 4 ++-- 10 files changed, 94 insertions(+), 31 deletions(-) diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index f9287990b..dde022677 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use cdk::amount::{amount_for_offer, to_unit, Amount}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, + PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; @@ -105,7 +105,7 @@ impl MintLightning for Cln { // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let last_pay_index = self.get_last_pay_index().await?; let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?; @@ -197,7 +197,13 @@ impl MintLightning for Cln { None => payment_hash, }; - break Some(((request_look_up, amount_sats.into()), (cln_client, last_pay_idx, cancel_token, is_active))); + let response = WaitInvoiceResponse { + payment_lookup_id: request_look_up, + payment_amount: amount_sats.into(), + unit: CurrencyUnit::Sat + }; + + break Some((response, (cln_client, last_pay_idx, cancel_token, is_active))); } Err(e) => { tracing::warn!("Error fetching invoice: {e}"); diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 34af1c8d7..ea4b2696c 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -16,7 +16,7 @@ use bitcoin::secp256k1::{Secp256k1, SecretKey}; use cdk::amount::{to_unit, Amount}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, + PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, }; use cdk::mint; use cdk::mint::types::PaymentRequest; @@ -122,11 +122,16 @@ impl MintLightning for FakeWallet { async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; let receiver_stream = ReceiverStream::new(receiver); self.wait_invoice_is_active.store(true, Ordering::SeqCst); - Ok(Box::pin(receiver_stream.map(|label| (label, Amount::ZERO)))) + + Ok(Box::pin(receiver_stream.map(|label| WaitInvoiceResponse { + payment_lookup_id: label, + payment_amount: Amount::ZERO, + unit: CurrencyUnit::Sat, + }))) } async fn get_payment_quote( diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index b1ff2642e..5fd98b2b6 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::mint_memory::MintMemoryDatabase; +use cdk::cdk_lightning::WaitInvoiceResponse; use cdk::dhke::construct_proofs; use cdk::mint::MintQuote; use cdk::nuts::{ @@ -79,8 +80,13 @@ async fn mint_proofs( mint.localstore.add_mint_quote(quote.clone()).await?; - mint.pay_mint_quote_for_request_id(&request_lookup, amount) - .await?; + let wait_invoice = WaitInvoiceResponse { + payment_lookup_id: request_lookup, + payment_amount: amount, + unit: CurrencyUnit::Sat, + }; + + mint.pay_mint_quote_for_request_id(wait_invoice).await?; let keyset_id = Id::from(&keys); let premint = PreMintSecrets::random(keyset_id, amount, split_target)?; diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index 2248d3d3b..b0993caf6 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -13,7 +13,7 @@ use axum::Router; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, + PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; @@ -92,7 +92,7 @@ impl MintLightning for LNbits { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self .receiver .lock() @@ -129,7 +129,13 @@ impl MintLightning for LNbits { match check { Ok(state) => { if state { - Some(((msg, Amount::ZERO), (receiver, lnbits_api, cancel_token, is_active))) + let response = WaitInvoiceResponse { + payment_lookup_id: msg, + payment_amount: Amount::ZERO, + unit: CurrencyUnit::Sat + }; + + Some((response , (receiver, lnbits_api, cancel_token, is_active))) } else { None } diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 5fdf7c9ac..cd4167d40 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -16,7 +16,7 @@ use async_trait::async_trait; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, + PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; @@ -99,7 +99,7 @@ impl MintLightning for Lnd { async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let mut client = fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file) .await @@ -141,7 +141,14 @@ impl MintLightning for Lnd { match msg { Ok(Some(msg)) => { if msg.state == 1 { - Some(((hex::encode(msg.r_hash), Amount::ZERO), (stream, cancel_token, is_active))) + let wait_response = WaitInvoiceResponse { + payment_lookup_id: hex::encode(msg.r_hash), + payment_amount: Amount::ZERO, + unit: CurrencyUnit::Sat + + }; + + Some((wait_response , (stream, cancel_token, is_active))) } else { None } diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index e5a8a9052..75979916f 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -14,7 +14,7 @@ use axum::Router; use cdk::amount::{amount_for_offer, to_unit, Amount}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, + PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; @@ -101,7 +101,7 @@ impl MintLightning for Phoenixd { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { let receiver = self .receiver .lock() @@ -135,8 +135,13 @@ impl MintLightning for Phoenixd { match check { Ok(state) => { if state.is_paid { + let wait_invoice = WaitInvoiceResponse { + payment_lookup_id: msg.payment_hash, + payment_amount: Amount::ZERO, + unit: CurrencyUnit::Sat + }; // Yield the payment hash and continue the stream - Some(((msg.payment_hash, Amount::ZERO), (receiver, phoenixd_api, cancel_token, is_active))) + Some((wait_invoice, (receiver, phoenixd_api, cancel_token, is_active))) } else { // Invoice not paid yet, continue waiting // We need to continue the stream, so we return the same state diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 7a0251f78..588bcb744 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -13,7 +13,7 @@ use axum::Router; use cdk::amount::Amount; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, + PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, }; use cdk::nuts::{ CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, @@ -89,7 +89,7 @@ impl MintLightning for Strike { #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err> { + ) -> Result + Send>>, Self::Err> { self.strike_api .subscribe_to_invoice_webhook(self.webhook_url.clone()) .await?; @@ -103,6 +103,7 @@ impl MintLightning for Strike { let strike_api = self.strike_api.clone(); let cancel_token = self.wait_invoice_cancel_token.clone(); + let unit = self.unit.clone(); Ok(futures::stream::unfold( ( @@ -110,8 +111,9 @@ impl MintLightning for Strike { strike_api, cancel_token, Arc::clone(&self.wait_invoice_is_active), + unit ), - |(mut receiver, strike_api, cancel_token, is_active)| async move { + |(mut receiver, strike_api, cancel_token, is_active, unit)| async move { tokio::select! { _ = cancel_token.cancelled() => { @@ -129,7 +131,12 @@ impl MintLightning for Strike { match check { Ok(state) => { if state.state == InvoiceState::Paid { - Some(((msg, Amount::ZERO), (receiver, strike_api, cancel_token, is_active))) + let wait_response = WaitInvoiceResponse { + payment_lookup_id: msg, + payment_amount: Amount::ZERO, + unit + }; + Some((wait_response , (receiver, strike_api, cancel_token, is_active, unit))) } else { None } diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 1c0d58de9..9596eb00d 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -51,6 +51,17 @@ pub enum Error { Amount(#[from] crate::amount::Error), } +/// Wait any invoice response +#[derive(Debug, Clone, Hash, Serialize, Deserialize, Default)] +pub struct WaitInvoiceResponse { + /// Payment look up id + pub payment_lookup_id: String, + /// Payment amount + pub payment_amount: Amount, + /// Unit + pub unit: CurrencyUnit, +} + /// MintLighting Trait #[async_trait] pub trait MintLightning { @@ -88,7 +99,7 @@ pub trait MintLightning { /// Returns a stream of request_lookup_id once invoices are paid async fn wait_any_invoice( &self, - ) -> Result + Send>>, Self::Err>; + ) -> Result + Send>>, Self::Err>; /// Is wait invoice active fn is_wait_invoice_active(&self) -> bool; diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 0b532e585..ce26bce86 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -1,6 +1,9 @@ use tracing::instrument; -use crate::{nuts::MintQuoteState, types::LnKey, util::unix_time, Amount, Error}; +use crate::{ + cdk_lightning::WaitInvoiceResponse, nuts::MintQuoteState, types::LnKey, util::unix_time, + Amount, Error, +}; use super::{ nut04, CurrencyUnit, Mint, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response, @@ -193,17 +196,22 @@ impl Mint { #[instrument(skip_all)] pub async fn pay_mint_quote_for_request_id( &self, - request_lookup_id: &str, - amount: Amount, + wait_invoice_response: WaitInvoiceResponse, ) -> Result<(), Error> { + let WaitInvoiceResponse { + payment_lookup_id, + payment_amount, + unit, + } = wait_invoice_response; if let Ok(Some(mint_quote)) = self .localstore - .get_mint_quote_by_request_lookup_id(request_lookup_id) + .get_mint_quote_by_request_lookup_id(&payment_lookup_id) .await { tracing::debug!( - "Received payment notification for mint quote {}", - mint_quote.id + "Quote {} paid by lookup id {}", + mint_quote.id, + payment_lookup_id ); self.localstore .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) @@ -214,7 +222,9 @@ impl Mint { .await? .unwrap(); - let amount_paid = quote.amount_paid + amount; + let amount_paid = quote.amount_paid + payment_amount; + + assert!(unit == quote.unit); let quote = MintQuote { id: quote.id, diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 30a6b02cf..0aacdd475 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -209,8 +209,8 @@ impl Mint { result = ln.wait_any_invoice() => { match result { Ok(mut stream) => { - while let Some(request_lookup_id) = stream.next().await { - if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id.0, request_lookup_id.1).await { + while let Some(wait_invoice_response) = stream.next().await { + if let Err(err) = mint.pay_mint_quote_for_request_id(wait_invoice_response).await { tracing::warn!("{:?}", err); } } From aa31ad3d57670ca658cbacd6b59f462b5a2a472e Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 26 Oct 2024 13:45:31 +0100 Subject: [PATCH 07/35] fix: wallet bolt12 mint --- crates/cdk/src/wallet/client.rs | 4 ++-- crates/cdk/src/wallet/mint_bolt12.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index 16b5bd110..68659134c 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -148,7 +148,7 @@ impl HttpClient { amount: Amount, unit: CurrencyUnit, description: Option, - ) -> Result { + ) -> Result { let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt12"])?; let request = MintQuoteBolt11Request { @@ -162,7 +162,7 @@ impl HttpClient { let res = res.json::().await?; - match serde_json::from_value::(res.clone()) { + match serde_json::from_value::(res.clone()) { Ok(mint_quote_response) => Ok(mint_quote_response), Err(err) => { tracing::warn!("{}", err); diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs index bfc87ec0c..28f5876de 100644 --- a/crates/cdk/src/wallet/mint_bolt12.rs +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -53,7 +53,7 @@ impl Wallet { amount, unit, request: quote_res.request, - state: quote_res.state, + state: crate::nuts::MintQuoteState::Unpaid, expiry: quote_res.expiry.unwrap_or(0), amount_minted: Amount::ZERO, amount_paid: Amount::ZERO, From 22faa1df7999bd39a76619acd1cd80d4e6f306fc Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 30 Oct 2024 13:20:18 +0000 Subject: [PATCH 08/35] chore: clippy --- crates/cdk-strike/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 588bcb744..4337a6df5 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -103,7 +103,7 @@ impl MintLightning for Strike { let strike_api = self.strike_api.clone(); let cancel_token = self.wait_invoice_cancel_token.clone(); - let unit = self.unit.clone(); + let unit = self.unit; Ok(futures::stream::unfold( ( From 05b00c114e00263bff81de9d991fcd3514e007af Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 30 Oct 2024 14:41:48 +0000 Subject: [PATCH 09/35] feat: only enable bolt12 if configured --- crates/cdk-cli/src/main.rs | 4 ++-- crates/cdk-cli/src/sub_commands/melt.rs | 1 - crates/cdk-mintd/src/main.rs | 24 +++++++++++++----------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 7d1a286e2..fe36f3876 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -25,7 +25,7 @@ const DEFAULT_WORK_DIR: &str = ".cdk-cli"; #[derive(Parser)] #[command(name = "cashu-tool")] #[command(author = "thesimplekid ")] -#[command(version = "0.1.0")] +#[command(version = "0.4.0")] #[command(author, version, about, long_about = None)] struct Cli { /// Database engine to use (sqlite/redb) @@ -85,7 +85,7 @@ enum Commands { #[tokio::main] async fn main() -> Result<()> { let args: Cli = Cli::parse(); - let default_filter = "warn"; + let default_filter = args.log_level; let sqlx_filter = "sqlx=warn"; diff --git a/crates/cdk-cli/src/sub_commands/melt.rs b/crates/cdk-cli/src/sub_commands/melt.rs index 4ed5e09a6..e2f9a0282 100644 --- a/crates/cdk-cli/src/sub_commands/melt.rs +++ b/crates/cdk-cli/src/sub_commands/melt.rs @@ -6,7 +6,6 @@ use anyhow::{bail, Result}; use cdk::amount::Amount; use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey}; -// use cdk::Bolt11Invoice; use clap::Args; use crate::sub_commands::balance::mint_balances; diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 1f668c35e..34323d3d4 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -153,18 +153,20 @@ async fn main() -> anyhow::Result<()> { cln.clone(), ); - let ln_key = LnKey { - unit: CurrencyUnit::Sat, - method: PaymentMethod::Bolt12, - }; - ln_backends.insert(ln_key, cln.clone()); + if cln_settings.bolt12 { + let ln_key = LnKey { + unit: CurrencyUnit::Sat, + method: PaymentMethod::Bolt12, + }; + ln_backends.insert(ln_key, cln.clone()); - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt12, - mint_melt_limits, - cln, - ) + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt12, + mint_melt_limits, + cln, + ) + } } LnBackend::Strike => { let strike_settings = settings.clone().strike.expect("Checked on config load"); From d51526b898fe223cf400c6cc6dddf918857703f3 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 30 Oct 2024 14:46:30 +0000 Subject: [PATCH 10/35] feat: swagger for bolt12 --- crates/cdk-axum/src/bolt12_router.rs | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/cdk-axum/src/bolt12_router.rs b/crates/cdk-axum/src/bolt12_router.rs index cf9d7cb26..963c9260a 100644 --- a/crates/cdk-axum/src/bolt12_router.rs +++ b/crates/cdk-axum/src/bolt12_router.rs @@ -9,6 +9,14 @@ use cdk::nuts::{ use crate::{into_response, MintState}; +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/mint/quote/bolt12", + responses( + (status = 200, description = "Successful response", body = MintQuoteBolt12Response, content_type = "application/json") + ) +))] /// Get mint bolt12 quote pub async fn get_mint_bolt12_quote( State(state): State, @@ -23,6 +31,18 @@ pub async fn get_mint_bolt12_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1", + path = "/mint/quote/bolt12/{quote_id}", + params( + ("quote_id" = String, description = "The quote ID"), + ), + responses( + (status = 200, description = "Successful response", body = MintQuoteBolt12Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] /// Get mint bolt12 quote pub async fn get_check_mint_bolt12_quote( State(state): State, @@ -37,6 +57,16 @@ pub async fn get_check_mint_bolt12_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/mint/bolt12", + request_body(content = MintBolt11Request, description = "Request params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] /// Request a quote for melting tokens pub async fn post_mint_bolt12( State(state): State, @@ -54,6 +84,16 @@ pub async fn post_mint_bolt12( Ok(Json(res)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/melt/quote/bolt12", + request_body(content = MeltQuoteBolt12Request, description = "Quote params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] pub async fn get_melt_bolt12_quote( State(state): State, Json(payload): Json, @@ -67,6 +107,19 @@ pub async fn get_melt_bolt12_quote( Ok(Json(quote)) } +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1", + path = "/melt/bolt12", + request_body(content = MeltBolt12Request, description = "Melt params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange +/// +/// Requests tokens to be destroyed and sent out via Lightning. pub async fn post_melt_bolt12( State(state): State, Json(payload): Json, From 6410d2b6855d14720652bd35b68c27a9aa5a1582 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 30 Oct 2024 15:07:03 +0000 Subject: [PATCH 11/35] feat: return error for lnd bolt12 --- crates/cdk-lnd/src/error.rs | 3 +++ crates/cdk-lnd/src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/cdk-lnd/src/error.rs b/crates/cdk-lnd/src/error.rs index 8d7d7fda6..3c62d6093 100644 --- a/crates/cdk-lnd/src/error.rs +++ b/crates/cdk-lnd/src/error.rs @@ -20,6 +20,9 @@ pub enum Error { /// Payment failed #[error("LND payment failed")] PaymentFailed, + /// Unsupported method + #[error("Unsupported method")] + UnsupportedMethod, /// Wrong invoice type #[error("Wrong invoice type")] WrongRequestType, diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index cd4167d40..7f1fcf836 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -403,7 +403,7 @@ impl MintLightning for Lnd { &self, _melt_quote_request: &MeltQuoteBolt12Request, ) -> Result { - todo!() + Err(Error::UnsupportedMethod.into()) } /// Pay a bolt12 offer @@ -413,7 +413,7 @@ impl MintLightning for Lnd { _amount: Option, _max_fee_amount: Option, ) -> Result { - todo!() + Err(Error::UnsupportedMethod.into()) } /// Create bolt12 offer @@ -425,6 +425,6 @@ impl MintLightning for Lnd { _unix_expiry: u64, _single_use: bool, ) -> Result { - todo!() + Err(Error::UnsupportedMethod.into()) } } From 3c56b376e6d7e86e2a91e81e06247db231f683d5 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 31 Oct 2024 12:51:54 +0000 Subject: [PATCH 12/35] chore: bolt 12 request updates --- .pre-commit-config.yaml | 2 +- crates/cdk-cli/src/sub_commands/mint.rs | 22 +++++++++++++++------- crates/cdk-redb/src/mint/migrations.rs | 2 +- crates/cdk-sqlite/src/mint/mod.rs | 2 +- crates/cdk/src/mint/mint_18.rs | 4 +--- crates/cdk/src/mint/mint_nut04.rs | 6 +++--- crates/cdk/src/mint/types.rs | 7 +++++-- crates/cdk/src/nuts/nut19.rs | 4 ++-- crates/cdk/src/wallet/client.rs | 12 ++---------- crates/cdk/src/wallet/mint_bolt12.rs | 18 ++++++++++++++---- flake.lock | 8 ++++---- flake.nix | 4 ++-- 12 files changed, 51 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f79b367e..285c0be37 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/v4wkgb0g7safy7b8s1qjfsvgqzjjdvss-pre-commit-config.json \ No newline at end of file +/nix/store/dqzwknkkrd4k0s60rrl86m0xyfqpkcfi-pre-commit-config.json \ No newline at end of file diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index 4f250f8b6..605da3b6e 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -9,17 +9,15 @@ use cdk::mint_url::MintUrl; use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod}; use cdk::wallet::multi_mint_wallet::WalletKey; use cdk::wallet::{MultiMintWallet, Wallet}; -use cdk::Amount; use clap::Args; -use serde::{Deserialize, Serialize}; use tokio::time::sleep; -#[derive(Args, Serialize, Deserialize)] +#[derive(Args)] pub struct MintSubCommand { /// Mint url mint_url: MintUrl, /// Amount - amount: u64, + amount: Option, /// Currency unit e.g. sat #[arg(short, long, default_value = "sat")] unit: String, @@ -27,7 +25,6 @@ pub struct MintSubCommand { #[arg(long, default_value = "bolt11")] method: String, /// Quote description - #[serde(skip_serializing_if = "Option::is_none")] description: Option, } @@ -60,12 +57,23 @@ pub async fn mint( PaymentMethod::Bolt11 => { println!("Bolt11"); wallet - .mint_quote(Amount::from(sub_command_args.amount), description) + .mint_quote( + sub_command_args + .amount + .expect("Amount must be defined") + .into(), + description, + ) .await? } PaymentMethod::Bolt12 => { wallet - .mint_bolt12_quote(Amount::from(sub_command_args.amount), description) + .mint_bolt12_quote( + sub_command_args.amount.map(|a| a.into()), + description, + false, + None, + ) .await? } _ => panic!("Unsupported unit"), diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index 0f8101406..4b90e50c5 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -176,7 +176,7 @@ impl From for MintQuote { // TODO: Create real migrations amount_paid: Amount::ZERO, amount_issued: Amount::ZERO, - single_use: None, + single_use: true, } } } diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index 121d43dde..ce980be6f 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -1300,7 +1300,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { // TODO: Get these values amount_paid: Amount::ZERO, amount_issued: Amount::ZERO, - single_use: None, + single_use: true, }) } diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index cdc9c8183..ec7a37d0a 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -52,8 +52,6 @@ impl Mint { return Err(Error::InvoiceDescriptionUnsupported); } - let single_use = single_use.unwrap_or(true); - let create_invoice_response = ln .create_bolt12_offer( amount, @@ -77,7 +75,7 @@ impl Mint { create_invoice_response.request_lookup_id.clone(), Amount::ZERO, Amount::ZERO, - Some(single_use), + single_use, ); tracing::debug!( diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index ce26bce86..7861b30b5 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -107,7 +107,7 @@ impl Mint { create_invoice_response.request_lookup_id.clone(), Amount::ZERO, Amount::ZERO, - None, + true, ); tracing::debug!( @@ -237,7 +237,7 @@ impl Mint { request_lookup_id: quote.request_lookup_id, amount_paid, amount_issued: quote.amount_issued, - single_use: None, + single_use: quote.single_use, }; tracing::debug!( @@ -358,7 +358,7 @@ impl Mint { .total_amount() .map_err(|_| Error::AmountOverflow)?, request_lookup_id: quote.request_lookup_id, - single_use: None, + single_use: quote.single_use, }; self.localstore.add_mint_quote(mint_quote).await?; diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index d764e2ac3..09ef6357c 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -30,11 +30,14 @@ pub struct MintQuote { /// Value used by ln backend to look up state of request pub request_lookup_id: String, /// Amount paid + #[serde(default)] pub amount_paid: Amount, /// Amount issued + #[serde(default)] pub amount_issued: Amount, /// Single use - pub single_use: Option, + #[serde(default)] + pub single_use: bool, } impl MintQuote { @@ -49,7 +52,7 @@ impl MintQuote { request_lookup_id: String, amount_paid: Amount, amount_issued: Amount, - single_use: Option, + single_use: bool, ) -> Self { let id = Uuid::new_v4(); diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs index 992fb0572..a15529005 100644 --- a/crates/cdk/src/nuts/nut19.rs +++ b/crates/cdk/src/nuts/nut19.rs @@ -29,7 +29,7 @@ pub struct MintQuoteBolt12Request { /// Memo to create the invoice with pub description: Option, /// Single use - pub single_use: Option, + pub single_use: bool, /// Expiry pub expiry: Option, } @@ -42,7 +42,7 @@ pub struct MintQuoteBolt12Response { /// Payment request to fulfil pub request: String, /// Single use - pub single_use: Option, + pub single_use: bool, /// Unix timestamp until the quote is valid pub expiry: Option, /// Amount that has been paid diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index 68659134c..a9d6af927 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -9,7 +9,7 @@ use super::Error; use crate::error::ErrorResponse; use crate::mint_url::MintUrl; use crate::nuts::nut15::Mpp; -use crate::nuts::nut19::MintQuoteBolt12Response; +use crate::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, MeltBolt11Request, MeltBolt12Request, MeltQuoteBolt11Request, @@ -145,18 +145,10 @@ impl HttpClient { pub async fn post_mint_bolt12_quote( &self, mint_url: MintUrl, - amount: Amount, - unit: CurrencyUnit, - description: Option, + request: MintQuoteBolt12Request, ) -> Result { let url = mint_url.join_paths(&["v1", "mint", "quote", "bolt12"])?; - let request = MintQuoteBolt11Request { - amount, - unit, - description, - }; - let res = self.inner.post(url).json(&request).send().await?; println!("{:?}", res); diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs index 28f5876de..824214d93 100644 --- a/crates/cdk/src/wallet/mint_bolt12.rs +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -2,7 +2,7 @@ use tracing::instrument; use super::MintQuote; use crate::nuts::nut00::ProofsMethods; -use crate::nuts::nut19::MintQuoteBolt12Response; +use crate::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use crate::nuts::PaymentMethod; use crate::{ amount::SplitTarget, @@ -18,8 +18,10 @@ impl Wallet { #[instrument(skip(self))] pub async fn mint_bolt12_quote( &self, - amount: Amount, + amount: Option, description: Option, + single_use: bool, + expiry: Option, ) -> Result { let mint_url = self.mint_url.clone(); let unit = self.unit; @@ -41,16 +43,24 @@ impl Wallet { } } + let mint_request = MintQuoteBolt12Request { + amount, + unit, + description, + single_use, + expiry, + }; + let quote_res = self .client - .post_mint_bolt12_quote(mint_url.clone(), amount, unit, description) + .post_mint_bolt12_quote(mint_url.clone(), mint_request) .await?; let quote = MintQuote { mint_url, id: quote_res.quote.clone(), payment_method: PaymentMethod::Bolt12, - amount, + amount: amount.unwrap_or(Amount::ZERO), unit, request: quote_res.request, state: crate::nuts::MintQuoteState::Unpaid, diff --git a/flake.lock b/flake.lock index d8d8de915..f03717648 100644 --- a/flake.lock +++ b/flake.lock @@ -57,16 +57,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730137625, - "narHash": "sha256-9z8oOgFZiaguj+bbi3k4QhAD6JabWrnv7fscC/mt0KE=", + "lastModified": 1730200266, + "narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64b80bfb316b57cdb8919a9110ef63393d74382a", + "rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index b40785577..003ab6982 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CDK Flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -63,7 +63,7 @@ pkg-config curl just - protobuf3_20 + protobuf nixpkgs-fmt rust-analyzer typos From 9780b3810bd5d7a07025ee8ac894ef6bc26aa7ed Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 31 Oct 2024 13:01:44 +0000 Subject: [PATCH 13/35] fix: uuid as isser --- crates/cdk-cln/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index dde022677..23679c6d1 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -618,12 +618,16 @@ impl MintLightning for Cln { None => "any".to_string(), }; + // It seems that the only way to force cln to create a unique offer + // is to encode some random data in the offer + let issuer = Uuid::new_v4().to_string(); + let cln_response = cln_client .call(cln_rpc::Request::Offer(OfferRequest { absolute_expiry: Some(unix_expiry), description: Some(description), label: Some(label), - issuer: None, + issuer: Some(issuer), quantity_max: None, recurrence: None, recurrence_base: None, From 6fa9b41695ae358ebc7d5b00a4e44ad3e87249f4 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 31 Oct 2024 14:15:06 +0000 Subject: [PATCH 14/35] feat: cli bolt12 flags --- crates/cdk-cli/src/sub_commands/mint.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index 605da3b6e..43e2cf336 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -26,6 +26,12 @@ pub struct MintSubCommand { method: String, /// Quote description description: Option, + /// Expiry + #[arg(short, long)] + expiry: Option, + /// Expiry + #[arg(short, long)] + single_use: Option, } pub async fn mint( @@ -71,8 +77,8 @@ pub async fn mint( .mint_bolt12_quote( sub_command_args.amount.map(|a| a.into()), description, - false, - None, + sub_command_args.single_use.unwrap_or(false), + sub_command_args.expiry, ) .await? } From 3764dae63281ee3ea81ce5fe97ffa7520accb8b2 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 31 Oct 2024 19:45:12 +0000 Subject: [PATCH 15/35] feat: bolt12 trait --- crates/cdk-cln/src/bolt12.rs | 222 ++++++++++++++++++ crates/cdk-cln/src/lib.rs | 211 +---------------- crates/cdk-fake-wallet/src/lib.rs | 37 +-- .../cdk-integration-tests/src/init_regtest.rs | 1 + crates/cdk-integration-tests/src/lib.rs | 2 + crates/cdk-integration-tests/tests/mint.rs | 3 +- crates/cdk-lnbits/src/lib.rs | 37 +-- crates/cdk-lnd/src/lib.rs | 37 +-- crates/cdk-mintd/src/mint.rs | 2 + crates/cdk-phoenixd/src/bolt12.rs | 114 +++++++++ crates/cdk-phoenixd/src/lib.rs | 106 +-------- crates/cdk-strike/src/lib.rs | 36 +-- crates/cdk/src/cdk_lightning/bolt12.rs | 39 +++ crates/cdk/src/cdk_lightning/mod.rs | 27 +-- crates/cdk/src/mint/melt.rs | 34 ++- crates/cdk/src/mint/mint_18.rs | 7 +- crates/cdk/src/mint/mod.rs | 10 + 17 files changed, 451 insertions(+), 474 deletions(-) create mode 100644 crates/cdk-cln/src/bolt12.rs create mode 100644 crates/cdk-phoenixd/src/bolt12.rs create mode 100644 crates/cdk/src/cdk_lightning/bolt12.rs diff --git a/crates/cdk-cln/src/bolt12.rs b/crates/cdk-cln/src/bolt12.rs new file mode 100644 index 000000000..e7ad1f9a3 --- /dev/null +++ b/crates/cdk-cln/src/bolt12.rs @@ -0,0 +1,222 @@ +use std::str::FromStr; + +use async_trait::async_trait; +use cdk::amount::{amount_for_offer, to_unit, Amount}; +use cdk::cdk_lightning::bolt12::MintBolt12Lightning; +use cdk::cdk_lightning::{ + self, Bolt12PaymentQuoteResponse, CreateOfferResponse, MintLightning, PayInvoiceResponse, +}; +use cdk::mint; +use cdk::mint::types::PaymentRequest; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt12Request, MeltQuoteState}; +use cdk::util::{hex, unix_time}; +use cln_rpc::model::requests::{FetchinvoiceRequest, OfferRequest, PayRequest}; +use cln_rpc::model::responses::PayStatus; +use cln_rpc::model::Request; +use cln_rpc::primitives::Amount as CLN_Amount; +use lightning::offers::invoice::Bolt12Invoice; +use lightning::offers::offer::Offer; +use uuid::Uuid; + +use super::Cln; +use super::Error; + +#[async_trait] +impl MintBolt12Lightning for Cln { + type Err = cdk_lightning::Error; + + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + let offer = + Offer::from_str(&melt_quote_request.request).map_err(|_| Error::UnknownInvoice)?; + + let amount = match melt_quote_request.amount { + Some(amount) => amount, + None => amount_for_offer(&offer, &CurrencyUnit::Msat)?, + }; + + let mut cln_client = self.cln_client.lock().await; + let cln_response = cln_client + .call(Request::FetchInvoice(FetchinvoiceRequest { + amount_msat: Some(CLN_Amount::from_msat(amount.into())), + offer: melt_quote_request.request.clone(), + payer_note: None, + quantity: None, + recurrence_counter: None, + recurrence_label: None, + recurrence_start: None, + timeout: None, + })) + .await; + + let amount = to_unit(amount, &CurrencyUnit::Msat, &melt_quote_request.unit)?; + + match cln_response { + Ok(cln_rpc::Response::FetchInvoice(invoice_response)) => { + let bolt12_invoice = + Bolt12Invoice::try_from(hex::decode(&invoice_response.invoice).unwrap()) + .unwrap(); + + Ok(Bolt12PaymentQuoteResponse { + request_lookup_id: bolt12_invoice.payment_hash().to_string(), + amount, + fee: Amount::ZERO, + state: MeltQuoteState::Unpaid, + invoice: Some(invoice_response.invoice), + }) + } + c => { + tracing::debug!("{:?}", c); + tracing::error!("Error attempting to pay invoice for offer",); + Err(Error::WrongClnResponse.into()) + } + } + } + + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + _amount: Option, + max_fee: Option, + ) -> Result { + let bolt12 = &match melt_quote.request { + PaymentRequest::Bolt12 { offer: _, invoice } => invoice.ok_or(Error::UnknownInvoice)?, + PaymentRequest::Bolt11 { .. } => return Err(Error::WrongPaymentType.into()), + }; + + let pay_state = self + .check_outgoing_payment(&melt_quote.request_lookup_id) + .await?; + + match pay_state.status { + MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (), + MeltQuoteState::Paid => { + tracing::debug!("Melt attempted on invoice already paid"); + return Err(Self::Err::InvoiceAlreadyPaid); + } + MeltQuoteState::Pending => { + tracing::debug!("Melt attempted on invoice already pending"); + return Err(Self::Err::InvoicePaymentPending); + } + } + + let mut cln_client = self.cln_client.lock().await; + let cln_response = cln_client + .call(Request::Pay(PayRequest { + bolt11: bolt12.to_string(), + amount_msat: None, + label: None, + riskfactor: None, + maxfeepercent: None, + retry_for: None, + maxdelay: None, + exemptfee: None, + localinvreqid: None, + exclude: None, + maxfee: max_fee + .map(|a| { + let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?; + Ok::(CLN_Amount::from_msat( + msat.into(), + )) + }) + .transpose()?, + description: None, + partial_msat: None, + })) + .await; + + let response = match cln_response { + Ok(cln_rpc::Response::Pay(pay_response)) => { + let status = match pay_response.status { + PayStatus::COMPLETE => MeltQuoteState::Paid, + PayStatus::PENDING => MeltQuoteState::Pending, + PayStatus::FAILED => MeltQuoteState::Failed, + }; + PayInvoiceResponse { + payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), + payment_lookup_id: pay_response.payment_hash.to_string(), + status, + total_spent: to_unit( + pay_response.amount_sent_msat.msat(), + &CurrencyUnit::Msat, + &melt_quote.unit, + )?, + unit: melt_quote.unit, + } + } + _ => { + tracing::error!("Error attempting to pay invoice: {}", bolt12); + return Err(Error::WrongClnResponse.into()); + } + }; + + Ok(response) + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + amount: Option, + unit: &CurrencyUnit, + description: String, + unix_expiry: u64, + single_use: bool, + ) -> Result { + let time_now = unix_time(); + assert!(unix_expiry > time_now); + let mut cln_client = self.cln_client.lock().await; + + let label = Uuid::new_v4().to_string(); + + let amount = match amount { + Some(amount) => { + let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; + + amount.to_string() + } + None => "any".to_string(), + }; + + // It seems that the only way to force cln to create a unique offer + // is to encode some random data in the offer + let issuer = Uuid::new_v4().to_string(); + + let cln_response = cln_client + .call(cln_rpc::Request::Offer(OfferRequest { + absolute_expiry: Some(unix_expiry), + description: Some(description), + label: Some(label), + issuer: Some(issuer), + quantity_max: None, + recurrence: None, + recurrence_base: None, + recurrence_limit: None, + recurrence_paywindow: None, + recurrence_start_any_period: None, + single_use: Some(single_use), + amount, + })) + .await + .map_err(Error::from)?; + + match cln_response { + cln_rpc::Response::Offer(offer_res) => { + let offer = Offer::from_str(&offer_res.bolt12).unwrap(); + let expiry = offer.absolute_expiry().map(|t| t.as_secs()); + + Ok(CreateOfferResponse { + request_lookup_id: offer_res.offer_id.to_string(), + request: offer, + expiry, + }) + } + _ => { + tracing::warn!("CLN returned wrong response kind"); + Err(Error::WrongClnResponse.into()) + } + } + } +} diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 23679c6d1..669c642d4 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -11,21 +11,18 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use cdk::amount::{amount_for_offer, to_unit, Amount}; +use cdk::amount::{to_unit, Amount}; use cdk::cdk_lightning::{ - self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, -}; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::util::{hex, unix_time}; use cdk::{mint, Bolt11Invoice}; use cln_rpc::model::requests::{ - FetchinvoiceRequest, InvoiceRequest, ListinvoicesRequest, ListpaysRequest, OfferRequest, - PayRequest, WaitanyinvoiceRequest, + InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest, }; use cln_rpc::model::responses::{ ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus, @@ -35,12 +32,11 @@ use cln_rpc::model::Request; use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny}; use error::Error; use futures::{Stream, StreamExt}; -use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::offer::Offer; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use uuid::Uuid; +mod bolt12; pub mod error; /// CLN mint backend @@ -462,201 +458,6 @@ impl MintLightning for Cln { } } } - - async fn get_bolt12_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result { - let offer = - Offer::from_str(&melt_quote_request.request).map_err(|_| Error::UnknownInvoice)?; - - let amount = match melt_quote_request.amount { - Some(amount) => amount, - None => amount_for_offer(&offer, &CurrencyUnit::Msat)?, - }; - - let mut cln_client = self.cln_client.lock().await; - let cln_response = cln_client - .call(Request::FetchInvoice(FetchinvoiceRequest { - amount_msat: Some(CLN_Amount::from_msat(amount.into())), - offer: melt_quote_request.request.clone(), - payer_note: None, - quantity: None, - recurrence_counter: None, - recurrence_label: None, - recurrence_start: None, - timeout: None, - })) - .await; - - let amount = to_unit(amount, &CurrencyUnit::Msat, &melt_quote_request.unit)?; - - match cln_response { - Ok(cln_rpc::Response::FetchInvoice(invoice_response)) => { - let bolt12_invoice = - Bolt12Invoice::try_from(hex::decode(&invoice_response.invoice).unwrap()) - .unwrap(); - - Ok(Bolt12PaymentQuoteResponse { - request_lookup_id: bolt12_invoice.payment_hash().to_string(), - amount, - fee: Amount::ZERO, - state: MeltQuoteState::Unpaid, - invoice: Some(invoice_response.invoice), - }) - } - c => { - tracing::debug!("{:?}", c); - tracing::error!("Error attempting to pay invoice for offer",); - Err(Error::WrongClnResponse.into()) - } - } - } - - async fn pay_bolt12_offer( - &self, - melt_quote: mint::MeltQuote, - _amount: Option, - max_fee: Option, - ) -> Result { - let bolt12 = &match melt_quote.request { - PaymentRequest::Bolt12 { offer: _, invoice } => invoice.ok_or(Error::UnknownInvoice)?, - PaymentRequest::Bolt11 { .. } => return Err(Error::WrongPaymentType.into()), - }; - - let pay_state = self - .check_outgoing_payment(&melt_quote.request_lookup_id) - .await?; - - match pay_state.status { - MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (), - MeltQuoteState::Paid => { - tracing::debug!("Melt attempted on invoice already paid"); - return Err(Self::Err::InvoiceAlreadyPaid); - } - MeltQuoteState::Pending => { - tracing::debug!("Melt attempted on invoice already pending"); - return Err(Self::Err::InvoicePaymentPending); - } - } - - let mut cln_client = self.cln_client.lock().await; - let cln_response = cln_client - .call(Request::Pay(PayRequest { - bolt11: bolt12.to_string(), - amount_msat: None, - label: None, - riskfactor: None, - maxfeepercent: None, - retry_for: None, - maxdelay: None, - exemptfee: None, - localinvreqid: None, - exclude: None, - maxfee: max_fee - .map(|a| { - let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?; - Ok::(CLN_Amount::from_msat( - msat.into(), - )) - }) - .transpose()?, - description: None, - partial_msat: None, - })) - .await; - - let response = match cln_response { - Ok(cln_rpc::Response::Pay(pay_response)) => { - let status = match pay_response.status { - PayStatus::COMPLETE => MeltQuoteState::Paid, - PayStatus::PENDING => MeltQuoteState::Pending, - PayStatus::FAILED => MeltQuoteState::Failed, - }; - PayInvoiceResponse { - payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), - payment_lookup_id: pay_response.payment_hash.to_string(), - status, - total_spent: to_unit( - pay_response.amount_sent_msat.msat(), - &CurrencyUnit::Msat, - &melt_quote.unit, - )?, - unit: melt_quote.unit, - } - } - _ => { - tracing::error!("Error attempting to pay invoice: {}", bolt12); - return Err(Error::WrongClnResponse.into()); - } - }; - - Ok(response) - } - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - amount: Option, - unit: &CurrencyUnit, - description: String, - unix_expiry: u64, - single_use: bool, - ) -> Result { - let time_now = unix_time(); - assert!(unix_expiry > time_now); - let mut cln_client = self.cln_client.lock().await; - - let label = Uuid::new_v4().to_string(); - - let amount = match amount { - Some(amount) => { - let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; - - amount.to_string() - } - None => "any".to_string(), - }; - - // It seems that the only way to force cln to create a unique offer - // is to encode some random data in the offer - let issuer = Uuid::new_v4().to_string(); - - let cln_response = cln_client - .call(cln_rpc::Request::Offer(OfferRequest { - absolute_expiry: Some(unix_expiry), - description: Some(description), - label: Some(label), - issuer: Some(issuer), - quantity_max: None, - recurrence: None, - recurrence_base: None, - recurrence_limit: None, - recurrence_paywindow: None, - recurrence_start_any_period: None, - single_use: Some(single_use), - amount, - })) - .await - .map_err(Error::from)?; - - match cln_response { - cln_rpc::Response::Offer(offer_res) => { - let offer = Offer::from_str(&offer_res.bolt12).unwrap(); - let expiry = offer.absolute_expiry().map(|t| t.as_secs()); - - Ok(CreateOfferResponse { - request_lookup_id: offer_res.offer_id.to_string(), - request: offer, - expiry, - }) - } - _ => { - tracing::warn!("CLN returned wrong response kind"); - Err(Error::WrongClnResponse.into()) - } - } - } } impl Cln { diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index ea4b2696c..7cbe16e98 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -15,15 +15,13 @@ use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{Secp256k1, SecretKey}; use cdk::amount::{to_unit, Amount}; use cdk::cdk_lightning::{ - self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + WaitInvoiceResponse, }; use cdk::mint; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, -}; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::util::unix_time; use error::Error; use futures::stream::StreamExt; @@ -289,35 +287,6 @@ impl MintLightning for FakeWallet { unit: self.get_settings().unit, }) } - - async fn get_bolt12_payment_quote( - &self, - _melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result { - todo!() - } - - /// Pay a bolt12 offer - async fn pay_bolt12_offer( - &self, - _melt_quote: mint::MeltQuote, - _amount: Option, - _max_fee_amount: Option, - ) -> Result { - todo!() - } - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - _amount: Option, - _unit: &CurrencyUnit, - _description: String, - _unix_expiry: u64, - _single_use: bool, - ) -> Result { - todo!() - } } /// Create fake invoice diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 78074b26e..9b08bca94 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -169,6 +169,7 @@ where quote_ttl, Arc::new(database), ln_backends, + HashMap::new(), supported_units, ) .await?; diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 7a727d7b3..c93231cd2 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -79,9 +79,11 @@ pub async fn start_mint( quote_ttl, Arc::new(MintMemoryDatabase::default()), ln_backends.clone(), + HashMap::new(), supported_units, ) .await?; + let cache_time_to_live = 3600; let cache_time_to_idle = 3600; diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 5fd98b2b6..2f1a996bb 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -48,6 +48,7 @@ async fn new_mint(fee: u64) -> Mint { quote_ttl, Arc::new(MintMemoryDatabase::default()), HashMap::new(), + HashMap::new(), supported_units, ) .await @@ -75,7 +76,7 @@ async fn mint_proofs( request_lookup.to_string(), Amount::ZERO, Amount::ZERO, - None, + true, ); mint.localstore.add_mint_quote(quote.clone()).await?; diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index b0993caf6..3e00ce717 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -12,14 +12,12 @@ use async_trait::async_trait; use axum::Router; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::cdk_lightning::{ - self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, -}; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::util::unix_time; use cdk::{mint, Bolt11Invoice}; use error::Error; @@ -335,35 +333,6 @@ impl MintLightning for LNbits { Ok(pay_response) } - - async fn get_bolt12_payment_quote( - &self, - _melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } - - /// Pay a bolt12 offer - async fn pay_bolt12_offer( - &self, - _melt_quote: mint::MeltQuote, - _amount: Option, - _max_fee_amount: Option, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - _amount: Option, - _unit: &CurrencyUnit, - _description: String, - _unix_expiry: u64, - _single_use: bool, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } } fn lnbits_to_melt_status(status: &str, pending: bool) -> MeltQuoteState { diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 7f1fcf836..2fabed455 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -15,14 +15,12 @@ use anyhow::anyhow; use async_trait::async_trait; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::cdk_lightning::{ - self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, -}; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::util::{hex, unix_time}; use cdk::{mint, Bolt11Invoice}; use error::Error; @@ -398,33 +396,4 @@ impl MintLightning for Lnd { // If the stream is exhausted without a final status Err(Error::UnknownPaymentStatus.into()) } - - async fn get_bolt12_payment_quote( - &self, - _melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } - - /// Pay a bolt12 offer - async fn pay_bolt12_offer( - &self, - _melt_quote: mint::MeltQuote, - _amount: Option, - _max_fee_amount: Option, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - _amount: Option, - _unit: &CurrencyUnit, - _description: String, - _unix_expiry: u64, - _single_use: bool, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } } diff --git a/crates/cdk-mintd/src/mint.rs b/crates/cdk-mintd/src/mint.rs index d4eb1ad05..852d1c20b 100644 --- a/crates/cdk-mintd/src/mint.rs +++ b/crates/cdk-mintd/src/mint.rs @@ -219,6 +219,8 @@ impl MintBuilder { .clone() .ok_or(anyhow!("Localstore not set"))?, self.ln.clone().ok_or(anyhow!("Ln backends not set"))?, + // TODO: bolt12 + HashMap::new(), self.supported_units.clone(), ) .await?) diff --git a/crates/cdk-phoenixd/src/bolt12.rs b/crates/cdk-phoenixd/src/bolt12.rs new file mode 100644 index 000000000..ebf7ae731 --- /dev/null +++ b/crates/cdk-phoenixd/src/bolt12.rs @@ -0,0 +1,114 @@ +use std::str::FromStr; + +use anyhow::anyhow; +use async_trait::async_trait; +use cdk::amount::{amount_for_offer, Amount}; +use cdk::cdk_lightning::bolt12::MintBolt12Lightning; +use cdk::cdk_lightning::{ + self, Bolt12PaymentQuoteResponse, CreateOfferResponse, MintLightning, PayInvoiceResponse, +}; +use cdk::mint; +use cdk::mint::types::PaymentRequest; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt12Request, MeltQuoteState}; +use cdk::util::hex; +use lightning::offers::offer::Offer; + +use super::Error; +use crate::Phoenixd; + +#[async_trait] +impl MintBolt12Lightning for Phoenixd { + type Err = cdk_lightning::Error; + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + if CurrencyUnit::Sat != melt_quote_request.unit { + return Err(Error::UnsupportedUnit.into()); + } + + let offer = Offer::from_str(&melt_quote_request.request) + .map_err(|_| Error::Anyhow(anyhow!("Invalid offer")))?; + + let amount = match melt_quote_request.amount { + Some(amount) => amount, + None => amount_for_offer(&offer, &CurrencyUnit::Sat)?, + }; + + let relative_fee_reserve = + (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; + + let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); + + let mut fee = match relative_fee_reserve > absolute_fee_reserve { + true => relative_fee_reserve, + false => absolute_fee_reserve, + }; + + // Fee in phoenixd is always 0.04 + 4 sat + fee += 4; + + Ok(Bolt12PaymentQuoteResponse { + request_lookup_id: hex::encode(offer.id().0), + amount, + fee: fee.into(), + state: MeltQuoteState::Unpaid, + invoice: None, + }) + } + + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + amount: Option, + _max_fee_amount: Option, + ) -> Result { + let offer = &match melt_quote.request { + PaymentRequest::Bolt12 { offer, invoice: _ } => offer, + PaymentRequest::Bolt11 { .. } => return Err(Error::WrongRequestType.into()), + }; + + let amount = match amount { + Some(amount) => amount, + None => amount_for_offer(offer, &CurrencyUnit::Sat)?, + }; + + let pay_response = self + .phoenixd_api + .pay_bolt12_offer(offer.to_string(), amount.into(), None) + .await?; + + // The pay invoice response does not give the needed fee info so we have to check. + let check_outgoing_response = self + .check_outgoing_payment(&pay_response.payment_id) + .await?; + + tracing::debug!( + "Phd offer {} with amount {} with fee {} total spent {}", + check_outgoing_response.status, + amount, + check_outgoing_response.total_spent - amount, + check_outgoing_response.total_spent + ); + + Ok(PayInvoiceResponse { + payment_lookup_id: pay_response.payment_id, + payment_preimage: Some(pay_response.payment_preimage), + status: check_outgoing_response.status, + total_spent: check_outgoing_response.total_spent, + unit: CurrencyUnit::Sat, + }) + } + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + _amount: Option, + _unit: &CurrencyUnit, + _description: String, + _unix_expiry: u64, + _single_use: bool, + ) -> Result { + Err(Error::UnsupportedMethod.into()) + } +} diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index 75979916f..fd01e1c09 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -11,26 +11,23 @@ use std::sync::Arc; use anyhow::anyhow; use async_trait::async_trait; use axum::Router; -use cdk::amount::{amount_for_offer, to_unit, Amount}; +use cdk::amount::{to_unit, Amount}; use cdk::cdk_lightning::{ - self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + WaitInvoiceResponse, }; use cdk::mint::types::PaymentRequest; use cdk::mint::FeeReserve; -use cdk::nuts::{ - CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, -}; -use cdk::util::hex; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::{mint, Bolt11Invoice}; use error::Error; use futures::{Stream, StreamExt}; -use lightning::offers::offer::Offer; use phoenixd_rs::webhooks::WebhookResponse; use phoenixd_rs::{InvoiceRequest, Phoenixd as PhoenixdApi}; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; +mod bolt12; pub mod error; /// Phoenixd @@ -331,97 +328,4 @@ impl MintLightning for Phoenixd { Ok(state) } - - async fn get_bolt12_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result { - if CurrencyUnit::Sat != melt_quote_request.unit { - return Err(Error::UnsupportedUnit.into()); - } - - let offer = Offer::from_str(&melt_quote_request.request) - .map_err(|_| Error::Anyhow(anyhow!("Invalid offer")))?; - - let amount = match melt_quote_request.amount { - Some(amount) => amount, - None => amount_for_offer(&offer, &CurrencyUnit::Sat)?, - }; - - let relative_fee_reserve = - (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; - - let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into(); - - let mut fee = match relative_fee_reserve > absolute_fee_reserve { - true => relative_fee_reserve, - false => absolute_fee_reserve, - }; - - // Fee in phoenixd is always 0.04 + 4 sat - fee += 4; - - Ok(Bolt12PaymentQuoteResponse { - request_lookup_id: hex::encode(offer.id().0), - amount, - fee: fee.into(), - state: MeltQuoteState::Unpaid, - invoice: None, - }) - } - - async fn pay_bolt12_offer( - &self, - melt_quote: mint::MeltQuote, - amount: Option, - _max_fee_amount: Option, - ) -> Result { - let offer = &match melt_quote.request { - PaymentRequest::Bolt12 { offer, invoice: _ } => offer, - PaymentRequest::Bolt11 { .. } => return Err(Error::WrongRequestType.into()), - }; - - let amount = match amount { - Some(amount) => amount, - None => amount_for_offer(offer, &CurrencyUnit::Sat)?, - }; - - let pay_response = self - .phoenixd_api - .pay_bolt12_offer(offer.to_string(), amount.into(), None) - .await?; - - // The pay invoice response does not give the needed fee info so we have to check. - let check_outgoing_response = self - .check_outgoing_payment(&pay_response.payment_id) - .await?; - - tracing::debug!( - "Phd offer {} with amount {} with fee {} total spent {}", - check_outgoing_response.status, - amount, - check_outgoing_response.total_spent - amount, - check_outgoing_response.total_spent - ); - - Ok(PayInvoiceResponse { - payment_lookup_id: pay_response.payment_id, - payment_preimage: Some(pay_response.payment_preimage), - status: check_outgoing_response.status, - total_spent: check_outgoing_response.total_spent, - unit: CurrencyUnit::Sat, - }) - } - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - _amount: Option, - _unit: &CurrencyUnit, - _description: String, - _unix_expiry: u64, - _single_use: bool, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } } diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 4337a6df5..9849ca03b 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -12,12 +12,10 @@ use async_trait::async_trait; use axum::Router; use cdk::amount::Amount; use cdk::cdk_lightning::{ - self, Bolt12PaymentQuoteResponse, CreateInvoiceResponse, CreateOfferResponse, MintLightning, - PayInvoiceResponse, PaymentQuoteResponse, Settings, WaitInvoiceResponse, -}; -use cdk::nuts::{ - CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteState, MintQuoteState, + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, + WaitInvoiceResponse, }; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::util::unix_time; use cdk::{mint, Bolt11Invoice}; use error::Error; @@ -308,34 +306,6 @@ impl MintLightning for Strike { Ok(pay_invoice_response) } - - async fn get_bolt12_payment_quote( - &self, - _melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } - - async fn pay_bolt12_offer( - &self, - _melt_quote: mint::MeltQuote, - _amount: Option, - _max_fee_amount: Option, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - _amount: Option, - _unit: &CurrencyUnit, - _description: String, - _unix_expiry: u64, - _single_use: bool, - ) -> Result { - Err(Error::UnsupportedMethod.into()) - } } impl Strike { diff --git a/crates/cdk/src/cdk_lightning/bolt12.rs b/crates/cdk/src/cdk_lightning/bolt12.rs new file mode 100644 index 000000000..e4e806953 --- /dev/null +++ b/crates/cdk/src/cdk_lightning/bolt12.rs @@ -0,0 +1,39 @@ +//! CDK Mint Bolt12 + +use async_trait::async_trait; + +use super::{Bolt12PaymentQuoteResponse, CreateOfferResponse, Error, PayInvoiceResponse}; +use crate::nuts::nut20::MeltQuoteBolt12Request; +use crate::nuts::CurrencyUnit; +use crate::{mint, Amount}; + +/// MintLighting Bolt12 Trait +#[async_trait] +pub trait MintBolt12Lightning { + /// Mint Lightning Error + type Err: Into + From; + + /// Bolt12 Payment quote + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result; + + /// Pay a bolt12 offer + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + amount: Option, + max_fee_amount: Option, + ) -> Result; + + /// Create bolt12 offer + async fn create_bolt12_offer( + &self, + amount: Option, + unit: &CurrencyUnit, + description: String, + unix_expiry: u64, + single_use: bool, + ) -> Result; +} diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 9596eb00d..b3cd588c1 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -9,10 +9,11 @@ use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::nuts::nut20::MeltQuoteBolt12Request; use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use crate::{mint, Amount}; +pub mod bolt12; + /// CDK Lightning Error #[derive(Debug, Error)] pub enum Error { @@ -118,30 +119,6 @@ pub trait MintLightning { &self, request_lookup_id: &str, ) -> Result; - - /// Bolt12 Payment quote - async fn get_bolt12_payment_quote( - &self, - melt_quote_request: &MeltQuoteBolt12Request, - ) -> Result; - - /// Pay a bolt12 offer - async fn pay_bolt12_offer( - &self, - melt_quote: mint::MeltQuote, - amount: Option, - max_fee_amount: Option, - ) -> Result; - - /// Create bolt12 offer - async fn create_bolt12_offer( - &self, - amount: Option, - unit: &CurrencyUnit, - description: String, - unix_expiry: u64, - single_use: bool, - ) -> Result; } /// Create invoice response diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index d94263a90..517252924 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -148,7 +148,7 @@ impl Mint { self.check_melt_request_acceptable(amount, *unit, PaymentMethod::Bolt12)?; let ln = self - .ln + .bolt12_backends .get(&LnKey::new(*unit, PaymentMethod::Bolt12)) .ok_or_else(|| { tracing::info!("Could not get ln backend for {}, bolt11 ", unit); @@ -557,10 +557,42 @@ impl Mint { let attempt_to_pay = match melt_request.get_payment_method() { PaymentMethod::Bolt11 => { + let ln = match self.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) { + Some(ln) => ln, + None => { + tracing::info!( + "Could not get ln backend for {}, bolt11 ", + quote.unit + ); + if let Err(err) = self.process_unpaid_melt(melt_request).await { + tracing::error!("Could not reset melt quote state: {}", err); + } + + return Err(Error::UnitUnsupported); + } + }; ln.pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve)) .await } PaymentMethod::Bolt12 => { + let ln = match self + .bolt12_backends + .get(&LnKey::new(quote.unit, PaymentMethod::Bolt12)) + { + Some(ln) => ln, + None => { + tracing::info!( + "Could not get ln backend for {}, bolt11 ", + quote.unit + ); + if let Err(err) = self.process_unpaid_melt(melt_request).await { + tracing::error!("Could not reset melt quote state: {}", err); + } + + return Err(Error::UnitUnsupported); + } + }; + ln.pay_bolt12_offer(quote.clone(), partial_amount, Some(quote.fee_reserve)) .await } diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index ec7a37d0a..2487a5af9 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -34,7 +34,7 @@ impl Mint { } let ln = self - .ln + .bolt12_backends .get(&LnKey::new(unit, PaymentMethod::Bolt12)) .ok_or_else(|| { tracing::info!("Bolt11 mint request for unsupported unit"); @@ -47,11 +47,6 @@ impl Mint { None => unix_time() + self.quote_ttl.mint_ttl, }; - if description.is_some() && !ln.get_settings().invoice_description { - tracing::error!("Backend does not support invoice description"); - return Err(Error::InvoiceDescriptionUnsupported); - } - let create_invoice_response = ln .create_bolt12_offer( amount, diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 0aacdd475..eeb97bbdc 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -14,6 +14,7 @@ use tracing::instrument; use self::types::PaymentRequest; use crate::cdk_database::{self, MintDatabase}; +use crate::cdk_lightning::bolt12::MintBolt12Lightning; use crate::cdk_lightning::{self, MintLightning}; use crate::dhke::{sign_message, verify_message}; use crate::error::Error; @@ -48,6 +49,9 @@ pub struct Mint { pub localstore: Arc + Send + Sync>, /// Ln backends for mint pub ln: HashMap + Send + Sync>>, + /// Ln backends for mint + pub bolt12_backends: + HashMap + Send + Sync>>, /// Active Mint Keysets keysets: Arc>>, secp_ctx: Secp256k1, @@ -63,6 +67,10 @@ impl Mint { quote_ttl: QuoteTTL, localstore: Arc + Send + Sync>, ln: HashMap + Send + Sync>>, + bolt12: HashMap< + LnKey, + Arc + Send + Sync>, + >, // Hashmap where the key is the unit and value is (input fee ppk, max_order) supported_units: HashMap, ) -> Result { @@ -178,6 +186,7 @@ impl Mint { localstore, mint_info, ln, + bolt12_backends: bolt12, }) } @@ -719,6 +728,7 @@ mod tests { config.quote_ttl, localstore, HashMap::new(), + HashMap::new(), config.supported_units, ) .await From fbdd3d62294c2769ac86b5a8d93db31c5c23922d Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Tue, 5 Nov 2024 21:22:22 +0000 Subject: [PATCH 16/35] fix: flake --- .pre-commit-config.yaml | 2 +- flake.lock | 34 +++++++++++++--------------------- flake.nix | 2 +- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 285c0be37..b244f9c82 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/dqzwknkkrd4k0s60rrl86m0xyfqpkcfi-pre-commit-config.json \ No newline at end of file +/nix/store/c904x2hnkcm7xn6bin3nf7jzsfaaylzw-pre-commit-config.json \ No newline at end of file diff --git a/flake.lock b/flake.lock index b0ee7c436..52a605216 100644 --- a/flake.lock +++ b/flake.lock @@ -57,35 +57,27 @@ }, "nixpkgs": { "locked": { -<<<<<<< HEAD - "lastModified": 1730200266, - "narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd", -======= "lastModified": 1730741070, "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", "owner": "NixOS", "repo": "nixpkgs", "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", ->>>>>>> main "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-stable": { "locked": { - "lastModified": 1720386169, - "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", + "lastModified": 1730741070, + "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", "type": "github" }, "original": { @@ -97,11 +89,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1719082008, - "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "lastModified": 1730768919, + "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", "type": "github" }, "original": { @@ -119,11 +111,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1730302582, - "narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=", + "lastModified": 1730814269, + "narHash": "sha256-fWPHyhYE6xvMI1eGY3pwBTq85wcy1YXqdzTZF+06nOg=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf", + "rev": "d70155fdc00df4628446352fc58adc640cd705c2", "type": "github" }, "original": { @@ -147,11 +139,11 @@ ] }, "locked": { - "lastModified": 1730687492, - "narHash": "sha256-xQVadjquBA/tFxDt5A55LJ1D1AvkVWsnrKC2o+pr8F4=", + "lastModified": 1730773675, + "narHash": "sha256-pULo7GryzLkqGveWvnNWVz1Kk6EJqvq+HQeSkwvr7DA=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "41814763a2c597755b0755dbe3e721367a5e420f", + "rev": "e19e9d54fac1e53f73411ebe22d19f946b1ba0bd", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2226b1629..dc67d02eb 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CDK Flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; rust-overlay = { url = "github:oxalica/rust-overlay"; From 5fe70de8f8e32a50b25abb2cb264417e6dd13a8f Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Tue, 5 Nov 2024 21:33:44 +0000 Subject: [PATCH 17/35] chore: clippy --- crates/cdk/src/wallet/mint_bolt12.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs index 47c556092..1ca25f537 100644 --- a/crates/cdk/src/wallet/mint_bolt12.rs +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -35,7 +35,7 @@ impl Wallet { .ok_or(Error::IncorrectMint)? .nuts .nut04 - .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11) + .get_settings(unit, &crate::nuts::PaymentMethod::Bolt11) .ok_or(Error::UnsupportedUnit)?; if !mint_method_settings.description { From 05b8bb14f75eb278565c7361983ecc9383c68818 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 6 Nov 2024 08:38:03 +0000 Subject: [PATCH 18/35] feat: mint builder to cdk --- .pre-commit-config.yaml | 2 +- crates/cdk-mintd/src/lib.rs | 1 - crates/cdk-mintd/src/main.rs | 2 +- .../src/mint.rs => cdk/src/mint/builder.rs} | 18 +++++++++++++----- crates/cdk/src/mint/mod.rs | 2 ++ flake.lock | 8 ++++---- flake.nix | 2 +- 7 files changed, 22 insertions(+), 13 deletions(-) rename crates/{cdk-mintd/src/mint.rs => cdk/src/mint/builder.rs} (96%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b244f9c82..41cc13f5f 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/c904x2hnkcm7xn6bin3nf7jzsfaaylzw-pre-commit-config.json \ No newline at end of file +/nix/store/9p08fy8g4sijw8k1l9spf8npjbjk34xf-pre-commit-config.json \ No newline at end of file diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 92ab8f49b..9cc385198 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; pub mod cli; pub mod config; -pub mod mint; pub mod setup; fn expand_path(path: &str) -> Option { diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index d5e8fd29d..4222304a9 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -15,9 +15,9 @@ use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; use cdk::mint::{MeltQuote, Mint}; +use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::{ContactInfo, CurrencyUnit, MeltQuoteState, MintVersion, PaymentMethod}; use cdk::types::LnKey; -use cdk_mintd::mint::{MintBuilder, MintMeltLimits}; use cdk_mintd::setup::LnBackendSetup; use cdk_redb::MintRedbDatabase; use cdk_sqlite::MintSqliteDatabase; diff --git a/crates/cdk-mintd/src/mint.rs b/crates/cdk/src/mint/builder.rs similarity index 96% rename from crates/cdk-mintd/src/mint.rs rename to crates/cdk/src/mint/builder.rs index 889d191ac..ae91fa5b0 100644 --- a/crates/cdk-mintd/src/mint.rs +++ b/crates/cdk/src/mint/builder.rs @@ -1,8 +1,10 @@ -use core::panic; +//! Mint Builder + use std::{collections::HashMap, sync::Arc}; use anyhow::anyhow; -use cdk::{ + +use crate::{ amount::Amount, cdk_database::{self, MintDatabase}, cdk_lightning::{self, MintLightning}, @@ -31,6 +33,7 @@ pub struct MintBuilder { } impl MintBuilder { + /// New mint builder pub fn new() -> MintBuilder { MintBuilder::default() } @@ -44,7 +47,7 @@ impl MintBuilder { self } - // Set mint url + /// Set mint url pub fn with_mint_url(mut self, mint_url: String) -> Self { self.mint_url = Some(mint_url); self @@ -181,7 +184,6 @@ impl MintBuilder { self.mint_info.nuts.nut19 = Some(nut19_settings); } - _ => panic!("Unsupported unit"), } ln.insert(ln_key.clone(), ln_backend); @@ -206,12 +208,13 @@ impl MintBuilder { } /// Set pubkey - pub fn with_pubkey(mut self, pubkey: cdk::nuts::PublicKey) -> Self { + pub fn with_pubkey(mut self, pubkey: crate::nuts::PublicKey) -> Self { self.mint_info.pubkey = Some(pubkey); self } + /// Build mint pub async fn build(&self) -> anyhow::Result { Ok(Mint::new( self.mint_url.as_ref().ok_or(anyhow!("Mint url not set"))?, @@ -231,10 +234,15 @@ impl MintBuilder { } } +/// Mint Melt Limits #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct MintMeltLimits { + /// Min mint amount pub mint_min: Amount, + /// Max mint amount pub mint_max: Amount, + /// Min melt amount pub melt_min: Amount, + /// Max melt amount pub melt_max: Amount, } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 01fc94685..4c77c320b 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -25,6 +25,7 @@ use crate::types::{LnKey, QuoteTTL}; use crate::util::unix_time; use crate::Amount; +mod builder; mod check_spendable; mod info; mod keysets; @@ -34,6 +35,7 @@ mod mint_nut04; mod swap; pub mod types; +pub use builder::{MintBuilder, MintMeltLimits}; pub use types::{MeltQuote, MintQuote}; /// Cashu Mint diff --git a/flake.lock b/flake.lock index 52a605216..afd3e2bc1 100644 --- a/flake.lock +++ b/flake.lock @@ -57,16 +57,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730741070, - "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", + "lastModified": 1730785428, + "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", + "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index dc67d02eb..2226b1629 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CDK Flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay = { url = "github:oxalica/rust-overlay"; From f656bf16dfb6fc67cd38bd3d9df627ae3a535ec7 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 6 Nov 2024 16:55:58 +0000 Subject: [PATCH 19/35] chore: nix flake stable --- .pre-commit-config.yaml | 2 +- flake.lock | 8 ++++---- flake.nix | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41cc13f5f..b244f9c82 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/9p08fy8g4sijw8k1l9spf8npjbjk34xf-pre-commit-config.json \ No newline at end of file +/nix/store/c904x2hnkcm7xn6bin3nf7jzsfaaylzw-pre-commit-config.json \ No newline at end of file diff --git a/flake.lock b/flake.lock index afd3e2bc1..52a605216 100644 --- a/flake.lock +++ b/flake.lock @@ -57,16 +57,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730785428, - "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", + "lastModified": 1730741070, + "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", + "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 2226b1629..dc67d02eb 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CDK Flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; rust-overlay = { url = "github:oxalica/rust-overlay"; From 9c1341dcb67b2a6351cb8bfbf386efcdd0fb502e Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 8 Nov 2024 19:58:07 +0000 Subject: [PATCH 20/35] feat: nut19 signature on mint witness --- bindings/cdk-js/src/nuts/nut04.rs | 12 +- bindings/cdk-js/src/wallet.rs | 4 +- crates/cdk-cli/src/sub_commands/mint.rs | 6 +- crates/cdk-integration-tests/src/lib.rs | 6 +- .../tests/fake_wallet.rs | 114 +++++++++++++--- crates/cdk-integration-tests/tests/mint.rs | 2 + crates/cdk-integration-tests/tests/regtest.rs | 27 ++-- crates/cdk-redb/src/mint/migrations.rs | 1 + .../20241108093102_mint_mint_quote_pubkey.sql | 1 + crates/cdk-sqlite/src/mint/mod.rs | 11 +- ...0241108092756_wallet_mint_quote_pubkey.sql | 1 + crates/cdk-sqlite/src/wallet/mod.rs | 11 +- crates/cdk/examples/mint-token.rs | 4 +- crates/cdk/examples/p2pk.rs | 4 +- crates/cdk/examples/proof-selection.rs | 4 +- crates/cdk/examples/wallet.rs | 4 +- crates/cdk/src/error.rs | 20 ++- crates/cdk/src/mint/mint_nut04.rs | 9 ++ crates/cdk/src/mint/types.rs | 6 +- crates/cdk/src/nuts/mod.rs | 1 + crates/cdk/src/nuts/nut04.rs | 13 +- crates/cdk/src/nuts/nut19.rs | 122 ++++++++++++++++++ crates/cdk/src/wallet/mint.rs | 29 ++++- crates/cdk/src/wallet/multi_mint_wallet.rs | 8 +- crates/cdk/src/wallet/types.rs | 4 +- flake.lock | 12 +- flake.nix | 2 +- 27 files changed, 369 insertions(+), 69 deletions(-) create mode 100644 crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql create mode 100644 crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql create mode 100644 crates/cdk/src/nuts/nut19.rs diff --git a/bindings/cdk-js/src/nuts/nut04.rs b/bindings/cdk-js/src/nuts/nut04.rs index 6cbdb33ce..aa90310c3 100644 --- a/bindings/cdk-js/src/nuts/nut04.rs +++ b/bindings/cdk-js/src/nuts/nut04.rs @@ -88,10 +88,18 @@ impl From for JsMintBolt11Request { impl JsMintBolt11Request { /// Try From Base 64 String #[wasm_bindgen(constructor)] - pub fn new(quote: String, outputs: JsValue) -> Result { + pub fn new( + quote: String, + outputs: JsValue, + witness: Option, + ) -> Result { let outputs = serde_wasm_bindgen::from_value(outputs).map_err(into_err)?; Ok(JsMintBolt11Request { - inner: MintBolt11Request { quote, outputs }, + inner: MintBolt11Request { + quote, + outputs, + witness, + }, }) } diff --git a/bindings/cdk-js/src/wallet.rs b/bindings/cdk-js/src/wallet.rs index 8f4cf405c..50e8c9642 100644 --- a/bindings/cdk-js/src/wallet.rs +++ b/bindings/cdk-js/src/wallet.rs @@ -93,7 +93,7 @@ impl JsWallet { ) -> Result { let quote = self .inner - .mint_quote(amount.into(), description) + .mint_quote(amount.into(), description, None) .await .map_err(into_err)?; @@ -142,7 +142,7 @@ impl JsWallet { Ok(self .inner - .mint("e_id, target, conditions) + .mint("e_id, target, conditions, None) .await .map_err(into_err)? .into()) diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index 46ce6a27c..daf486e04 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -52,7 +52,7 @@ pub async fn mint( }; let quote = wallet - .mint_quote(Amount::from(sub_command_args.amount), description) + .mint_quote(Amount::from(sub_command_args.amount), description, None) .await?; println!("Quote: {:#?}", quote); @@ -69,7 +69,9 @@ pub async fn mint( sleep(Duration::from_secs(2)).await; } - let receive_amount = wallet.mint("e.id, SplitTarget::default(), None).await?; + let receive_amount = wallet + .mint("e.id, SplitTarget::default(), None, None) + .await?; println!("Received {receive_amount} from mint {mint_url}"); diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 2e52b0345..d16c95d3d 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -125,7 +125,7 @@ pub async fn wallet_mint( split_target: SplitTarget, description: Option, ) -> Result<()> { - let quote = wallet.mint_quote(amount, description).await?; + let quote = wallet.mint_quote(amount, description, None).await?; loop { let status = wallet.mint_quote_state("e.id).await?; @@ -138,7 +138,7 @@ pub async fn wallet_mint( sleep(Duration::from_secs(2)).await; } - let receive_amount = wallet.mint("e.id, split_target, None).await?; + let receive_amount = wallet.mint("e.id, split_target, None, None).await?; println!("Minted: {}", receive_amount); @@ -161,6 +161,7 @@ pub async fn mint_proofs( amount, unit: CurrencyUnit::Sat, description, + pubkey: None, }; let mint_quote = wallet_client @@ -187,6 +188,7 @@ pub async fn mint_proofs( let request = MintBolt11Request { quote: mint_quote.quote, outputs: premint_secrets.blinded_messages(), + witness: None, }; let mint_response = wallet_client.post_mint(mint_url.parse()?, request).await?; diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index cf0ea1bef..bb0851f45 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -1,12 +1,13 @@ use std::sync::Arc; use std::time::Duration; -use anyhow::Result; +use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::amount::SplitTarget; use cdk::cdk_database::WalletMemoryDatabase; use cdk::nuts::{ - CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, PreMintSecrets, State, + CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, PreMintSecrets, SecretKey, + State, }; use cdk::wallet::client::{HttpClient, HttpClientMethods}; use cdk::wallet::Wallet; @@ -27,12 +28,12 @@ async fn test_fake_tokens_pending() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -67,12 +68,12 @@ async fn test_fake_melt_payment_fail() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -130,12 +131,12 @@ async fn test_fake_melt_payment_fail_and_check() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -175,12 +176,12 @@ async fn test_fake_melt_payment_return_fail_status() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -235,12 +236,12 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -296,12 +297,12 @@ async fn test_fake_melt_payment_err_paid() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription { @@ -334,12 +335,12 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; let fake_description = FakeInvoiceDescription::default(); @@ -377,6 +378,87 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_with_witness() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + let secret = SecretKey::generate(); + let mint_quote = wallet + .mint_quote(100.into(), None, Some(secret.public_key())) + .await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; + + let mint_amount = wallet + .mint(&mint_quote.id, SplitTarget::default(), None, Some(secret)) + .await?; + + assert!(mint_amount == 100.into()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_without_witness() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let secret = SecretKey::generate(); + let mint_quote = wallet + .mint_quote(100.into(), None, Some(secret.public_key())) + .await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; + + let mint_amount = wallet + .mint(&mint_quote.id, SplitTarget::default(), None, None) + .await; + + match mint_amount { + Err(cdk::error::Error::SecretKeyNotProvided) => Ok(()), + _ => bail!("Wrong mint response for minting without witness"), + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fake_mint_with_wrong_witness() -> Result<()> { + let wallet = Wallet::new( + MINT_URL, + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + let secret = SecretKey::generate(); + let mint_quote = wallet + .mint_quote(100.into(), None, Some(secret.public_key())) + .await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; + let secret = SecretKey::generate(); + + let mint_amount = wallet + .mint(&mint_quote.id, SplitTarget::default(), None, Some(secret)) + .await; + + match mint_amount { + Err(cdk::error::Error::IncorrectSecretKey) => Ok(()), + _ => { + bail!("Wrong mint response for minting without witness") + } + } +} + // Keep polling the state of the mint quote id until it's paid async fn wait_for_mint_to_be_paid(wallet: &Wallet, mint_quote_id: &str) -> Result<()> { loop { diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 3c9f1258a..3082eb207 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -78,6 +78,7 @@ async fn mint_proofs( amount, unix_time() + 36000, request_lookup.to_string(), + None, ); mint.localstore.add_mint_quote(quote.clone()).await?; @@ -90,6 +91,7 @@ async fn mint_proofs( let mint_request = MintBolt11Request { quote: quote.id, outputs: premint.blinded_messages(), + witness: None, }; let after_mint = mint.process_mint_request(mint_request).await?; diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 107fa49eb..737060b0a 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -73,12 +73,12 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> { .expect("Failed to connect"); let (mut write, mut reader) = ws_stream.split(); - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(mint_amount == 100.into()); @@ -151,14 +151,14 @@ async fn test_regtest_mint_melt() -> Result<()> { let mint_amount = Amount::from(100); - let mint_quote = wallet.mint_quote(mint_amount, None).await?; + let mint_quote = wallet.mint_quote(mint_amount, None, None).await?; assert_eq!(mint_quote.amount, mint_amount); lnd_client.pay_invoice(mint_quote.request).await?; let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(mint_amount == 100.into()); @@ -179,12 +179,12 @@ async fn test_restore() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(wallet.total_balance().await? == 100.into()); @@ -235,12 +235,12 @@ async fn test_pay_invoice_twice() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert_eq!(mint_amount, 100.into()); @@ -287,12 +287,12 @@ async fn test_internal_payment() -> Result<()> { None, )?; - let mint_quote = wallet.mint_quote(100.into(), None).await?; + let mint_quote = wallet.mint_quote(100.into(), None, None).await?; lnd_client.pay_invoice(mint_quote.request).await?; let _mint_amount = wallet - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; assert!(wallet.total_balance().await? == 100.into()); @@ -307,7 +307,7 @@ async fn test_internal_payment() -> Result<()> { None, )?; - let mint_quote = wallet_2.mint_quote(10.into(), None).await?; + let mint_quote = wallet_2.mint_quote(10.into(), None, None).await?; let melt = wallet.melt_quote(mint_quote.request.clone(), None).await?; @@ -316,7 +316,7 @@ async fn test_internal_payment() -> Result<()> { let _melted = wallet.melt(&melt.id).await.unwrap(); let _wallet_2_mint = wallet_2 - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await .unwrap(); @@ -358,7 +358,7 @@ async fn test_cached_mint() -> Result<()> { let mint_amount = Amount::from(100); - let quote = wallet.mint_quote(mint_amount, None).await?; + let quote = wallet.mint_quote(mint_amount, None, None).await?; lnd_client.pay_invoice(quote.request).await?; loop { @@ -381,6 +381,7 @@ async fn test_cached_mint() -> Result<()> { let request = MintBolt11Request { quote: quote.id, outputs: premint_secrets.blinded_messages(), + witness: None, }; let response = http_client diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index 90feaeffb..0ba894633 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -59,6 +59,7 @@ impl From for MintQuote { state: quote.state, expiry: quote.expiry, request_lookup_id: Bolt11Invoice::from_str("e.request).unwrap().to_string(), + pubkey: None, } } } diff --git a/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql b/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql new file mode 100644 index 000000000..06501e14f --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql @@ -0,0 +1 @@ +ALTER TABLE mint_quote ADD pubkey TEXT; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index c72e5220a..7bec2ec79 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -205,8 +205,8 @@ WHERE active = 1 let res = sqlx::query( r#" INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry, request_lookup_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?); +(id, mint_url, amount, unit, request, state, expiry, request_lookup_id, pubkey) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -217,6 +217,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?); .bind(quote.state.to_string()) .bind(quote.expiry as i64) .bind(quote.request_lookup_id) + .bind(quote.pubkey.map(|p| p.to_string())) .execute(&mut transaction) .await; @@ -1277,6 +1278,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; let row_request_lookup_id: Option = row.try_get("request_lookup_id").map_err(Error::from)?; + let row_pubkey: Option = row.try_get("pubkey").map_err(Error::from)?; let request_lookup_id = match row_request_lookup_id { Some(id) => id, @@ -1286,6 +1288,10 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { }, }; + let pubkey = row_pubkey + .map(|key| PublicKey::from_str(&key)) + .transpose()?; + Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, @@ -1295,6 +1301,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, expiry: row_expiry as u64, request_lookup_id, + pubkey, }) } diff --git a/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql b/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql new file mode 100644 index 000000000..06501e14f --- /dev/null +++ b/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_pubkey.sql @@ -0,0 +1 @@ +ALTER TABLE mint_quote ADD pubkey TEXT; diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index 0decfac3b..644ca1b20 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -342,8 +342,8 @@ WHERE id=? sqlx::query( r#" INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry) -VALUES (?, ?, ?, ?, ?, ?, ?); +(id, mint_url, amount, unit, request, state, expiry, pubkey) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -353,6 +353,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?); .bind(quote.request) .bind(quote.state.to_string()) .bind(quote.expiry as i64) + .bind(quote.pubkey.map(|p| p.to_string())) .execute(&self.pool) .await .map_err(Error::from)?; @@ -823,9 +824,14 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { 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_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; + let row_pubkey: Option = row.try_get("pubkey").map_err(Error::from)?; let state = MintQuoteState::from_str(&row_state)?; + let pubkey = row_pubkey + .map(|key| PublicKey::from_str(&key)) + .transpose()?; + Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, @@ -834,6 +840,7 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result { request: row_request, state, expiry: row_expiry as u64, + pubkey, }) } diff --git a/crates/cdk/examples/mint-token.rs b/crates/cdk/examples/mint-token.rs index 195fb0ff7..ef2713364 100644 --- a/crates/cdk/examples/mint-token.rs +++ b/crates/cdk/examples/mint-token.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Error> { let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Quote: {:#?}", quote); @@ -39,7 +39,7 @@ async fn main() -> Result<(), Error> { } let receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/examples/p2pk.rs b/crates/cdk/examples/p2pk.rs index 6e51f781e..8868fc138 100644 --- a/crates/cdk/examples/p2pk.rs +++ b/crates/cdk/examples/p2pk.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Error> { let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Minting nuts ..."); @@ -39,7 +39,7 @@ async fn main() -> Result<(), Error> { } let _receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/examples/proof-selection.rs b/crates/cdk/examples/proof-selection.rs index 210b77319..95f67924d 100644 --- a/crates/cdk/examples/proof-selection.rs +++ b/crates/cdk/examples/proof-selection.rs @@ -24,7 +24,7 @@ async fn main() { for amount in [64] { let amount = Amount::from(amount); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Pay request: {}", quote.request); @@ -41,7 +41,7 @@ async fn main() { } let receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/examples/wallet.rs b/crates/cdk/examples/wallet.rs index 93b6fa23e..dc35014c9 100644 --- a/crates/cdk/examples/wallet.rs +++ b/crates/cdk/examples/wallet.rs @@ -24,7 +24,7 @@ async fn main() { let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); - let quote = wallet.mint_quote(amount, None).await.unwrap(); + let quote = wallet.mint_quote(amount, None, None).await.unwrap(); println!("Pay request: {}", quote.request); @@ -41,7 +41,7 @@ async fn main() { } let receive_amount = wallet - .mint("e.id, SplitTarget::default(), None) + .mint("e.id, SplitTarget::default(), None, None) .await .unwrap(); diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 5ddfac64a..ed531c223 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -162,6 +162,12 @@ pub enum Error { /// Invoice Description not supported #[error("Invoice Description not supported")] InvoiceDescriptionUnsupported, + /// Secretkey to sign mint quote not provided + #[error("Secretkey to sign mint quote not provided")] + SecretKeyNotProvided, + /// Incorrect secret key provided + #[error("Incorrect secretkey provided")] + IncorrectSecretKey, /// Custom Error #[error("`{0}`")] Custom(String), @@ -176,7 +182,7 @@ pub enum Error { /// Parse int error #[error(transparent)] ParseInt(#[from] std::num::ParseIntError), - /// Parse Url Error + /// Parse 9rl Error #[error(transparent)] UrlParseError(#[from] url::ParseError), /// Utf8 parse error @@ -239,6 +245,9 @@ pub enum Error { /// NUT18 Error #[error(transparent)] NUT18(#[from] crate::nuts::nut18::Error), + /// NUT19 Error + #[error(transparent)] + NUT19(#[from] crate::nuts::nut19::Error), /// Database Error #[cfg(any(feature = "wallet", feature = "mint"))] #[error(transparent)] @@ -369,6 +378,11 @@ impl From for ErrorResponse { error: Some(err.to_string()), detail: None, }, + Error::NUT19(err) => ErrorResponse { + code: ErrorCode::WitnessMissingOrInvalid, + error: Some(err.to_string()), + detail: None, + }, _ => ErrorResponse { code: ErrorCode::Unknown(9999), error: Some(err.to_string()), @@ -439,6 +453,8 @@ pub enum ErrorCode { TransactionUnbalanced, /// Amount outside of allowed range AmountOutofLimitRange, + /// Witness missing or invalid + WitnessMissingOrInvalid, /// Unknown error code Unknown(u16), } @@ -463,6 +479,7 @@ impl ErrorCode { 20005 => Self::QuotePending, 20006 => Self::InvoiceAlreadyPaid, 20007 => Self::QuoteExpired, + 20008 => Self::WitnessMissingOrInvalid, _ => Self::Unknown(code), } } @@ -486,6 +503,7 @@ impl ErrorCode { Self::QuotePending => 20005, Self::InvoiceAlreadyPaid => 20006, Self::QuoteExpired => 20007, + Self::WitnessMissingOrInvalid => 20008, Self::Unknown(code) => *code, } } diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 5631fd043..c1651e389 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -64,6 +64,7 @@ impl Mint { amount, unit, description, + pubkey, } = mint_quote_request; self.check_mint_request_acceptable(amount, &unit)?; @@ -104,6 +105,7 @@ impl Mint { amount, create_invoice_response.expiry.unwrap_or(0), create_invoice_response.request_lookup_id.clone(), + pubkey, ); tracing::debug!( @@ -146,6 +148,7 @@ impl Mint { request: quote.request, state, expiry: Some(quote.expiry), + pubkey: quote.pubkey, }) } @@ -277,6 +280,12 @@ impl Mint { MintQuoteState::Paid => (), } + // If the there is a public key provoided in mint quote request + // verify the signature is provided for the mint request + if let Some(pubkey) = mint_quote.pubkey { + mint_request.verify_witness(pubkey)?; + } + let blinded_messages: Vec = mint_request .outputs .iter() diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index 44047fd9b..9baac35bb 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::CurrencyUnit; +use super::{CurrencyUnit, PublicKey}; use crate::mint_url::MintUrl; use crate::nuts::{MeltQuoteState, MintQuoteState}; use crate::Amount; @@ -27,6 +27,8 @@ pub struct MintQuote { pub expiry: u64, /// Value used by ln backend to look up state of request pub request_lookup_id: String, + /// Pubkey + pub pubkey: Option, } impl MintQuote { @@ -38,6 +40,7 @@ impl MintQuote { amount: Amount, expiry: u64, request_lookup_id: String, + pubkey: Option, ) -> Self { let id = Uuid::new_v4(); @@ -50,6 +53,7 @@ impl MintQuote { state: MintQuoteState::Unpaid, expiry, request_lookup_id, + pubkey, } } } diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index eb1f81707..7f913f49a 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -21,6 +21,7 @@ pub mod nut15; #[cfg(feature = "mint")] pub mod nut17; pub mod nut18; +pub mod nut19; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 40a6f8d4b..0202c2d76 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod}; -use super::MintQuoteState; +use super::{MintQuoteState, PublicKey}; use crate::Amount; /// NUT04 Error @@ -32,7 +32,11 @@ pub struct MintQuoteBolt11Request { /// Unit wallet would like to pay with pub unit: CurrencyUnit, /// Memo to create the invoice with + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// NUT-19 Pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, } /// Possible states of a quote @@ -90,6 +94,9 @@ pub struct MintQuoteBolt11Response { pub state: MintQuoteState, /// Unix timestamp until the quote is valid pub expiry: Option, + /// NUT-19 Pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, } #[cfg(feature = "mint")] @@ -100,6 +107,7 @@ impl From for MintQuoteBolt11Response { request: mint_quote.request, state: mint_quote.state, expiry: Some(mint_quote.expiry), + pubkey: mint_quote.pubkey, } } } @@ -114,6 +122,9 @@ pub struct MintBolt11Request { /// Outputs #[cfg_attr(feature = "swagger", schema(max_items = 1_000))] pub outputs: Vec, + /// Signature + #[serde(skip_serializing_if = "Option::is_none")] + pub witness: Option, } impl MintBolt11Request { diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs new file mode 100644 index 000000000..7896089b8 --- /dev/null +++ b/crates/cdk/src/nuts/nut19.rs @@ -0,0 +1,122 @@ +//! Mint Quote Signatures + +use std::str::FromStr; + +use bitcoin::secp256k1::schnorr::Signature; +use thiserror::Error; + +use super::{MintBolt11Request, PublicKey, SecretKey}; + +/// Nut19 Error +#[derive(Debug, Error)] +pub enum Error { + /// Witness not provided + #[error("Witness not provided")] + WitnessMissing, + /// Quote witness invalid signature + #[error("Quote witness invalid signature")] + InvalidWitness, + /// Nut01 error + #[error(transparent)] + NUT01(#[from] crate::nuts::nut01::Error), +} + +impl MintBolt11Request { + /// Constructs the message to be signed according to NUT-19 specification. + /// + /// The message is constructed by concatenating: + /// 1. The quote ID + /// 2. All blinded secrets (B_0 through B_n) + /// + /// Format: `quote_id || B_0 || B_1 || ... || B_n` + pub fn msg_to_sign(&self) -> String { + // Pre-calculate capacity to avoid reallocations + let capacity = self.quote.len() + (self.outputs.len() * 66); + let mut msg = String::with_capacity(capacity); + + msg.push_str(&self.quote); + for output in &self.outputs { + msg.push_str(&output.blinded_secret.to_hex()); + } + msg + } + + /// Sign [`MintBolt11Request`] + pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> { + let msg = self.msg_to_sign(); + + let signature: Signature = secret_key.sign(msg.as_bytes())?; + + self.witness = Some(signature.to_string()); + + Ok(()) + } + + /// Verify signature on [`MintBolt11Request`] + pub fn verify_witness(&self, pubkey: PublicKey) -> Result<(), Error> { + let witness = self.witness.as_ref().ok_or(Error::WitnessMissing)?; + + let signature = Signature::from_str(witness).map_err(|_| Error::InvalidWitness)?; + + let msg_to_sign = self.msg_to_sign(); + + pubkey.verify(msg_to_sign.as_bytes(), &signature)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_msg_to_sign() { + let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"witness":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap(); + + let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"; + + let request_msg_to_sign = request.msg_to_sign(); + + assert_eq!(expected_msg_to_sign, request_msg_to_sign); + } + + #[test] + fn test_valid_signature() { + let pubkey = PublicKey::from_hex( + "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", + ) + .unwrap(); + + let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "witness": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap(); + + assert!(request.verify_witness(pubkey).is_ok()); + } + + #[test] + fn test_mint_request_signature() { + let mut request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap(); + + let secret = + SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa") + .unwrap(); + + request.sign(secret.clone()).unwrap(); + + assert!(request.verify_witness(secret.public_key()).is_ok()); + } + + #[test] + fn test_invalid_signature() { + let pubkey = PublicKey::from_hex( + "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", + ) + .unwrap(); + + let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"witness":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap(); + + // Signature is on a different quote id verification should fail + assert!(request.verify_witness(pubkey).is_err()); + } +} diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 77b9bb706..295bd9abd 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -6,7 +6,7 @@ use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, - SpendingConditions, State, + PublicKey, SecretKey, SpendingConditions, State, }; use crate::types::ProofInfo; use crate::util::unix_time; @@ -35,7 +35,7 @@ impl Wallet { /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; /// let amount = Amount::from(100); /// - /// let quote = wallet.mint_quote(amount, None).await?; + /// let quote = wallet.mint_quote(amount, None, None).await?; /// Ok(()) /// } /// ``` @@ -44,6 +44,7 @@ impl Wallet { &self, amount: Amount, description: Option, + pubkey: Option, ) -> Result { let mint_url = self.mint_url.clone(); let unit = self.unit.clone(); @@ -69,6 +70,7 @@ impl Wallet { amount, unit: unit.clone(), description, + pubkey, }; let quote_res = self @@ -84,6 +86,7 @@ impl Wallet { request: quote_res.request, state: quote_res.state, expiry: quote_res.expiry.unwrap_or(0), + pubkey, }; self.localstore.add_mint_quote(quote.clone()).await?; @@ -124,8 +127,9 @@ impl Wallet { let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?; if mint_quote_response.state == MintQuoteState::Paid { + // TODO: Need to pass in keys here let amount = self - .mint(&mint_quote.id, SplitTarget::default(), None) + .mint(&mint_quote.id, SplitTarget::default(), None, None) .await?; total_amount += amount; } else if mint_quote.expiry.le(&unix_time()) { @@ -157,10 +161,12 @@ impl Wallet { /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); /// let amount = Amount::from(100); /// - /// let quote = wallet.mint_quote(amount, None).await?; + /// let quote = wallet.mint_quote(amount, None, None).await?; /// let quote_id = quote.id; /// // To be called after quote request is paid - /// let amount_minted = wallet.mint("e_id, SplitTarget::default(), None).await?; + /// let amount_minted = wallet + /// .mint("e_id, SplitTarget::default(), None, None) + /// .await?; /// /// Ok(()) /// } @@ -171,6 +177,7 @@ impl Wallet { quote_id: &str, amount_split_target: SplitTarget, spending_conditions: Option, + secret_key: Option, ) -> Result { // Check that mint is in store of mints if self @@ -219,11 +226,21 @@ impl Wallet { )?, }; - let request = MintBolt11Request { + let mut request = MintBolt11Request { quote: quote_id.to_string(), outputs: premint_secrets.blinded_messages(), + witness: None, }; + if let Some(pubkey) = quote_info.pubkey { + let secret_key = secret_key.ok_or(Error::SecretKeyNotProvided)?; + if pubkey != secret_key.public_key() { + return Err(Error::IncorrectSecretKey); + } + + request.sign(secret_key)?; + } + let mint_res = self .client .post_mint(self.mint_url.clone(), request) diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index b0f048b2e..346265a70 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -16,7 +16,7 @@ use super::types::SendKind; use super::Error; use crate::amount::SplitTarget; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, Proof, SecretKey, SpendingConditions, Token}; +use crate::nuts::{CurrencyUnit, Proof, PublicKey, SecretKey, SpendingConditions, Token}; use crate::types::Melted; use crate::wallet::types::MintQuote; use crate::{Amount, Wallet}; @@ -166,13 +166,14 @@ impl MultiMintWallet { wallet_key: &WalletKey, amount: Amount, description: Option, + pubkey: Option, ) -> Result { let wallet = self .get_wallet(wallet_key) .await .ok_or(Error::UnknownWallet(wallet_key.clone()))?; - wallet.mint_quote(amount, description).await + wallet.mint_quote(amount, description, pubkey).await } /// Check all mint quotes @@ -215,13 +216,14 @@ impl MultiMintWallet { wallet_key: &WalletKey, quote_id: &str, conditions: Option, + secret_key: Option, ) -> Result { let wallet = self .get_wallet(wallet_key) .await .ok_or(Error::UnknownWallet(wallet_key.clone()))?; wallet - .mint(quote_id, SplitTarget::default(), conditions) + .mint(quote_id, SplitTarget::default(), conditions, secret_key) .await } diff --git a/crates/cdk/src/wallet/types.rs b/crates/cdk/src/wallet/types.rs index 309a4c1cf..af793c882 100644 --- a/crates/cdk/src/wallet/types.rs +++ b/crates/cdk/src/wallet/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState}; +use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PublicKey}; use crate::Amount; /// Mint Quote Info @@ -23,6 +23,8 @@ pub struct MintQuote { pub state: MintQuoteState, /// Expiration time of quote pub expiry: u64, + /// Publickey [NUT-19] + pub pubkey: Option, } /// Melt Quote Info diff --git a/flake.lock b/flake.lock index f01519d10..b0cde25b2 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -139,11 +139,11 @@ ] }, "locked": { - "lastModified": 1731378398, - "narHash": "sha256-a0QWaiX8+AJ9/XBLGMDy6c90GD7HzpxKVdlFwCke5Pw=", + "lastModified": 1731637922, + "narHash": "sha256-6iuzRINXyPX4DfUQZIGafpJnzjFXjVRYMymB10/jFFY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0ae9fc2f2fe5361837d59c0bdebbda176427111e", + "rev": "db10c66da18e816030b884388545add8cf096647", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index aebb30d00..468552bd4 100644 --- a/flake.nix +++ b/flake.nix @@ -53,7 +53,7 @@ targets = [ "wasm32-unknown-unknown" ]; # wasm }; - # Nighly for creating lock files + # Nightly used for formatting nightly_toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { extensions = [ "rustfmt" "clippy" "rust-analyzer" ]; }); From 311aa23701219df53fc57e21b7811c3102876f6a Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 16 Nov 2024 09:06:41 +0000 Subject: [PATCH 21/35] feat: use utf-8 bytes --- crates/cdk/src/nuts/nut19.rs | 45 +++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs index 7896089b8..a4e789625 100644 --- a/crates/cdk/src/nuts/nut19.rs +++ b/crates/cdk/src/nuts/nut19.rs @@ -24,19 +24,20 @@ pub enum Error { impl MintBolt11Request { /// Constructs the message to be signed according to NUT-19 specification. /// - /// The message is constructed by concatenating: - /// 1. The quote ID - /// 2. All blinded secrets (B_0 through B_n) + /// The message is constructed by concatenating (as UTF-8 encoded bytes): + /// 1. The quote ID (as UTF-8) + /// 2. All blinded secrets (B_0 through B_n) converted to hex strings (as UTF-8) /// /// Format: `quote_id || B_0 || B_1 || ... || B_n` - pub fn msg_to_sign(&self) -> String { + /// where each component is encoded as UTF-8 bytes + pub fn msg_to_sign(&self) -> Vec { // Pre-calculate capacity to avoid reallocations let capacity = self.quote.len() + (self.outputs.len() * 66); - let mut msg = String::with_capacity(capacity); - - msg.push_str(&self.quote); + let mut msg = Vec::with_capacity(capacity); + msg.append(&mut self.quote.clone().into_bytes()); // String.into_bytes() produces UTF-8 for output in &self.outputs { - msg.push_str(&output.blinded_secret.to_hex()); + // to_hex() creates a hex string, into_bytes() converts it to UTF-8 bytes + msg.append(&mut output.blinded_secret.to_hex().into_bytes()); } msg } @@ -45,7 +46,7 @@ impl MintBolt11Request { pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> { let msg = self.msg_to_sign(); - let signature: Signature = secret_key.sign(msg.as_bytes())?; + let signature: Signature = secret_key.sign(&msg)?; self.witness = Some(signature.to_string()); @@ -60,7 +61,7 @@ impl MintBolt11Request { let msg_to_sign = self.msg_to_sign(); - pubkey.verify(msg_to_sign.as_bytes(), &signature)?; + pubkey.verify(&msg_to_sign, &signature)?; Ok(()) } @@ -75,7 +76,29 @@ mod tests { fn test_msg_to_sign() { let request: MintBolt11Request = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"witness":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap(); - let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"; + // let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"; + + let expected_msg_to_sign = [ + 57, 100, 55, 52, 53, 50, 55, 48, 45, 49, 52, 48, 53, 45, 52, 54, 100, 101, 45, 98, 53, + 99, 53, 45, 101, 50, 55, 54, 50, 98, 52, 102, 53, 101, 48, 48, 48, 51, 52, 50, 101, 53, + 98, 99, 99, 55, 55, 102, 53, 98, 50, 97, 51, 99, 50, 97, 102, 98, 52, 48, 98, 98, 53, + 57, 49, 97, 49, 101, 50, 55, 100, 97, 56, 51, 99, 100, 100, 99, 57, 54, 56, 97, 98, + 100, 99, 48, 101, 99, 52, 57, 48, 52, 50, 48, 49, 97, 50, 48, 49, 56, 51, 52, 48, 51, + 50, 102, 100, 51, 99, 52, 100, 99, 52, 57, 97, 50, 56, 52, 52, 97, 56, 57, 57, 57, 56, + 100, 53, 101, 57, 100, 53, 98, 48, 102, 48, 98, 48, 48, 100, 100, 101, 57, 51, 49, 48, + 48, 54, 51, 97, 99, 98, 56, 97, 57, 50, 101, 50, 102, 100, 97, 102, 97, 52, 49, 50, 54, + 100, 52, 48, 51, 51, 98, 54, 102, 100, 101, 53, 48, 98, 54, 97, 48, 100, 102, 101, 54, + 49, 97, 100, 49, 52, 56, 102, 102, 102, 49, 54, 55, 97, 100, 57, 99, 102, 56, 51, 48, + 56, 100, 101, 100, 53, 102, 54, 102, 54, 98, 50, 102, 101, 48, 48, 48, 97, 48, 51, 54, + 99, 52, 54, 52, 99, 51, 49, 49, 48, 50, 98, 101, 53, 97, 53, 53, 102, 48, 51, 101, 53, + 99, 48, 97, 97, 101, 97, 55, 55, 53, 57, 53, 100, 53, 55, 52, 98, 99, 101, 57, 50, 99, + 54, 100, 53, 55, 97, 50, 97, 48, 102, 98, 50, 98, 53, 57, 53, 53, 99, 48, 98, 56, 55, + 101, 52, 53, 50, 48, 101, 48, 54, 98, 53, 51, 48, 50, 50, 48, 57, 102, 99, 50, 56, 55, + 51, 102, 50, 56, 53, 50, 49, 99, 98, 100, 100, 101, 55, 102, 55, 98, 51, 98, 98, 49, + 53, 50, 49, 48, 48, 50, 52, 54, 51, 102, 53, 57, 55, 57, 54, 56, 54, 102, 100, 49, 53, + 54, 102, 50, 51, 102, 101, 54, 97, 56, 97, 97, 50, 98, 55, 57, + ] + .to_vec(); let request_msg_to_sign = request.msg_to_sign(); From f427ba612ae689054a2465d7c8f1b3dcfb688c62 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 17 Nov 2024 09:01:07 +0000 Subject: [PATCH 22/35] feat: wait invoice payment id --- .pre-commit-config.yaml | 2 +- crates/cdk-cln/src/bolt12.rs | 10 +++++ crates/cdk-cln/src/lib.rs | 9 +++-- crates/cdk-fake-wallet/src/lib.rs | 3 +- crates/cdk-integration-tests/tests/mint.rs | 5 ++- crates/cdk-lnbits/src/lib.rs | 5 ++- crates/cdk-lnd/src/lib.rs | 7 ++-- crates/cdk-phoenixd/src/bolt12.rs | 11 ++++++ crates/cdk-phoenixd/src/lib.rs | 5 ++- crates/cdk-redb/src/mint/migrations.rs | 4 +- crates/cdk-sqlite/src/mint/mod.rs | 2 + crates/cdk-strike/src/lib.rs | 5 ++- crates/cdk/src/cdk_lightning/bolt12.rs | 12 +++++- crates/cdk/src/cdk_lightning/mod.rs | 8 +++- crates/cdk/src/error.rs | 3 ++ crates/cdk/src/mint/mint_18.rs | 2 + crates/cdk/src/mint/mint_nut04.rs | 45 +++++++++++++++++++--- crates/cdk/src/mint/types.rs | 12 +++++- flake.lock | 8 ++-- flake.nix | 2 +- 20 files changed, 129 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b244f9c82..41cc13f5f 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/c904x2hnkcm7xn6bin3nf7jzsfaaylzw-pre-commit-config.json \ No newline at end of file +/nix/store/9p08fy8g4sijw8k1l9spf8npjbjk34xf-pre-commit-config.json \ No newline at end of file diff --git a/crates/cdk-cln/src/bolt12.rs b/crates/cdk-cln/src/bolt12.rs index e7ad1f9a3..65dfa7a8b 100644 --- a/crates/cdk-cln/src/bolt12.rs +++ b/crates/cdk-cln/src/bolt12.rs @@ -1,3 +1,4 @@ +use std::pin::Pin; use std::str::FromStr; use async_trait::async_trait; @@ -5,6 +6,7 @@ use cdk::amount::{amount_for_offer, to_unit, Amount}; use cdk::cdk_lightning::bolt12::MintBolt12Lightning; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateOfferResponse, MintLightning, PayInvoiceResponse, + WaitInvoiceResponse, }; use cdk::mint; use cdk::mint::types::PaymentRequest; @@ -14,6 +16,7 @@ use cln_rpc::model::requests::{FetchinvoiceRequest, OfferRequest, PayRequest}; use cln_rpc::model::responses::PayStatus; use cln_rpc::model::Request; use cln_rpc::primitives::Amount as CLN_Amount; +use futures::Stream; use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::Offer; use uuid::Uuid; @@ -25,6 +28,13 @@ use super::Error; impl MintBolt12Lightning for Cln { type Err = cdk_lightning::Error; + /// Listen for bolt12 offers to be paid + async fn wait_any_offer( + &self, + ) -> Result + Send>>, Self::Err> { + todo!() + } + async fn get_bolt12_payment_quote( &self, melt_quote_request: &MeltQuoteBolt12Request, diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 669c642d4..708b01d38 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -163,7 +163,7 @@ impl MintLightning for Cln { let amount_msats = wait_any_response.amount_received_msat.expect("status is paid there should be an amount"); let amount_sats = amount_msats.msat() / 1000; - let request_look_up = match wait_any_response.bolt12 { + let request_lookup_id = match wait_any_response.bolt12 { // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up. // Since this is not returned in the wait any response, // we need to do a second query for it. @@ -190,13 +190,14 @@ impl MintLightning for Cln { } } } - None => payment_hash, + None => payment_hash.clone(), }; let response = WaitInvoiceResponse { - payment_lookup_id: request_look_up, + request_lookup_id, payment_amount: amount_sats.into(), - unit: CurrencyUnit::Sat + unit: CurrencyUnit::Sat, + payment_id: payment_hash }; break Some((response, (cln_client, last_pay_idx, cancel_token, is_active))); diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 7cbe16e98..4223d04f3 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -126,9 +126,10 @@ impl MintLightning for FakeWallet { self.wait_invoice_is_active.store(true, Ordering::SeqCst); Ok(Box::pin(receiver_stream.map(|label| WaitInvoiceResponse { - payment_lookup_id: label, + request_lookup_id: label.clone(), payment_amount: Amount::ZERO, unit: CurrencyUnit::Sat, + payment_id: label, }))) } diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 5ad48112c..5e4ec27c3 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -82,14 +82,17 @@ async fn mint_proofs( Amount::ZERO, Amount::ZERO, true, + true, + vec![], ); mint.localstore.add_mint_quote(quote.clone()).await?; let wait_invoice = WaitInvoiceResponse { - payment_lookup_id: request_lookup, + request_lookup_id: request_lookup, payment_amount: amount, unit: CurrencyUnit::Sat, + payment_id: request_lookup, }; mint.pay_mint_quote_for_request_id(wait_invoice).await?; diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index 3e00ce717..f0af24c97 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -128,9 +128,10 @@ impl MintLightning for LNbits { Ok(state) => { if state { let response = WaitInvoiceResponse { - payment_lookup_id: msg, + request_lookup_id: msg.clone(), payment_amount: Amount::ZERO, - unit: CurrencyUnit::Sat + unit: CurrencyUnit::Sat, + payment_id: msg }; Some((response , (receiver, lnbits_api, cancel_token, is_active))) diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 2fabed455..129c7a4f1 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -139,11 +139,12 @@ impl MintLightning for Lnd { match msg { Ok(Some(msg)) => { if msg.state == 1 { + let payment_hash = hex::encode(msg.r_hash); let wait_response = WaitInvoiceResponse { - payment_lookup_id: hex::encode(msg.r_hash), + request_lookup_id: payment_hash.clone(), payment_amount: Amount::ZERO, - unit: CurrencyUnit::Sat - + unit: CurrencyUnit::Sat, + payment_id: payment_hash }; Some((wait_response , (stream, cancel_token, is_active))) diff --git a/crates/cdk-phoenixd/src/bolt12.rs b/crates/cdk-phoenixd/src/bolt12.rs index ebf7ae731..34cd0f815 100644 --- a/crates/cdk-phoenixd/src/bolt12.rs +++ b/crates/cdk-phoenixd/src/bolt12.rs @@ -1,3 +1,4 @@ +use std::pin::Pin; use std::str::FromStr; use anyhow::anyhow; @@ -6,11 +7,13 @@ use cdk::amount::{amount_for_offer, Amount}; use cdk::cdk_lightning::bolt12::MintBolt12Lightning; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateOfferResponse, MintLightning, PayInvoiceResponse, + WaitInvoiceResponse, }; use cdk::mint; use cdk::mint::types::PaymentRequest; use cdk::nuts::{CurrencyUnit, MeltQuoteBolt12Request, MeltQuoteState}; use cdk::util::hex; +use futures::Stream; use lightning::offers::offer::Offer; use super::Error; @@ -19,6 +22,7 @@ use crate::Phoenixd; #[async_trait] impl MintBolt12Lightning for Phoenixd { type Err = cdk_lightning::Error; + async fn get_bolt12_payment_quote( &self, melt_quote_request: &MeltQuoteBolt12Request, @@ -111,4 +115,11 @@ impl MintBolt12Lightning for Phoenixd { ) -> Result { Err(Error::UnsupportedMethod.into()) } + + /// Listen for bolt12 offers to be paid + async fn wait_any_offer( + &self, + ) -> Result + Send>>, Self::Err> { + Err(Error::UnsupportedMethod.into()) + } } diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index fd01e1c09..1615a050f 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -133,9 +133,10 @@ impl MintLightning for Phoenixd { Ok(state) => { if state.is_paid { let wait_invoice = WaitInvoiceResponse { - payment_lookup_id: msg.payment_hash, + request_lookup_id: msg.payment_hash.clone(), payment_amount: Amount::ZERO, - unit: CurrencyUnit::Sat + unit: CurrencyUnit::Sat, + payment_id: msg.payment_hash }; // Yield the payment hash and continue the stream Some((wait_invoice, (receiver, phoenixd_api, cancel_token, is_active))) diff --git a/crates/cdk-redb/src/mint/migrations.rs b/crates/cdk-redb/src/mint/migrations.rs index 4b90e50c5..6766bacf0 100644 --- a/crates/cdk-redb/src/mint/migrations.rs +++ b/crates/cdk-redb/src/mint/migrations.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use cdk::mint::types::PaymentRequest; use cdk::mint::MintQuote; use cdk::mint_url::MintUrl; -use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, Proof, State}; +use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PaymentMethod, Proof, State}; use cdk::Amount; use lightning_invoice::Bolt11Invoice; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; @@ -177,6 +177,8 @@ impl From for MintQuote { amount_paid: Amount::ZERO, amount_issued: Amount::ZERO, single_use: true, + payment_method: PaymentMethod::Bolt11, + payment_ids: vec![], } } } diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index ce980be6f..ba5d14dfe 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -1301,6 +1301,8 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { amount_paid: Amount::ZERO, amount_issued: Amount::ZERO, single_use: true, + payment_method: PaymentMethod::Bolt11, + payment_ids: Vec::new(), }) } diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 57d657850..fff4998df 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -130,9 +130,10 @@ impl MintLightning for Strike { Ok(state) => { if state.state == InvoiceState::Paid { let wait_response = WaitInvoiceResponse { - payment_lookup_id: msg, + request_lookup_id: msg.clone(), payment_amount: Amount::ZERO, - unit: unit.clone() + unit: unit.clone(), + payment_id: msg }; Some((wait_response , (receiver, strike_api, cancel_token, is_active, unit))) } else { diff --git a/crates/cdk/src/cdk_lightning/bolt12.rs b/crates/cdk/src/cdk_lightning/bolt12.rs index e4e806953..a855a245f 100644 --- a/crates/cdk/src/cdk_lightning/bolt12.rs +++ b/crates/cdk/src/cdk_lightning/bolt12.rs @@ -1,8 +1,13 @@ //! CDK Mint Bolt12 +use std::pin::Pin; + use async_trait::async_trait; +use futures::Stream; -use super::{Bolt12PaymentQuoteResponse, CreateOfferResponse, Error, PayInvoiceResponse}; +use super::{ + Bolt12PaymentQuoteResponse, CreateOfferResponse, Error, PayInvoiceResponse, WaitInvoiceResponse, +}; use crate::nuts::nut20::MeltQuoteBolt12Request; use crate::nuts::CurrencyUnit; use crate::{mint, Amount}; @@ -13,6 +18,11 @@ pub trait MintBolt12Lightning { /// Mint Lightning Error type Err: Into + From; + /// Listen for bolt12 offers to be paid + async fn wait_any_offer( + &self, + ) -> Result + Send>>, Self::Err>; + /// Bolt12 Payment quote async fn get_bolt12_payment_quote( &self, diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index b3cd588c1..349459efc 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -55,12 +55,16 @@ pub enum Error { /// Wait any invoice response #[derive(Debug, Clone, Hash, Serialize, Deserialize, Default)] pub struct WaitInvoiceResponse { - /// Payment look up id - pub payment_lookup_id: String, + /// Request look up id + /// Id that relates the quote and payment request + pub request_lookup_id: String, /// Payment amount pub payment_amount: Amount, /// Unit pub unit: CurrencyUnit, + /// Unique id of payment + // Payment hash + pub payment_id: String, } /// MintLighting Trait diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 61c2aaadf..7188b445a 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -105,6 +105,9 @@ pub enum Error { /// Internal Error #[error("Internal Error")] Internal, + /// Payment already added to quote + #[error("Payment is already accounted for in quote")] + PaymentAlreadySeen, // Wallet Errors /// P2PK spending conditions not met diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index 9653bf845..a76266490 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -64,6 +64,7 @@ impl Mint { let quote = MintQuote::new( self.mint_url.clone(), create_invoice_response.request.to_string(), + PaymentMethod::Bolt12, unit.clone(), amount, create_invoice_response.expiry.unwrap_or(0), @@ -71,6 +72,7 @@ impl Mint { Amount::ZERO, Amount::ZERO, single_use, + vec![], ); tracing::debug!( diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 8ed5d9d2f..381d159fe 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -101,6 +101,7 @@ impl Mint { let quote = MintQuote::new( self.mint_url.clone(), create_invoice_response.request.to_string(), + PaymentMethod::Bolt11, unit.clone(), Some(amount), create_invoice_response.expiry.unwrap_or(0), @@ -108,6 +109,7 @@ impl Mint { Amount::ZERO, Amount::ZERO, true, + vec![], ); tracing::debug!( @@ -204,37 +206,67 @@ impl Mint { wait_invoice_response: WaitInvoiceResponse, ) -> Result<(), Error> { let WaitInvoiceResponse { - payment_lookup_id, + request_lookup_id, payment_amount, unit, + payment_id, } = wait_invoice_response; if let Ok(Some(mint_quote)) = self .localstore - .get_mint_quote_by_request_lookup_id(&payment_lookup_id) + .get_mint_quote_by_request_lookup_id(&request_lookup_id) .await { tracing::debug!( - "Quote {} paid by lookup id {}", + "Quote {} with lookup id {} paid by {}", mint_quote.id, - payment_lookup_id + request_lookup_id, + payment_id ); + + if (mint_quote.single_use || mint_quote.payment_method == PaymentMethod::Bolt11) + && mint_quote.state == MintQuoteState::Issued + { + tracing::info!( + "Payment notification for quote {} already issued.", + mint_quote.id + ); + return Err(Error::IssuedQuote); + } + self.localstore .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) .await?; + let quote = self .localstore .get_mint_quote(&mint_quote.id) .await? .unwrap(); + assert!(unit == quote.unit); + + let mut payment_ids = quote.payment_ids; + + // We check if this payment has already been seen for this mint quote + // If it is we do not want to continue and add it to the paid balance of the quote + if payment_ids.contains(&payment_id) { + tracing::info!( + "Received update for payment {} already seen for quote {}", + payment_id, + mint_quote.id + ); + return Err(Error::PaymentAlreadySeen); + } + let amount_paid = quote.amount_paid + payment_amount; - assert!(unit == quote.unit); + payment_ids.push(payment_id); let quote = MintQuote { id: quote.id, mint_url: quote.mint_url, amount: quote.amount, + payment_method: quote.payment_method, unit: quote.unit, request: quote.request, state: MintQuoteState::Paid, @@ -243,6 +275,7 @@ impl Mint { amount_paid, amount_issued: quote.amount_issued, single_use: quote.single_use, + payment_ids, }; tracing::debug!( @@ -345,6 +378,7 @@ impl Mint { let mint_quote = MintQuote { id: quote.id, mint_url: quote.mint_url, + payment_method: quote.payment_method, amount: quote.amount, unit: quote.unit, request: quote.request, @@ -357,6 +391,7 @@ impl Mint { .map_err(|_| Error::AmountOverflow)?, request_lookup_id: quote.request_lookup_id, single_use: quote.single_use, + payment_ids: quote.payment_ids, }; self.localstore.add_mint_quote(mint_quote.clone()).await?; diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index 09ef6357c..8a8d6b8f9 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -5,7 +5,7 @@ use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::CurrencyUnit; +use super::{CurrencyUnit, PaymentMethod}; use crate::mint_url::MintUrl; use crate::nuts::{MeltQuoteState, MintQuoteState}; use crate::Amount; @@ -19,6 +19,9 @@ pub struct MintQuote { pub mint_url: MintUrl, /// Amount of quote pub amount: Option, + /// Payment Method + #[serde(default)] + pub payment_method: PaymentMethod, /// Unit of quote pub unit: CurrencyUnit, /// Quote payment request e.g. bolt11 @@ -38,6 +41,9 @@ pub struct MintQuote { /// Single use #[serde(default)] pub single_use: bool, + /// Payment of payment(s) that filled quote + #[serde(default)] + pub payment_ids: Vec, } impl MintQuote { @@ -46,6 +52,7 @@ impl MintQuote { pub fn new( mint_url: MintUrl, request: String, + payment_method: PaymentMethod, unit: CurrencyUnit, amount: Option, expiry: u64, @@ -53,6 +60,7 @@ impl MintQuote { amount_paid: Amount, amount_issued: Amount, single_use: bool, + payment_ids: Vec, ) -> Self { let id = Uuid::new_v4(); @@ -60,6 +68,7 @@ impl MintQuote { mint_url, id: id.to_string(), amount, + payment_method, unit, request, state: MintQuoteState::Unpaid, @@ -68,6 +77,7 @@ impl MintQuote { amount_paid, amount_issued, single_use, + payment_ids, } } } diff --git a/flake.lock b/flake.lock index 52a605216..afd3e2bc1 100644 --- a/flake.lock +++ b/flake.lock @@ -57,16 +57,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730741070, - "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", + "lastModified": 1730785428, + "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", + "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index dc67d02eb..2226b1629 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CDK Flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay = { url = "github:oxalica/rust-overlay"; From b056e8623721991bce5365e0bad34db11a65556c Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 17 Nov 2024 09:17:55 +0000 Subject: [PATCH 23/35] chore: fmt, clippy, typos --- .pre-commit-config.yaml | 1 - crates/cdk-cln/src/bolt12.rs | 3 +- crates/cdk-integration-tests/src/lib.rs | 2 +- crates/cdk-integration-tests/tests/mint.rs | 8 ++-- crates/cdk/src/amount.rs | 9 ++--- crates/cdk/src/mint/melt.rs | 37 +++++++---------- crates/cdk/src/mint/mint_18.rs | 11 +++--- crates/cdk/src/mint/mod.rs | 5 +-- crates/cdk/src/mint/types.rs | 6 ++- crates/cdk/src/nuts/nut06.rs | 2 +- crates/cdk/src/nuts/nut17/mod.rs | 3 +- crates/cdk/src/nuts/nut20.rs | 4 +- crates/cdk/src/wallet/mint_bolt12.rs | 15 ++++--- flake.lock | 46 +++++----------------- flake.nix | 4 +- 15 files changed, 56 insertions(+), 100 deletions(-) delete mode 120000 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 120000 index 41cc13f5f..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1 +0,0 @@ -/nix/store/9p08fy8g4sijw8k1l9spf8npjbjk34xf-pre-commit-config.json \ No newline at end of file diff --git a/crates/cdk-cln/src/bolt12.rs b/crates/cdk-cln/src/bolt12.rs index 65dfa7a8b..7e91aa387 100644 --- a/crates/cdk-cln/src/bolt12.rs +++ b/crates/cdk-cln/src/bolt12.rs @@ -21,8 +21,7 @@ use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::Offer; use uuid::Uuid; -use super::Cln; -use super::Error; +use super::{Cln, Error}; #[async_trait] impl MintBolt12Lightning for Cln { diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index ed4daead8..604dd3571 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -39,7 +39,7 @@ pub fn create_backends_fake_wallet( let ln_key = LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11); let wallet = Arc::new(FakeWallet::new( - fee_reserve.clone(), + fee_reserve, HashMap::default(), HashSet::default(), 0, diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index f807c20e0..fcb5eaa5d 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -14,8 +14,8 @@ use cdk::mint::MintQuote; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut17::Params; use cdk::nuts::{ - CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PreMintSecrets, - ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, + CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod, + PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, }; use cdk::types::QuoteTTL; use cdk::util::unix_time; @@ -76,6 +76,7 @@ async fn mint_proofs( let quote = MintQuote::new( mint.mint_url.clone(), "".to_string(), + PaymentMethod::Bolt11, CurrencyUnit::Sat, Some(amount), unix_time() + 36000, @@ -83,14 +84,13 @@ async fn mint_proofs( Amount::ZERO, Amount::ZERO, true, - true, vec![], ); mint.localstore.add_mint_quote(quote.clone()).await?; let wait_invoice = WaitInvoiceResponse { - request_lookup_id: request_lookup, + request_lookup_id: request_lookup.clone(), payment_amount: amount, unit: CurrencyUnit::Sat, payment_id: request_lookup, diff --git a/crates/cdk/src/amount.rs b/crates/cdk/src/amount.rs index 087d01784..b5aa8d902 100644 --- a/crates/cdk/src/amount.rs +++ b/crates/cdk/src/amount.rs @@ -6,9 +6,8 @@ use std::cmp::Ordering; use std::fmt; use std::str::FromStr; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use lightning::offers::offer::Offer; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use crate::nuts::CurrencyUnit; @@ -25,9 +24,9 @@ pub enum Error { /// Cannot convert units #[error("Cannot convert units")] CannotConvertUnits, - /// Amount undefinded + /// Amount undefined #[error("Amount undefined")] - AmountUndefinded, + AmountUndefined, /// Utf8 parse error #[error(transparent)] Utf8ParseError(#[from] std::string::FromUtf8Error), @@ -310,7 +309,7 @@ where /// Convert offer to amount in unit pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result { - let offer_amount = offer.amount().ok_or(Error::AmountUndefinded)?; + let offer_amount = offer.amount().ok_or(Error::AmountUndefined)?; let (amount, currency) = match offer_amount { lightning::offers::offer::Amount::Bitcoin { amount_msats } => { diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 4789d2537..ce5417cda 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -6,30 +6,21 @@ use anyhow::bail; use lightning::offers::offer::Offer; use tracing::instrument; -use crate::amount::amount_for_offer; -use crate::cdk_lightning; -use crate::cdk_lightning::MintLightning; -use crate::cdk_lightning::PayInvoiceResponse; -use crate::nuts::nut00::ProofsMethods; -use crate::nuts::nut11::enforce_sig_flag; -use crate::nuts::nut11::EnforceSigFlag; -use crate::{ - amount::to_unit, mint::SigFlag, nuts::Id, nuts::MeltQuoteState, types::LnKey, util::unix_time, - Amount, Error, -}; - use super::nut05::MeltRequestTrait; use super::types::PaymentRequest; -use super::BlindSignature; -use super::CurrencyUnit; -use super::MeltQuote; -use super::MeltQuoteBolt11Request; -use super::MeltQuoteBolt11Response; -use super::MeltQuoteBolt12Request; -use super::Mint; -use super::PaymentMethod; -use super::State; -use crate::nuts::PublicKey; +use super::{ + BlindSignature, CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, + MeltQuoteBolt12Request, Mint, PaymentMethod, State, +}; +use crate::amount::{amount_for_offer, to_unit}; +use crate::cdk_lightning::{MintLightning, PayInvoiceResponse}; +use crate::mint::SigFlag; +use crate::nuts::nut00::ProofsMethods; +use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag}; +use crate::nuts::{Id, MeltQuoteState, PublicKey}; +use crate::types::LnKey; +use crate::util::unix_time; +use crate::{cdk_lightning, Amount, Error}; impl Mint { fn check_melt_request_acceptable( @@ -460,7 +451,7 @@ impl Mint { if let Ok(Some(quote)) = self .localstore - .get_melt_quote(&melt_request.get_quote_id()) + .get_melt_quote(melt_request.get_quote_id()) .await { self.pubsub_manager diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index a76266490..827b3bd33 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -1,11 +1,10 @@ use tracing::instrument; -use crate::{types::LnKey, util::unix_time, Amount, Error}; - -use super::{ - nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}, - Mint, MintQuote, PaymentMethod, -}; +use super::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; +use super::{Mint, MintQuote, PaymentMethod}; +use crate::types::LnKey; +use crate::util::unix_time; +use crate::{Amount, Error}; impl Mint { /// Create new mint bolt11 quote diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index b18fc03b3..26f294b57 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -596,10 +596,7 @@ fn create_new_keyset( } fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { - let unit_index = match unit.derivation_index() { - Some(index) => index, - None => return None, - }; + let unit_index = unit.derivation_index()?; Some(DerivationPath::from(vec![ ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), diff --git a/crates/cdk/src/mint/types.rs b/crates/cdk/src/mint/types.rs index 8a8d6b8f9..c001598f1 100644 --- a/crates/cdk/src/mint/types.rs +++ b/crates/cdk/src/mint/types.rs @@ -150,10 +150,12 @@ pub enum PaymentRequest { } mod offer_serde { - use super::Offer; - use serde::{self, Deserialize, Deserializer, Serializer}; use std::str::FromStr; + use serde::{self, Deserialize, Deserializer, Serializer}; + + use super::Offer; + pub fn serialize(offer: &Offer, serializer: S) -> Result where S: Serializer, diff --git a/crates/cdk/src/nuts/nut06.rs b/crates/cdk/src/nuts/nut06.rs index c1772f01c..750cf7d60 100644 --- a/crates/cdk/src/nuts/nut06.rs +++ b/crates/cdk/src/nuts/nut06.rs @@ -237,7 +237,7 @@ pub struct Nuts { pub nut15: Option, /// NUT17 Settings #[cfg(feature = "mint")] - pub nut17: super::nut17::SupportedSettings, + pub nut17: Option, /// NUT04 Settings #[serde(rename = "18")] #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/cdk/src/nuts/nut17/mod.rs b/crates/cdk/src/nuts/nut17/mod.rs index 3ddd1f828..6f57457ad 100644 --- a/crates/cdk/src/nuts/nut17/mod.rs +++ b/crates/cdk/src/nuts/nut17/mod.rs @@ -118,10 +118,9 @@ impl Indexable for NotificationPayload { } } +/// Kind #[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] - -/// Kind pub enum Kind { /// Bolt 11 Melt Quote Bolt11MeltQuote, diff --git a/crates/cdk/src/nuts/nut20.rs b/crates/cdk/src/nuts/nut20.rs index dbb498084..30af3ac58 100644 --- a/crates/cdk/src/nuts/nut20.rs +++ b/crates/cdk/src/nuts/nut20.rs @@ -2,10 +2,10 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; +use super::nut05::MeltRequestTrait; +use super::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs}; use crate::Amount; -use super::{nut05::MeltRequestTrait, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs}; - /// NUT18 Error #[derive(Debug, Error)] pub enum Error { diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs index 1ca25f537..3111b74f5 100644 --- a/crates/cdk/src/wallet/mint_bolt12.rs +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -1,17 +1,16 @@ use tracing::instrument; use super::MintQuote; +use crate::amount::SplitTarget; +use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; -use crate::nuts::{MintBolt11Request, PaymentMethod}; -use crate::{ - amount::SplitTarget, - dhke::construct_proofs, - nuts::{nut12, PreMintSecrets, SpendingConditions, State}, - types::ProofInfo, - util::unix_time, - Amount, Error, Wallet, +use crate::nuts::{ + nut12, MintBolt11Request, PaymentMethod, PreMintSecrets, SpendingConditions, State, }; +use crate::types::ProofInfo; +use crate::util::unix_time; +use crate::{Amount, Error, Wallet}; impl Wallet { /// Mint Bolt12 diff --git a/flake.lock b/flake.lock index 601a91726..2ed178674 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -57,28 +57,16 @@ }, "nixpkgs": { "locked": { -<<<<<<< HEAD - "lastModified": 1730785428, - "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", + "lastModified": 1731652201, + "narHash": "sha256-XUO0JKP1hlww0d7mm3kpmIr4hhtR4zicg5Wwes9cPMg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", -======= - "lastModified": 1731386116, - "narHash": "sha256-lKA770aUmjPHdTaJWnP3yQ9OI1TigenUqVC3wweqZuI=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "689fed12a013f56d4c4d3f612489634267d86529", ->>>>>>> main + "rev": "c21b77913ea840f8bcf9adf4c41cecc2abffd38d", "type": "github" }, "original": { "owner": "NixOS", -<<<<<<< HEAD - "ref": "nixos-unstable", -======= "ref": "nixos-24.05", ->>>>>>> main "repo": "nixpkgs", "type": "github" } @@ -123,19 +111,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { -<<<<<<< HEAD - "lastModified": 1730814269, - "narHash": "sha256-fWPHyhYE6xvMI1eGY3pwBTq85wcy1YXqdzTZF+06nOg=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "d70155fdc00df4628446352fc58adc640cd705c2", -======= "lastModified": 1731363552, "narHash": "sha256-vFta1uHnD29VUY4HJOO/D6p6rxyObnf+InnSMT4jlMU=", "owner": "cachix", "repo": "pre-commit-hooks.nix", "rev": "cd1af27aa85026ac759d5d3fccf650abe7e1bbf0", ->>>>>>> main "type": "github" }, "original": { @@ -159,19 +139,11 @@ ] }, "locked": { -<<<<<<< HEAD - "lastModified": 1730773675, - "narHash": "sha256-pULo7GryzLkqGveWvnNWVz1Kk6EJqvq+HQeSkwvr7DA=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "e19e9d54fac1e53f73411ebe22d19f946b1ba0bd", -======= - "lastModified": 1731378398, - "narHash": "sha256-a0QWaiX8+AJ9/XBLGMDy6c90GD7HzpxKVdlFwCke5Pw=", + "lastModified": 1731820690, + "narHash": "sha256-/hHFMTD+FGURXZ4JtfXoIgpy87zL505pVi6AL76Wc+U=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0ae9fc2f2fe5361837d59c0bdebbda176427111e", ->>>>>>> main + "rev": "bbab2ab9e1932133b1996baa1dc00fefe924ca81", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0de9d285b..f1931d2b1 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CDK Flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -53,7 +53,7 @@ targets = [ "wasm32-unknown-unknown" ]; # wasm }; - # Nighly for creating lock files + # Nightly for creating lock files nightly_toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { extensions = [ "rustfmt" "clippy" "rust-analyzer" ]; }); From c4ac11dd4ad5c3bf4987da80425744ed3def3ff8 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 17 Nov 2024 10:40:38 +0000 Subject: [PATCH 24/35] feat: sql bolt12 --- .../20241117101725_bolt12_migration.sql | 6 ++++ crates/cdk-sqlite/src/mint/mod.rs | 35 ++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 crates/cdk-sqlite/src/mint/migrations/20241117101725_bolt12_migration.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20241117101725_bolt12_migration.sql b/crates/cdk-sqlite/src/mint/migrations/20241117101725_bolt12_migration.sql new file mode 100644 index 000000000..94e48f218 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20241117101725_bolt12_migration.sql @@ -0,0 +1,6 @@ +ALTER TABLE mint_quote ADD single_use INTEGER; +ALTER TABLE mint_quote ADD payment_method TEXT; +ALTER TABLE mint_quote ADD payment_ids TEXT; +ALTER TABLE mint_quote ADD amount_paid INTEGER; +ALTER TABLE mint_quote ADD amount_issued INTEGER; + diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index ba5d14dfe..bb2033a6d 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -206,8 +206,8 @@ WHERE active = 1 let res = sqlx::query( r#" INSERT OR REPLACE INTO mint_quote -(id, mint_url, amount, unit, request, state, expiry, request_lookup_id) -VALUES (?, ?, ?, ?, ?, ?, ?, ?); +(id, mint_url, amount, unit, request, state, expiry, request_lookup_id, single_use, payment_method, payment_ids, amount_paid, amount_issued) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); "#, ) .bind(quote.id.to_string()) @@ -219,6 +219,11 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?); .bind(quote.state.to_string()) .bind(quote.expiry as i64) .bind(quote.request_lookup_id) + .bind(quote.single_use) + .bind(quote.payment_method.to_string()) + .bind(serde_json::to_string("e.payment_ids)?) + .bind(u64::from(quote.amount_paid) as i64) + .bind(u64::from(quote.amount_issued) as i64 ) .execute(&mut transaction) .await; @@ -1279,6 +1284,11 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; let row_request_lookup_id: Option = row.try_get("request_lookup_id").map_err(Error::from)?; + let row_single_use: Option = row.try_get("single_use").map_err(Error::from)?; + let row_amount_paid: Option = row.try_get("amount_paid").map_err(Error::from)?; + let row_amount_issued: Option = row.try_get("amount_issued").map_err(Error::from)?; + let row_payment_method: Option = row.try_get("payment_method").map_err(Error::from)?; + let row_payment_ids: Option = row.try_get("payment_ids").map_err(Error::from)?; let request_lookup_id = match row_request_lookup_id { Some(id) => id, @@ -1288,6 +1298,16 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { }, }; + let payment_method = match row_payment_method { + Some(method) => PaymentMethod::from_str(&method)?, + None => PaymentMethod::Bolt11, + }; + + let payment_ids: Vec = match row_payment_ids { + Some(ids) => serde_json::from_str(&ids)?, + None => vec![], + }; + Ok(MintQuote { id: row_id, mint_url: MintUrl::from_str(&row_mint_url)?, @@ -1297,12 +1317,11 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { state: MintQuoteState::from_str(&row_state).map_err(Error::from)?, expiry: row_expiry as u64, request_lookup_id, - // TODO: Get these values - amount_paid: Amount::ZERO, - amount_issued: Amount::ZERO, - single_use: true, - payment_method: PaymentMethod::Bolt11, - payment_ids: Vec::new(), + amount_paid: (row_amount_paid.unwrap_or_default() as u64).into(), + amount_issued: (row_amount_issued.unwrap_or_default() as u64).into(), + single_use: row_single_use.unwrap_or(true), + payment_method, + payment_ids, }) } From a4789ea61ec711efadb02a495a57a1d8fb5b739b Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 17 Nov 2024 22:23:12 +0000 Subject: [PATCH 25/35] feat: wait any invoice for bolt12 --- crates/cdk-cln/src/bolt12.rs | 137 ++++++++++++++++++++++++- crates/cdk-cln/src/lib.rs | 33 +----- crates/cdk-phoenixd/src/bolt12.rs | 10 ++ crates/cdk/src/cdk_lightning/bolt12.rs | 6 ++ crates/cdk/src/mint/mint_nut04.rs | 1 + crates/cdk/src/mint/mod.rs | 58 +++++++++++ 6 files changed, 211 insertions(+), 34 deletions(-) diff --git a/crates/cdk-cln/src/bolt12.rs b/crates/cdk-cln/src/bolt12.rs index 7e91aa387..37f9c8dcc 100644 --- a/crates/cdk-cln/src/bolt12.rs +++ b/crates/cdk-cln/src/bolt12.rs @@ -1,5 +1,8 @@ use std::pin::Pin; use std::str::FromStr; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; use async_trait::async_trait; use cdk::amount::{amount_for_offer, to_unit, Amount}; @@ -12,26 +15,152 @@ use cdk::mint; use cdk::mint::types::PaymentRequest; use cdk::nuts::{CurrencyUnit, MeltQuoteBolt12Request, MeltQuoteState}; use cdk::util::{hex, unix_time}; -use cln_rpc::model::requests::{FetchinvoiceRequest, OfferRequest, PayRequest}; -use cln_rpc::model::responses::PayStatus; +use cln_rpc::model::requests::{ + FetchinvoiceRequest, OfferRequest, PayRequest, WaitanyinvoiceRequest, +}; +use cln_rpc::model::responses::{PayStatus, WaitanyinvoiceResponse, WaitanyinvoiceStatus}; use cln_rpc::model::Request; use cln_rpc::primitives::Amount as CLN_Amount; -use futures::Stream; +use futures::{Stream, StreamExt}; use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::Offer; use uuid::Uuid; use super::{Cln, Error}; +use crate::fetch_invoice_by_payment_hash; #[async_trait] impl MintBolt12Lightning for Cln { type Err = cdk_lightning::Error; + /// Is wait invoice active + fn is_wait_invoice_active(&self) -> bool { + self.wait_invoice_is_active.load(Ordering::SeqCst) + } + + /// Cancel wait invoice + fn cancel_wait_invoice(&self) { + self.wait_invoice_cancel_token.cancel() + } + /// Listen for bolt12 offers to be paid async fn wait_any_offer( &self, ) -> Result + Send>>, Self::Err> { - todo!() + let last_pay_index = self.get_last_pay_index().await?; + let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?; + + let stream = futures::stream::unfold( + ( + cln_client, + last_pay_index, + self.wait_invoice_cancel_token.clone(), + Arc::clone(&self.bolt12_wait_invoice_is_active), + ), + |(mut cln_client, mut last_pay_idx, cancel_token, is_active)| async move { + // Set the stream as active + is_active.store(true, Ordering::SeqCst); + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + // Set the stream as inactive + is_active.store(false, Ordering::SeqCst); + // End the stream + return None; + } + result = cln_client.call(cln_rpc::Request::WaitAnyInvoice(WaitanyinvoiceRequest { + timeout: None, + lastpay_index: last_pay_idx, + })) => { + match result { + Ok(invoice) => { + + // Try to convert the invoice to WaitanyinvoiceResponse + let wait_any_response_result: Result = + invoice.try_into(); + + let wait_any_response = match wait_any_response_result { + Ok(response) => response, + Err(e) => { + tracing::warn!( + "Failed to parse WaitAnyInvoice response: {:?}", + e + ); + // Continue to the next iteration without panicking + continue; + } + }; + + // Check the status of the invoice + // We only want to yield invoices that have been paid + match wait_any_response.status { + WaitanyinvoiceStatus::PAID => (), + WaitanyinvoiceStatus::EXPIRED => continue, + } + + last_pay_idx = wait_any_response.pay_index; + + let payment_hash = wait_any_response.payment_hash.to_string(); + + + // TODO: Handle unit conversion + let amount_msats = wait_any_response.amount_received_msat.expect("status is paid there should be an amount"); + let amount_sats = amount_msats.msat() / 1000; + + let request_lookup_id = match wait_any_response.bolt12 { + // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up. + // Since this is not returned in the wait any response, + // we need to do a second query for it. + Some(_) => { + match fetch_invoice_by_payment_hash( + &mut cln_client, + &payment_hash, + ) + .await + { + Ok(Some(invoice)) => { + if let Some(local_offer_id) = invoice.local_offer_id { + local_offer_id.to_string() + } else { + continue; + } + } + Ok(None) => continue, + Err(e) => { + tracing::warn!( + "Error fetching invoice by payment hash: {e}" + ); + continue; + } + } + } + None => payment_hash.clone(), + }; + + let response = WaitInvoiceResponse { + request_lookup_id, + payment_amount: amount_sats.into(), + unit: CurrencyUnit::Sat, + payment_id: payment_hash + }; + + break Some((response, (cln_client, last_pay_idx, cancel_token, is_active))); + } + Err(e) => { + tracing::warn!("Error fetching invoice: {e}"); + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + } + } + } + } + } + }, + ) + .boxed(); + + Ok(stream) } async fn get_bolt12_payment_quote( diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 708b01d38..e1d6061e9 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -49,6 +49,7 @@ pub struct Cln { bolt12_melt: bool, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, + bolt12_wait_invoice_is_active: Arc, } impl Cln { @@ -69,6 +70,7 @@ impl Cln { bolt12_melt, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), + bolt12_wait_invoice_is_active: Arc::new(AtomicBool::new(false)), }) } } @@ -163,38 +165,9 @@ impl MintLightning for Cln { let amount_msats = wait_any_response.amount_received_msat.expect("status is paid there should be an amount"); let amount_sats = amount_msats.msat() / 1000; - let request_lookup_id = match wait_any_response.bolt12 { - // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up. - // Since this is not returned in the wait any response, - // we need to do a second query for it. - Some(_) => { - match fetch_invoice_by_payment_hash( - &mut cln_client, - &payment_hash, - ) - .await - { - Ok(Some(invoice)) => { - if let Some(local_offer_id) = invoice.local_offer_id { - local_offer_id.to_string() - } else { - continue; - } - } - Ok(None) => continue, - Err(e) => { - tracing::warn!( - "Error fetching invoice by payment hash: {e}" - ); - continue; - } - } - } - None => payment_hash.clone(), - }; let response = WaitInvoiceResponse { - request_lookup_id, + request_lookup_id: payment_hash.clone(), payment_amount: amount_sats.into(), unit: CurrencyUnit::Sat, payment_id: payment_hash diff --git a/crates/cdk-phoenixd/src/bolt12.rs b/crates/cdk-phoenixd/src/bolt12.rs index 34cd0f815..93d54915d 100644 --- a/crates/cdk-phoenixd/src/bolt12.rs +++ b/crates/cdk-phoenixd/src/bolt12.rs @@ -23,6 +23,16 @@ use crate::Phoenixd; impl MintBolt12Lightning for Phoenixd { type Err = cdk_lightning::Error; + fn is_wait_invoice_active(&self) -> bool { + // Paying to PHD bolt12 offer is not supported so this can never be active + false + } + + fn cancel_wait_invoice(&self) { + // Paying to PHD bolt12 offer is not supported so there is nothing to cancel + () + } + async fn get_bolt12_payment_quote( &self, melt_quote_request: &MeltQuoteBolt12Request, diff --git a/crates/cdk/src/cdk_lightning/bolt12.rs b/crates/cdk/src/cdk_lightning/bolt12.rs index a855a245f..06e21defa 100644 --- a/crates/cdk/src/cdk_lightning/bolt12.rs +++ b/crates/cdk/src/cdk_lightning/bolt12.rs @@ -18,6 +18,12 @@ pub trait MintBolt12Lightning { /// Mint Lightning Error type Err: Into + From; + /// Is wait invoice active + fn is_wait_invoice_active(&self) -> bool; + + /// Cancel wait invoice + fn cancel_wait_invoice(&self); + /// Listen for bolt12 offers to be paid async fn wait_any_offer( &self, diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 3d5294558..551500cad 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -260,6 +260,7 @@ impl Mint { let amount_paid = quote.amount_paid + payment_amount; + // Since this is the first time we've seen this payment we add it to seen payment. payment_ids.push(payment_id); let quote = MintQuote { diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 26f294b57..a7bb5e577 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -264,6 +264,64 @@ impl Mint { Ok(()) } + /// Wait for any offer to be paid + /// For each backend starts a task that waits for any offers to be paid + /// Once invoice is paid mint quote status is updated + #[allow(clippy::incompatible_msrv)] + // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) + pub async fn wait_for_paid_offers(&self, shutdown: Arc) -> Result<(), Error> { + let mint_arc = Arc::new(self.clone()); + + let mut join_set = JoinSet::new(); + + for (key, bolt12) in self.bolt12_backends.iter() { + if !bolt12.is_wait_invoice_active() { + let mint = Arc::clone(&mint_arc); + let bolt12 = Arc::clone(bolt12); + let shutdown = Arc::clone(&shutdown); + let key = key.clone(); + join_set.spawn(async move { + if !bolt12.is_wait_invoice_active() { + loop { + tokio::select! { + _ = shutdown.notified() => { + tracing::info!("Shutdown signal received, stopping task for {:?}", key); + bolt12.cancel_wait_invoice(); + break; + } + result = bolt12.wait_any_offer() => { + match result { + Ok(mut stream) => { + while let Some(wait_invoice_response) = stream.next().await { + if let Err(err) = mint.pay_mint_quote_for_request_id(wait_invoice_response).await { + tracing::warn!("{:?}", err); + } + } + } + Err(err) => { + tracing::warn!("Could not get invoice stream for {:?}: {}",key, err); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + } + } + } + } + }); + } + } + + // Spawn a task to manage the JoinSet + while let Some(result) = join_set.join_next().await { + match result { + Ok(_) => tracing::info!("A task completed successfully."), + Err(err) => tracing::warn!("A task failed: {:?}", err), + } + } + + Ok(()) + } + /// Fee required for proof set #[instrument(skip_all)] pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result { From fa2fb580fe7c17f7fd86c1b6d45c1cf088a09a05 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 17 Nov 2024 23:46:25 +0000 Subject: [PATCH 26/35] feat: bolt12 mint builder --- crates/cdk-cln/src/bolt12.rs | 5 +- crates/cdk-cln/src/lib.rs | 15 +- crates/cdk-fake-wallet/src/lib.rs | 2 - .../src/init_fake_wallet.rs | 8 +- .../cdk-integration-tests/src/init_regtest.rs | 13 +- crates/cdk-integration-tests/src/lib.rs | 2 +- crates/cdk-lnbits/src/lib.rs | 2 - crates/cdk-lnd/src/lib.rs | 2 - crates/cdk-mintd/src/main.rs | 77 +++------ crates/cdk-mintd/src/setup.rs | 2 +- crates/cdk-phoenixd/src/bolt12.rs | 1 - crates/cdk-phoenixd/src/lib.rs | 2 - crates/cdk-strike/src/lib.rs | 2 - crates/cdk/src/cdk_lightning/mod.rs | 4 - crates/cdk/src/mint/builder.rs | 152 ++++++++++-------- crates/cdk/src/mint/melt.rs | 38 ++--- crates/cdk/src/mint/mint_18.rs | 12 +- crates/cdk/src/mint/mint_nut04.rs | 12 +- crates/cdk/src/mint/mod.rs | 14 +- crates/cdk/src/types.rs | 1 + 20 files changed, 148 insertions(+), 218 deletions(-) diff --git a/crates/cdk-cln/src/bolt12.rs b/crates/cdk-cln/src/bolt12.rs index 37f9c8dcc..8d5c15197 100644 --- a/crates/cdk-cln/src/bolt12.rs +++ b/crates/cdk-cln/src/bolt12.rs @@ -33,17 +33,16 @@ use crate::fetch_invoice_by_payment_hash; impl MintBolt12Lightning for Cln { type Err = cdk_lightning::Error; - /// Is wait invoice active fn is_wait_invoice_active(&self) -> bool { self.wait_invoice_is_active.load(Ordering::SeqCst) } - /// Cancel wait invoice fn cancel_wait_invoice(&self) { self.wait_invoice_cancel_token.cancel() } - /// Listen for bolt12 offers to be paid + // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) + #[allow(clippy::incompatible_msrv)] async fn wait_any_offer( &self, ) -> Result + Send>>, Self::Err> { diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index e1d6061e9..cb2683771 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -45,8 +45,6 @@ pub struct Cln { rpc_socket: PathBuf, cln_client: Arc>, fee_reserve: FeeReserve, - bolt12_mint: bool, - bolt12_melt: bool, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, bolt12_wait_invoice_is_active: Arc, @@ -54,20 +52,13 @@ pub struct Cln { impl Cln { /// Create new [`Cln`] - pub async fn new( - rpc_socket: PathBuf, - fee_reserve: FeeReserve, - bolt12_mint: bool, - bolt12_melt: bool, - ) -> Result { + pub async fn new(rpc_socket: PathBuf, fee_reserve: FeeReserve) -> Result { let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?; Ok(Self { rpc_socket, cln_client: Arc::new(Mutex::new(cln_client)), fee_reserve, - bolt12_mint, - bolt12_melt, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), bolt12_wait_invoice_is_active: Arc::new(AtomicBool::new(false)), @@ -83,8 +74,6 @@ impl MintLightning for Cln { Settings { mpp: true, unit: CurrencyUnit::Msat, - bolt12_mint: self.bolt12_mint, - bolt12_melt: self.bolt12_melt, invoice_description: true, } } @@ -99,8 +88,8 @@ impl MintLightning for Cln { self.wait_invoice_cancel_token.cancel() } - #[allow(clippy::incompatible_msrv)] // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) + #[allow(clippy::incompatible_msrv)] async fn wait_any_invoice( &self, ) -> Result + Send>>, Self::Err> { diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 4223d04f3..f226cc97d 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -104,8 +104,6 @@ impl MintLightning for FakeWallet { Settings { mpp: true, unit: CurrencyUnit::Msat, - bolt12_mint: false, - bolt12_melt: false, invoice_description: true, } } diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index 40ef9d916..e6c47e7c8 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -7,7 +7,6 @@ use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning::MintLightning; use cdk::mint::FeeReserve; use cdk::nuts::CurrencyUnit; -use cdk::types::LnKey; use cdk_fake_wallet::FakeWallet; use tokio::sync::Notify; use tower_http::cors::CorsLayer; @@ -33,7 +32,7 @@ where tracing_subscriber::fmt().with_env_filter(env_filter).init(); let mut ln_backends: HashMap< - LnKey, + CurrencyUnit, Arc + Sync + Send>, > = HashMap::new(); @@ -44,10 +43,7 @@ where let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0); - ln_backends.insert( - LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11), - Arc::new(fake_wallet), - ); + ln_backends.insert(CurrencyUnit::Sat, Arc::new(fake_wallet)); let mint = create_mint(database, ln_backends.clone()).await?; let cache_ttl = 3600; diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index df8db2249..9550b4404 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -10,7 +10,7 @@ use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning::MintLightning; use cdk::mint::{FeeReserve, Mint}; use cdk::nuts::{CurrencyUnit, MintInfo}; -use cdk::types::{LnKey, QuoteTTL}; +use cdk::types::QuoteTTL; use cdk_cln::Cln as CdkCln; use ln_regtest_rs::bitcoin_client::BitcoinClient; use ln_regtest_rs::bitcoind::Bitcoind; @@ -138,13 +138,13 @@ pub async fn create_cln_backend(cln_client: &ClnClient) -> Result { percent_fee_reserve: 1.0, }; - Ok(CdkCln::new(rpc_path, fee_reserve, true, true).await?) + Ok(CdkCln::new(rpc_path, fee_reserve).await?) } pub async fn create_mint( database: D, ln_backends: HashMap< - LnKey, + CurrencyUnit, Arc + Sync + Send>, >, ) -> Result @@ -207,14 +207,11 @@ where let cln_backend = create_cln_backend(&cln_client).await?; let mut ln_backends: HashMap< - LnKey, + CurrencyUnit, Arc + Sync + Send>, > = HashMap::new(); - ln_backends.insert( - LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11), - Arc::new(cln_backend), - ); + ln_backends.insert(CurrencyUnit::Sat, Arc::new(cln_backend)); let mint = create_mint(database, ln_backends.clone()).await?; let cache_time_to_live = 3600; diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 604dd3571..9b2882f52 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -52,7 +52,7 @@ pub fn create_backends_fake_wallet( pub async fn start_mint( ln_backends: HashMap< - LnKey, + CurrencyUnit, Arc + Sync + Send>, >, supported_units: HashMap, diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index f0af24c97..e1ea7d79f 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -73,8 +73,6 @@ impl MintLightning for LNbits { Settings { mpp: false, unit: CurrencyUnit::Sat, - bolt12_mint: false, - bolt12_melt: false, invoice_description: true, } } diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 129c7a4f1..e76df7ec0 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -81,8 +81,6 @@ impl MintLightning for Lnd { Settings { mpp: true, unit: CurrencyUnit::Msat, - bolt12_mint: false, - bolt12_melt: false, invoice_description: true, } } diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 673ca8593..ae827ed1b 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -144,12 +144,8 @@ async fn main() -> anyhow::Result<()> { }; ln_backends.insert(ln_key, cln.clone()); - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - cln.clone(), - ); + mint_builder = + mint_builder.add_ln_backend(CurrencyUnit::Sat, mint_melt_limits, cln.clone()); if cln_settings.bolt12 { let ln_key = LnKey { @@ -158,12 +154,8 @@ async fn main() -> anyhow::Result<()> { }; ln_backends.insert(ln_key, cln.clone()); - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt12, - mint_melt_limits, - cln, - ) + mint_builder = + mint_builder.add_bolt12_ln_backend(CurrencyUnit::Sat, mint_melt_limits, cln) } } LnBackend::Strike => { @@ -178,12 +170,8 @@ async fn main() -> anyhow::Result<()> { .setup(&mut ln_routers, &settings, unit.clone()) .await?; - mint_builder = mint_builder.add_ln_backend( - unit, - PaymentMethod::Bolt11, - mint_melt_limits, - Arc::new(strike), - ); + mint_builder = + mint_builder.add_ln_backend(unit, mint_melt_limits, Arc::new(strike)); } } LnBackend::LNbits => { @@ -192,12 +180,8 @@ async fn main() -> anyhow::Result<()> { .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) .await?; - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - Arc::new(lnbits), - ); + mint_builder = + mint_builder.add_ln_backend(CurrencyUnit::Sat, mint_melt_limits, Arc::new(lnbits)); } LnBackend::Phoenixd => { let phd_settings = settings.clone().phoenixd.expect("Checked at config load"); @@ -205,12 +189,8 @@ async fn main() -> anyhow::Result<()> { .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) .await?; - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - Arc::new(phd), - ); + mint_builder = + mint_builder.add_ln_backend(CurrencyUnit::Sat, mint_melt_limits, Arc::new(phd)); } LnBackend::Lnd => { let lnd_settings = settings.clone().lnd.expect("Checked at config load"); @@ -218,12 +198,8 @@ async fn main() -> anyhow::Result<()> { .setup(&mut ln_routers, &settings, CurrencyUnit::Msat) .await?; - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - Arc::new(lnd), - ); + mint_builder = + mint_builder.add_ln_backend(CurrencyUnit::Sat, mint_melt_limits, Arc::new(lnd)); } LnBackend::FakeWallet => { let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); @@ -235,33 +211,16 @@ async fn main() -> anyhow::Result<()> { let fake = Arc::new(fake); - mint_builder = mint_builder.add_ln_backend( - unit.clone(), - PaymentMethod::Bolt11, - mint_melt_limits, - fake.clone(), - ); + mint_builder = + mint_builder.add_ln_backend(unit.clone(), mint_melt_limits, fake.clone()); - mint_builder = mint_builder.add_ln_backend( - unit, - PaymentMethod::Bolt12, - mint_melt_limits, - fake.clone(), - ); + // TODO: Bolt12 for fake + // mint_builder = + // mint_builder.add_bolt12_ln_backend(unit, mint_melt_limits, fake.clone()); } } }; - let support_bolt12_mint = ln_backends.iter().any(|(_k, ln)| { - let settings = ln.get_settings(); - settings.bolt12_mint - }); - - let support_bolt12_melt = ln_backends.iter().any(|(_k, ln)| { - let settings = ln.get_settings(); - settings.bolt12_melt - }); - if let Some(long_description) = &settings.mint_info.description_long { mint_builder = mint_builder.with_long_description(long_description.to_string()); } @@ -326,7 +285,7 @@ async fn main() -> anyhow::Result<()> { .seconds_to_extend_cache_by .unwrap_or(DEFAULT_CACHE_TTI_SECS); - let include_bolt12 = support_bolt12_mint || support_bolt12_melt; + let include_bolt12 = !mint.bolt12_backends.is_empty(); let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti, include_bolt12) diff --git a/crates/cdk-mintd/src/setup.rs b/crates/cdk-mintd/src/setup.rs index cfcc0294b..9a2cc820a 100644 --- a/crates/cdk-mintd/src/setup.rs +++ b/crates/cdk-mintd/src/setup.rs @@ -44,7 +44,7 @@ impl LnBackendSetup for config::Cln { percent_fee_reserve: self.fee_percent, }; - let cln = cdk_cln::Cln::new(cln_socket, fee_reserve, true, true).await?; + let cln = cdk_cln::Cln::new(cln_socket, fee_reserve).await?; Ok(cln) } diff --git a/crates/cdk-phoenixd/src/bolt12.rs b/crates/cdk-phoenixd/src/bolt12.rs index 93d54915d..6dcad8f58 100644 --- a/crates/cdk-phoenixd/src/bolt12.rs +++ b/crates/cdk-phoenixd/src/bolt12.rs @@ -30,7 +30,6 @@ impl MintBolt12Lightning for Phoenixd { fn cancel_wait_invoice(&self) { // Paying to PHD bolt12 offer is not supported so there is nothing to cancel - () } async fn get_bolt12_payment_quote( diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index 1615a050f..879f48c0e 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -81,8 +81,6 @@ impl MintLightning for Phoenixd { Settings { mpp: false, unit: CurrencyUnit::Sat, - bolt12_mint: false, - bolt12_melt: true, invoice_description: true, } } diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index fff4998df..a5ba28c91 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -70,8 +70,6 @@ impl MintLightning for Strike { Settings { mpp: false, unit: self.unit.clone(), - bolt12_mint: false, - bolt12_melt: false, invoice_description: true, } } diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 349459efc..0b64e8365 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -195,10 +195,6 @@ pub struct Bolt12PaymentQuoteResponse { pub struct Settings { /// MPP supported pub mpp: bool, - /// Supports bolt12 mint - pub bolt12_mint: bool, - /// Supports bolt12 melt - pub bolt12_melt: bool, /// Base unit of backend pub unit: CurrencyUnit, /// Invoice Description supported diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 14d70df13..ac00b4d13 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -7,13 +7,14 @@ use anyhow::anyhow; use crate::amount::Amount; use crate::cdk_database::{self, MintDatabase}; +use crate::cdk_lightning::bolt12::MintBolt12Lightning; use crate::cdk_lightning::{self, MintLightning}; use crate::mint::Mint; use crate::nuts::{ ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, MppMethodSettings, PaymentMethod, }; -use crate::types::{LnKey, QuoteTTL}; +use crate::types::QuoteTTL; /// Cashu Mint #[derive(Default)] @@ -24,8 +25,17 @@ pub struct MintBuilder { mint_info: MintInfo, /// Mint Storage backend localstore: Option + Send + Sync>>, - /// Ln backends for mint - ln: Option + Send + Sync>>>, + /// Bolt11 ln backends for mint + ln: Option< + HashMap + Send + Sync>>, + >, + /// Bolt12 backends for mint + bolt12_backends: Option< + HashMap< + CurrencyUnit, + Arc + Send + Sync>, + >, + >, seed: Option>, quote_ttl: Option, supported_units: HashMap, @@ -106,19 +116,15 @@ impl MintBuilder { pub fn add_ln_backend( mut self, unit: CurrencyUnit, - method: PaymentMethod, limits: MintMeltLimits, ln_backend: Arc + Send + Sync>, ) -> Self { - let ln_key = LnKey { - unit: unit.clone(), - method, - }; - let mut ln = self.ln.unwrap_or_default(); let settings = ln_backend.get_settings(); + let method = PaymentMethod::Bolt11; + if settings.mpp { let mpp_settings = MppMethodSettings { method, @@ -132,64 +138,32 @@ impl MintBuilder { self.mint_info.nuts.nut15 = Some(mpp); } - match method { - PaymentMethod::Bolt11 => { - let mint_method_settings = MintMethodSettings { - method, - unit: unit.clone(), - min_amount: Some(limits.mint_min), - max_amount: Some(limits.mint_max), - description: settings.invoice_description, - }; - - self.mint_info.nuts.nut04.methods.push(mint_method_settings); - self.mint_info.nuts.nut04.disabled = false; - - let melt_method_settings = MeltMethodSettings { - method, - unit, - min_amount: Some(limits.melt_min), - max_amount: Some(limits.melt_max), - }; - self.mint_info.nuts.nut05.methods.push(melt_method_settings); - self.mint_info.nuts.nut05.disabled = false; - } - PaymentMethod::Bolt12 => { - let mint_method_settings = MintMethodSettings { - method, - unit: unit.clone(), - min_amount: Some(limits.mint_min), - max_amount: Some(limits.mint_max), - description: settings.invoice_description, - }; - - let mut nut18_settings = self.mint_info.nuts.nut18.unwrap_or_default(); - - nut18_settings.methods.push(mint_method_settings); - nut18_settings.disabled = false; - - self.mint_info.nuts.nut18 = Some(nut18_settings); - - let melt_method_settings = MeltMethodSettings { - method, - unit: unit.clone(), - min_amount: Some(limits.melt_min), - max_amount: Some(limits.melt_max), - }; - - let mut nut19_settings = self.mint_info.nuts.nut19.unwrap_or_default(); - nut19_settings.methods.push(melt_method_settings); - nut19_settings.disabled = false; - - self.mint_info.nuts.nut19 = Some(nut19_settings); - } - } + let mint_method_settings = MintMethodSettings { + method, + unit: unit.clone(), + min_amount: Some(limits.mint_min), + max_amount: Some(limits.mint_max), + description: settings.invoice_description, + }; + + self.mint_info.nuts.nut04.methods.push(mint_method_settings); + self.mint_info.nuts.nut04.disabled = false; + + let melt_method_settings = MeltMethodSettings { + method, + unit: unit.clone(), + min_amount: Some(limits.melt_min), + max_amount: Some(limits.melt_max), + }; + self.mint_info.nuts.nut05.methods.push(melt_method_settings); + self.mint_info.nuts.nut05.disabled = false; - ln.insert(ln_key.clone(), ln_backend); + ln.insert(unit.clone(), ln_backend); let mut supported_units = self.supported_units.clone(); - supported_units.insert(ln_key.unit, (0, 32)); + // TODO: The max order and fee should be configutable + supported_units.insert(unit, (0, 32)); self.supported_units = supported_units; self.ln = Some(ln); @@ -197,6 +171,58 @@ impl MintBuilder { self } + /// Add ln backend + pub fn add_bolt12_ln_backend( + mut self, + unit: CurrencyUnit, + limits: MintMeltLimits, + ln_backend: Arc + Send + Sync>, + ) -> Self { + let mut ln = self.bolt12_backends.unwrap_or_default(); + + let method = PaymentMethod::Bolt12; + + let mint_method_settings = MintMethodSettings { + method, + unit: unit.clone(), + min_amount: Some(limits.mint_min), + max_amount: Some(limits.mint_max), + description: true, + }; + + let mut nut18_settings = self.mint_info.nuts.nut18.unwrap_or_default(); + + nut18_settings.methods.push(mint_method_settings); + nut18_settings.disabled = false; + + self.mint_info.nuts.nut18 = Some(nut18_settings); + + let melt_method_settings = MeltMethodSettings { + method, + unit: unit.clone(), + min_amount: Some(limits.melt_min), + max_amount: Some(limits.melt_max), + }; + + let mut nut19_settings = self.mint_info.nuts.nut19.unwrap_or_default(); + nut19_settings.methods.push(melt_method_settings); + nut19_settings.disabled = false; + + self.mint_info.nuts.nut19 = Some(nut19_settings); + + ln.insert(unit.clone(), ln_backend); + + let mut supported_units = self.supported_units.clone(); + + // TODO: The max order and fee should be configutable + supported_units.insert(unit, (0, 32)); + self.supported_units = supported_units; + + self.bolt12_backends = Some(ln); + + self + } + /// Set quote ttl pub fn with_quote_ttl(mut self, mint_ttl: u64, melt_ttl: u64) -> Self { let quote_ttl = QuoteTTL { mint_ttl, melt_ttl }; diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index ce5417cda..54b213bf7 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -18,7 +18,6 @@ use crate::mint::SigFlag; use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag}; use crate::nuts::{Id, MeltQuoteState, PublicKey}; -use crate::types::LnKey; use crate::util::unix_time; use crate::{cdk_lightning, Amount, Error}; @@ -77,14 +76,11 @@ impl Mint { self.check_melt_request_acceptable(amount, unit.clone(), PaymentMethod::Bolt11)?; - let ln = self - .ln - .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) - .ok_or_else(|| { - tracing::info!("Could not get ln backend for {}, bolt11 ", unit); + let ln = self.ln.get(unit).ok_or_else(|| { + tracing::info!("Could not get ln backend for {}, bolt11 ", unit); - Error::UnitUnsupported - })?; + Error::UnitUnsupported + })?; let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| { tracing::error!( @@ -143,14 +139,11 @@ impl Mint { self.check_melt_request_acceptable(amount, unit.clone(), PaymentMethod::Bolt12)?; - let ln = self - .bolt12_backends - .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt12)) - .ok_or_else(|| { - tracing::info!("Could not get ln backend for {}, bolt11 ", unit); + let ln = self.bolt12_backends.get(unit).ok_or_else(|| { + tracing::info!("Could not get ln backend for {}, bolt11 ", unit); - Error::UnitUnsupported - })?; + Error::UnitUnsupported + })?; let payment_quote = ln .get_bolt12_payment_quote(melt_request) @@ -552,10 +545,7 @@ impl Mint { } _ => None, }; - let ln = match self - .ln - .get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11)) - { + let ln = match self.ln.get("e.unit) { Some(ln) => ln, None => { tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit); @@ -569,10 +559,7 @@ impl Mint { let attempt_to_pay = match melt_request.get_payment_method() { PaymentMethod::Bolt11 => { - let ln = match self - .ln - .get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11)) - { + let ln = match self.ln.get("e.unit) { Some(ln) => ln, None => { tracing::info!( @@ -590,10 +577,7 @@ impl Mint { .await } PaymentMethod::Bolt12 => { - let ln = match self - .bolt12_backends - .get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt12)) - { + let ln = match self.bolt12_backends.get("e.unit) { Some(ln) => ln, None => { tracing::info!( diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index 827b3bd33..447a6da38 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -2,7 +2,6 @@ use tracing::instrument; use super::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use super::{Mint, MintQuote, PaymentMethod}; -use crate::types::LnKey; use crate::util::unix_time; use crate::{Amount, Error}; @@ -32,14 +31,11 @@ impl Mint { return Err(Error::MintingDisabled); } - let ln = self - .bolt12_backends - .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt12)) - .ok_or_else(|| { - tracing::info!("Bolt11 mint request for unsupported unit"); + let ln = self.bolt12_backends.get(&unit).ok_or_else(|| { + tracing::info!("Bolt11 mint request for unsupported unit"); - Error::UnitUnsupported - })?; + Error::UnitUnsupported + })?; let quote_expiry = match expiry { Some(expiry) => expiry, diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index 551500cad..682961409 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -6,7 +6,6 @@ use super::{ }; use crate::cdk_lightning::WaitInvoiceResponse; use crate::nuts::MintQuoteState; -use crate::types::LnKey; use crate::util::unix_time; use crate::{Amount, Error}; @@ -69,14 +68,11 @@ impl Mint { self.check_mint_request_acceptable(amount, &unit)?; - let ln = self - .ln - .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) - .ok_or_else(|| { - tracing::info!("Bolt11 mint request for unsupported unit"); + let ln = self.ln.get(&unit).ok_or_else(|| { + tracing::info!("Bolt11 mint request for unsupported unit"); - Error::UnitUnsupported - })?; + Error::UnitUnsupported + })?; let quote_expiry = unix_time() + self.quote_ttl.mint_ttl; diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index a7bb5e577..58c5a206d 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -21,7 +21,7 @@ use crate::error::Error; use crate::fees::calculate_fee; use crate::mint_url::MintUrl; use crate::nuts::*; -use crate::types::{LnKey, QuoteTTL}; +use crate::types::QuoteTTL; use crate::util::unix_time; use crate::Amount; @@ -50,10 +50,12 @@ pub struct Mint { /// Mint Storage backend pub localstore: Arc + Send + Sync>, /// Ln backends for mint - pub ln: HashMap + Send + Sync>>, + pub ln: HashMap + Send + Sync>>, /// Ln backends for mint - pub bolt12_backends: - HashMap + Send + Sync>>, + pub bolt12_backends: HashMap< + CurrencyUnit, + Arc + Send + Sync>, + >, /// Subscription manager pub pubsub_manager: Arc, /// Active Mint Keysets @@ -71,9 +73,9 @@ impl Mint { mint_info: MintInfo, quote_ttl: QuoteTTL, localstore: Arc + Send + Sync>, - ln: HashMap + Send + Sync>>, + ln: HashMap + Send + Sync>>, bolt12: HashMap< - LnKey, + CurrencyUnit, Arc + Send + Sync>, >, // Hashmap where the key is the unit and value is (input fee ppk, max_order) diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index 23ab9e897..4869b2350 100644 --- a/crates/cdk/src/types.rs +++ b/crates/cdk/src/types.rs @@ -141,6 +141,7 @@ impl ProofInfo { /// Key used in hashmap of ln backends to identify what unit and payment method /// it is for +// TODO: Check if this is actually used anywhere #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct LnKey { /// Unit of Payment backend From 98e3158b85e0c805b61ff83c3110c54ac4a95e3e Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 18 Nov 2024 08:40:29 +0000 Subject: [PATCH 27/35] feat: add settings to bolt12 --- crates/cdk-cln/src/bolt12.rs | 11 +++++- crates/cdk-mintd/src/main.rs | 2 +- crates/cdk-phoenixd/src/bolt12.rs | 11 +++++- crates/cdk/src/cdk_lightning/bolt12.rs | 17 +++++++++ crates/cdk/src/mint/builder.rs | 50 +++++++++++++++----------- 5 files changed, 67 insertions(+), 24 deletions(-) diff --git a/crates/cdk-cln/src/bolt12.rs b/crates/cdk-cln/src/bolt12.rs index 8d5c15197..10ac37fdd 100644 --- a/crates/cdk-cln/src/bolt12.rs +++ b/crates/cdk-cln/src/bolt12.rs @@ -6,7 +6,7 @@ use std::time::Duration; use async_trait::async_trait; use cdk::amount::{amount_for_offer, to_unit, Amount}; -use cdk::cdk_lightning::bolt12::MintBolt12Lightning; +use cdk::cdk_lightning::bolt12::{Bolt12Settings, MintBolt12Lightning}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateOfferResponse, MintLightning, PayInvoiceResponse, WaitInvoiceResponse, @@ -33,6 +33,15 @@ use crate::fetch_invoice_by_payment_hash; impl MintBolt12Lightning for Cln { type Err = cdk_lightning::Error; + fn get_settings(&self) -> Bolt12Settings { + Bolt12Settings { + mint: true, + melt: true, + unit: CurrencyUnit::Msat, + offer_description: true, + } + } + fn is_wait_invoice_active(&self) -> bool { self.wait_invoice_is_active.load(Ordering::SeqCst) } diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index ae827ed1b..d658a92e5 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -285,8 +285,8 @@ async fn main() -> anyhow::Result<()> { .seconds_to_extend_cache_by .unwrap_or(DEFAULT_CACHE_TTI_SECS); + // If there are any backend that support bolt12 we need to add the bolt12 router let include_bolt12 = !mint.bolt12_backends.is_empty(); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti, include_bolt12) .await?; diff --git a/crates/cdk-phoenixd/src/bolt12.rs b/crates/cdk-phoenixd/src/bolt12.rs index 6dcad8f58..1777f3971 100644 --- a/crates/cdk-phoenixd/src/bolt12.rs +++ b/crates/cdk-phoenixd/src/bolt12.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use anyhow::anyhow; use async_trait::async_trait; use cdk::amount::{amount_for_offer, Amount}; -use cdk::cdk_lightning::bolt12::MintBolt12Lightning; +use cdk::cdk_lightning::bolt12::{Bolt12Settings, MintBolt12Lightning}; use cdk::cdk_lightning::{ self, Bolt12PaymentQuoteResponse, CreateOfferResponse, MintLightning, PayInvoiceResponse, WaitInvoiceResponse, @@ -23,6 +23,15 @@ use crate::Phoenixd; impl MintBolt12Lightning for Phoenixd { type Err = cdk_lightning::Error; + fn get_settings(&self) -> Bolt12Settings { + Bolt12Settings { + mint: true, + melt: false, + unit: CurrencyUnit::Sat, + offer_description: false, + } + } + fn is_wait_invoice_active(&self) -> bool { // Paying to PHD bolt12 offer is not supported so this can never be active false diff --git a/crates/cdk/src/cdk_lightning/bolt12.rs b/crates/cdk/src/cdk_lightning/bolt12.rs index 06e21defa..0a760da57 100644 --- a/crates/cdk/src/cdk_lightning/bolt12.rs +++ b/crates/cdk/src/cdk_lightning/bolt12.rs @@ -4,6 +4,7 @@ use std::pin::Pin; use async_trait::async_trait; use futures::Stream; +use serde::{Deserialize, Serialize}; use super::{ Bolt12PaymentQuoteResponse, CreateOfferResponse, Error, PayInvoiceResponse, WaitInvoiceResponse, @@ -18,6 +19,9 @@ pub trait MintBolt12Lightning { /// Mint Lightning Error type Err: Into + From; + /// Backend Settings + fn get_settings(&self) -> Bolt12Settings; + /// Is wait invoice active fn is_wait_invoice_active(&self) -> bool; @@ -53,3 +57,16 @@ pub trait MintBolt12Lightning { single_use: bool, ) -> Result; } + +/// Ln backend settings +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bolt12Settings { + /// Mint supported + pub mint: bool, + /// Melt supported + pub melt: bool, + /// Base unit of backend + pub unit: CurrencyUnit, + /// Invoice Description supported + pub offer_description: bool, +} diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index ac00b4d13..16f99b14c 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -182,33 +182,41 @@ impl MintBuilder { let method = PaymentMethod::Bolt12; - let mint_method_settings = MintMethodSettings { - method, - unit: unit.clone(), - min_amount: Some(limits.mint_min), - max_amount: Some(limits.mint_max), - description: true, - }; + let settings = ln_backend.get_settings(); - let mut nut18_settings = self.mint_info.nuts.nut18.unwrap_or_default(); + // If the backend supports minting we add it to info signalling + if settings.mint { + let mint_method_settings = MintMethodSettings { + method, + unit: unit.clone(), + min_amount: Some(limits.mint_min), + max_amount: Some(limits.mint_max), + description: true, + }; - nut18_settings.methods.push(mint_method_settings); - nut18_settings.disabled = false; + let mut nut18_settings = self.mint_info.nuts.nut18.unwrap_or_default(); - self.mint_info.nuts.nut18 = Some(nut18_settings); + nut18_settings.methods.push(mint_method_settings); + nut18_settings.disabled = false; - let melt_method_settings = MeltMethodSettings { - method, - unit: unit.clone(), - min_amount: Some(limits.melt_min), - max_amount: Some(limits.melt_max), - }; + self.mint_info.nuts.nut18 = Some(nut18_settings); + } - let mut nut19_settings = self.mint_info.nuts.nut19.unwrap_or_default(); - nut19_settings.methods.push(melt_method_settings); - nut19_settings.disabled = false; + // If the backend supports melting we add it to info signalling + if settings.melt { + let melt_method_settings = MeltMethodSettings { + method, + unit: unit.clone(), + min_amount: Some(limits.melt_min), + max_amount: Some(limits.melt_max), + }; - self.mint_info.nuts.nut19 = Some(nut19_settings); + let mut nut19_settings = self.mint_info.nuts.nut19.unwrap_or_default(); + nut19_settings.methods.push(melt_method_settings); + nut19_settings.disabled = false; + + self.mint_info.nuts.nut19 = Some(nut19_settings); + } ln.insert(unit.clone(), ln_backend); From 0afa42c958a4720572baa3bd4bfa8e3a12b81f4e Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 18 Nov 2024 08:59:44 +0000 Subject: [PATCH 28/35] feat: mint check into cdk --- crates/cdk-mintd/src/main.rs | 40 +------------------------ crates/cdk/src/mint/mod.rs | 1 + crates/cdk/src/mint/start_up_check.rs | 43 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 crates/cdk/src/mint/start_up_check.rs diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index d658a92e5..3e82b6c23 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -261,9 +261,7 @@ async fn main() -> anyhow::Result<()> { // 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 - for ln in ln_backends.values() { - check_pending_mint_quotes(Arc::clone(&mint), Arc::clone(ln)).await?; - } + mint.check_pending_mint_quotes().await?; // Checks the status of all pending melt quotes // Pending melt quotes where the payment has gone through inputs are burnt @@ -341,42 +339,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -/// Used on mint start up to check status of all pending mint quotes -async fn check_pending_mint_quotes( - mint: Arc, - ln: Arc + Send + Sync>, -) -> Result<()> { - let mut pending_quotes = mint.get_pending_mint_quotes().await?; - tracing::info!("There are {} pending mint quotes.", pending_quotes.len()); - let mut unpaid_quotes = mint.get_unpaid_mint_quotes().await?; - tracing::info!("There are {} unpaid mint quotes.", unpaid_quotes.len()); - - unpaid_quotes.append(&mut pending_quotes); - - for quote in unpaid_quotes { - tracing::debug!("Checking status of mint quote: {}", quote.id); - let lookup_id = quote.request_lookup_id.as_str(); - match ln.check_incoming_invoice_status(lookup_id).await { - Ok(state) => { - if state != quote.state { - tracing::trace!("Mint quote status changed: {}", quote.id); - mint.localstore - .update_mint_quote_state("e.id, state) - .await?; - mint.pubsub_manager.mint_quote_bolt11_status(quote, state); - } - } - - Err(err) => { - tracing::warn!("Could not check state of pending invoice: {}", lookup_id); - tracing::error!("{}", err); - } - } - } - - Ok(()) -} - async fn check_pending_melt_quotes( mint: Arc, ln_backends: &HashMap + Send + Sync>>, diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 58c5a206d..8d47378f0 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -32,6 +32,7 @@ mod keysets; mod melt; mod mint_18; mod mint_nut04; +mod start_up_check; mod swap; pub mod types; diff --git a/crates/cdk/src/mint/start_up_check.rs b/crates/cdk/src/mint/start_up_check.rs new file mode 100644 index 000000000..7d3d920ba --- /dev/null +++ b/crates/cdk/src/mint/start_up_check.rs @@ -0,0 +1,43 @@ +//! Check used at mint start up +//! +//! These checks are need in the case the mint was offline and the lightning node was node. +//! These ensure that the status of the mint or melt quote matches in the mint db and on the node. + +use super::{Error, Mint}; + +impl Mint { + /// Check the status of all pending mint quotes in the mint db + /// with all the lighting backends. This check that any payments + /// received while the mint was offline are accounted for, and the wallet can mint associated ecash + pub async fn check_pending_mint_quotes(&self) -> Result<(), Error> { + let mut pending_quotes = self.get_pending_mint_quotes().await?; + tracing::info!("There are {} pending mint quotes.", pending_quotes.len()); + let mut unpaid_quotes = self.get_unpaid_mint_quotes().await?; + tracing::info!("There are {} unpaid mint quotes.", unpaid_quotes.len()); + + unpaid_quotes.append(&mut pending_quotes); + + for ln in self.ln.values() { + for quote in unpaid_quotes.iter() { + tracing::debug!("Checking status of mint quote: {}", quote.id); + let lookup_id = quote.request_lookup_id.as_str(); + match ln.check_incoming_invoice_status(lookup_id).await { + Ok(state) => { + if state != quote.state { + tracing::trace!("Mint quote status changed: {}", quote.id); + self.localstore + .update_mint_quote_state("e.id, state) + .await?; + } + } + + Err(err) => { + tracing::warn!("Could not check state of pending invoice: {}", lookup_id); + tracing::error!("{}", err); + } + } + } + } + Ok(()) + } +} From 524f2c3972f42b22fe1967474246c33986662ed5 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 18 Nov 2024 09:08:55 +0000 Subject: [PATCH 29/35] feat: melt startup check into cdk --- crates/cdk-mintd/src/main.rs | 91 +-------------------------- crates/cdk/src/error.rs | 4 ++ crates/cdk/src/mint/start_up_check.rs | 86 +++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 88 deletions(-) diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 3e82b6c23..9f53aa345 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -14,8 +14,8 @@ use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; -use cdk::mint::{MeltQuote, Mint, MintBuilder, MintMeltLimits}; -use cdk::nuts::{ContactInfo, CurrencyUnit, MeltQuoteState, MintVersion, PaymentMethod}; +use cdk::mint::{MintBuilder, MintMeltLimits}; +use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; use cdk::types::LnKey; use cdk_mintd::cli::CLIArgs; use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; @@ -266,7 +266,7 @@ async fn main() -> anyhow::Result<()> { // Checks the status of all pending melt quotes // Pending melt quotes where the payment has gone through inputs are burnt // Pending melt quotes where the payment has **failed** inputs are reset to unspent - check_pending_melt_quotes(Arc::clone(&mint), &ln_backends).await?; + mint.check_pending_melt_quotes().await?; let listen_addr = settings.info.listen_host; let listen_port = settings.info.listen_port; @@ -339,91 +339,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn check_pending_melt_quotes( - mint: Arc, - ln_backends: &HashMap + Send + Sync>>, -) -> Result<()> { - let melt_quotes = mint.localstore.get_melt_quotes().await?; - let pending_quotes: Vec = melt_quotes - .into_iter() - .filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown) - .collect(); - tracing::info!("There are {} pending melt quotes.", pending_quotes.len()); - - for pending_quote in pending_quotes { - tracing::debug!("Checking status for melt quote {}.", pending_quote.id); - let melt_request_ln_key = mint.localstore.get_melt_request(&pending_quote.id).await?; - - let (melt_request, ln_key) = match melt_request_ln_key { - None => ( - None, - LnKey { - unit: pending_quote.unit, - method: PaymentMethod::Bolt11, - }, - ), - Some((melt_request, ln_key)) => (Some(melt_request), ln_key), - }; - - let ln_backend = match ln_backends.get(&ln_key) { - Some(ln_backend) => ln_backend, - None => { - tracing::warn!("No backend for ln key: {:?}", ln_key); - continue; - } - }; - - let pay_invoice_response = ln_backend - .check_outgoing_payment(&pending_quote.request_lookup_id) - .await?; - - match melt_request { - Some(melt_request) => { - match pay_invoice_response.status { - MeltQuoteState::Paid => { - if let Err(err) = mint - .process_melt_request(&melt_request, pay_invoice_response.total_spent) - .await - { - tracing::error!( - "Could not process melt request for pending quote: {}", - melt_request.quote - ); - tracing::error!("{}", err); - } - } - MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => { - // Payment has not been made we want to unset - tracing::info!("Lightning payment for quote {} failed.", pending_quote.id); - if let Err(err) = mint.process_unpaid_melt(&melt_request).await { - tracing::error!("Could not reset melt quote state: {}", err); - } - } - MeltQuoteState::Pending => { - tracing::warn!( - "LN payment pending, proofs are stuck as pending for quote: {}", - melt_request.quote - ); - // Quote is still pending we do not want to do anything - // continue to check next quote - } - } - } - None => { - tracing::warn!( - "There is no stored melt request for pending melt quote: {}", - pending_quote.id - ); - - mint.localstore - .update_melt_quote_state(&pending_quote.id, pay_invoice_response.status) - .await?; - } - }; - } - Ok(()) -} - fn work_dir() -> Result { let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index d5632d243..844eeacdd 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -255,6 +255,10 @@ pub enum Error { #[cfg(any(feature = "wallet", feature = "mint"))] #[error(transparent)] Database(#[from] crate::cdk_database::Error), + /// Lightning Error + #[cfg(feature = "mint")] + #[error(transparent)] + Lightning(#[from] crate::cdk_lightning::Error), } impl From for Error { diff --git a/crates/cdk/src/mint/start_up_check.rs b/crates/cdk/src/mint/start_up_check.rs index 7d3d920ba..59eef658d 100644 --- a/crates/cdk/src/mint/start_up_check.rs +++ b/crates/cdk/src/mint/start_up_check.rs @@ -4,6 +4,7 @@ //! These ensure that the status of the mint or melt quote matches in the mint db and on the node. use super::{Error, Mint}; +use crate::mint::{MeltQuote, MeltQuoteState}; impl Mint { /// Check the status of all pending mint quotes in the mint db @@ -40,4 +41,89 @@ impl Mint { } Ok(()) } + + /// Checks the states of melt quotes that are **PENDING** or **UNKNOWN** to the mint with the ln node + pub async fn check_pending_melt_quotes(&self) -> Result<(), Error> { + let melt_quotes = self.localstore.get_melt_quotes().await?; + let pending_quotes: Vec = melt_quotes + .into_iter() + .filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown) + .collect(); + tracing::info!("There are {} pending melt quotes.", pending_quotes.len()); + + for pending_quote in pending_quotes { + tracing::debug!("Checking status for melt quote {}.", pending_quote.id); + let melt_request_ln_key = self.localstore.get_melt_request(&pending_quote.id).await?; + + let (melt_request, unit) = match melt_request_ln_key { + None => (None, pending_quote.unit), + Some((melt_request, ln_key)) => (Some(melt_request), ln_key.unit), + }; + + let ln_backend = match self.ln.get(&unit) { + Some(ln_backend) => ln_backend, + None => { + tracing::warn!("No backend for ln key: {:?}", unit); + continue; + } + }; + + let pay_invoice_response = ln_backend + .check_outgoing_payment(&pending_quote.request_lookup_id) + .await?; + + match melt_request { + Some(melt_request) => { + match pay_invoice_response.status { + MeltQuoteState::Paid => { + if let Err(err) = self + .process_melt_request( + &melt_request, + pay_invoice_response.total_spent, + ) + .await + { + tracing::error!( + "Could not process melt request for pending quote: {}", + melt_request.quote + ); + tracing::error!("{}", err); + } + } + MeltQuoteState::Unpaid + | MeltQuoteState::Unknown + | MeltQuoteState::Failed => { + // Payment has not been made we want to unset + tracing::info!( + "Lightning payment for quote {} failed.", + pending_quote.id + ); + if let Err(err) = self.process_unpaid_melt(&melt_request).await { + tracing::error!("Could not reset melt quote state: {}", err); + } + } + MeltQuoteState::Pending => { + tracing::warn!( + "LN payment pending, proofs are stuck as pending for quote: {}", + melt_request.quote + ); + // Quote is still pending we do not want to do anything + // continue to check next quote + } + } + } + None => { + tracing::warn!( + "There is no stored melt request for pending melt quote: {}", + pending_quote.id + ); + + self.localstore + .update_melt_quote_state(&pending_quote.id, pay_invoice_response.status) + .await?; + } + }; + } + Ok(()) + } } From 558aedaff16ed34a03ec22c38c3075f8b2c2eec5 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 18 Nov 2024 10:20:06 +0000 Subject: [PATCH 30/35] feat: rename to 20 and 21 --- crates/cdk-axum/src/bolt12_router.rs | 3 +- crates/cdk/src/cdk_lightning/bolt12.rs | 3 +- crates/cdk/src/error.rs | 4 +- crates/cdk/src/mint/mint_18.rs | 2 +- crates/cdk/src/nuts/mod.rs | 5 +- crates/cdk/src/nuts/nut19.rs | 66 ------------------ crates/cdk/src/nuts/nut20.rs | 97 ++++++++++++-------------- crates/cdk/src/nuts/nut21.rs | 77 ++++++++++++++++++++ crates/cdk/src/wallet/client.rs | 6 +- crates/cdk/src/wallet/mint_bolt12.rs | 4 +- 10 files changed, 133 insertions(+), 134 deletions(-) delete mode 100644 crates/cdk/src/nuts/nut19.rs create mode 100644 crates/cdk/src/nuts/nut21.rs diff --git a/crates/cdk-axum/src/bolt12_router.rs b/crates/cdk-axum/src/bolt12_router.rs index 963c9260a..25a83e642 100644 --- a/crates/cdk-axum/src/bolt12_router.rs +++ b/crates/cdk-axum/src/bolt12_router.rs @@ -1,10 +1,9 @@ use anyhow::Result; use axum::extract::{Json, Path, State}; use axum::response::Response; -use cdk::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use cdk::nuts::{ MeltBolt12Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request, MintBolt11Request, - MintBolt11Response, + MintBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, }; use crate::{into_response, MintState}; diff --git a/crates/cdk/src/cdk_lightning/bolt12.rs b/crates/cdk/src/cdk_lightning/bolt12.rs index 0a760da57..4eb7cdaa9 100644 --- a/crates/cdk/src/cdk_lightning/bolt12.rs +++ b/crates/cdk/src/cdk_lightning/bolt12.rs @@ -9,8 +9,7 @@ use serde::{Deserialize, Serialize}; use super::{ Bolt12PaymentQuoteResponse, CreateOfferResponse, Error, PayInvoiceResponse, WaitInvoiceResponse, }; -use crate::nuts::nut20::MeltQuoteBolt12Request; -use crate::nuts::CurrencyUnit; +use crate::nuts::{CurrencyUnit, MeltQuoteBolt12Request}; use crate::{mint, Amount}; /// MintLighting Bolt12 Trait diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 844eeacdd..a76ce9dce 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -247,10 +247,10 @@ pub enum Error { NUT18(#[from] crate::nuts::nut18::Error), /// NUT19 Error #[error(transparent)] - NUT19(#[from] crate::nuts::nut19::Error), + NUT19(#[from] crate::nuts::nut20::Error), /// NUT18 Error #[error(transparent)] - NUT20(#[from] crate::nuts::nut20::Error), + NUT20(#[from] crate::nuts::nut21::Error), /// Database Error #[cfg(any(feature = "wallet", feature = "mint"))] #[error(transparent)] diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_18.rs index 447a6da38..56f03bda0 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_18.rs @@ -1,6 +1,6 @@ use tracing::instrument; -use super::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; +use super::nut20::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use super::{Mint, MintQuote, PaymentMethod}; use crate::util::unix_time; use crate::{Amount, Error}; diff --git a/crates/cdk/src/nuts/mod.rs b/crates/cdk/src/nuts/mod.rs index b99b207e6..5c5e5ce89 100644 --- a/crates/cdk/src/nuts/mod.rs +++ b/crates/cdk/src/nuts/mod.rs @@ -21,8 +21,8 @@ pub mod nut15; #[cfg(feature = "mint")] pub mod nut17; pub mod nut18; -pub mod nut19; pub mod nut20; +pub mod nut21; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, @@ -54,4 +54,5 @@ pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings}; #[cfg(feature = "mint")] pub use nut17::{NotificationPayload, PubSubManager}; pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport}; -pub use nut20::{MeltBolt12Request, MeltQuoteBolt12Request}; +pub use nut20::{MintQuoteBolt12Request, MintQuoteBolt12Response}; +pub use nut21::{MeltBolt12Request, MeltQuoteBolt12Request}; diff --git a/crates/cdk/src/nuts/nut19.rs b/crates/cdk/src/nuts/nut19.rs deleted file mode 100644 index a15529005..000000000 --- a/crates/cdk/src/nuts/nut19.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! NUT-17: Mint Tokens via Bolt11 -//! -//! - -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use super::nut00::CurrencyUnit; -use crate::Amount; - -/// NUT04 Error -#[derive(Debug, Error)] -pub enum Error { - /// Unknown Quote State - #[error("Unknown Quote State")] - UnknownState, - /// Amount overflow - #[error("Amount overflow")] - AmountOverflow, -} - -/// Mint quote request [NUT-19] -#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct MintQuoteBolt12Request { - /// Amount - pub amount: Option, - /// Unit wallet would like to pay with - pub unit: CurrencyUnit, - /// Memo to create the invoice with - pub description: Option, - /// Single use - pub single_use: bool, - /// Expiry - pub expiry: Option, -} - -/// Mint quote response [NUT-19] -#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct MintQuoteBolt12Response { - /// Quote Id - pub quote: String, - /// Payment request to fulfil - pub request: String, - /// Single use - pub single_use: bool, - /// Unix timestamp until the quote is valid - pub expiry: Option, - /// Amount that has been paid - pub amount_paid: Amount, - /// Amount that has been issued - pub amount_issued: Amount, -} - -#[cfg(feature = "mint")] -impl From for MintQuoteBolt12Response { - fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt12Response { - MintQuoteBolt12Response { - quote: mint_quote.id, - request: mint_quote.request, - expiry: Some(mint_quote.expiry), - amount_paid: mint_quote.amount_paid, - amount_issued: mint_quote.amount_issued, - single_use: mint_quote.single_use, - } - } -} diff --git a/crates/cdk/src/nuts/nut20.rs b/crates/cdk/src/nuts/nut20.rs index 30af3ac58..a15529005 100644 --- a/crates/cdk/src/nuts/nut20.rs +++ b/crates/cdk/src/nuts/nut20.rs @@ -1,77 +1,66 @@ -//! Bolt12 +//! NUT-17: Mint Tokens via Bolt11 +//! +//! + use serde::{Deserialize, Serialize}; use thiserror::Error; -use super::nut05::MeltRequestTrait; -use super::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs}; +use super::nut00::CurrencyUnit; use crate::Amount; -/// NUT18 Error +/// NUT04 Error #[derive(Debug, Error)] pub enum Error { /// Unknown Quote State - #[error("Unknown quote state")] + #[error("Unknown Quote State")] UnknownState, /// Amount overflow - #[error("Amount Overflow")] + #[error("Amount overflow")] AmountOverflow, } -/// Melt quote request [NUT-18] +/// Mint quote request [NUT-19] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct MeltQuoteBolt12Request { - /// Bolt12 invoice to be paid - pub request: String, +pub struct MintQuoteBolt12Request { + /// Amount + pub amount: Option, /// Unit wallet would like to pay with pub unit: CurrencyUnit, - /// Payment Options - pub amount: Option, + /// Memo to create the invoice with + pub description: Option, + /// Single use + pub single_use: bool, + /// Expiry + pub expiry: Option, } -/// Melt Bolt12 Request [NUT-18] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MeltBolt12Request { - /// Quote ID +/// Mint quote response [NUT-19] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintQuoteBolt12Response { + /// Quote Id pub quote: String, - /// Proofs - pub inputs: Proofs, - /// Blinded Message that can be used to return change [NUT-08] - /// Amount field of BlindedMessages `SHOULD` be set to zero - pub outputs: Option>, + /// Payment request to fulfil + pub request: String, + /// Single use + pub single_use: bool, + /// Unix timestamp until the quote is valid + pub expiry: Option, + /// Amount that has been paid + pub amount_paid: Amount, + /// Amount that has been issued + pub amount_issued: Amount, } -impl MeltRequestTrait for MeltBolt12Request { - type Err = Error; - - fn get_quote_id(&self) -> &str { - &self.quote - } - - fn get_inputs(&self) -> &Proofs { - &self.inputs - } - - fn get_outputs(&self) -> &Option> { - &self.outputs - } - - fn inputs_amount(&self) -> Result { - Amount::try_sum(self.inputs.iter().map(|proof| proof.amount)) - .map_err(|_| Error::AmountOverflow) - } - - fn outputs_amount(&self) -> Result { - Amount::try_sum( - self.outputs - .as_ref() - .unwrap_or(&vec![]) - .iter() - .map(|proof| proof.amount), - ) - .map_err(|_| Error::AmountOverflow) - } - - fn get_payment_method(&self) -> PaymentMethod { - PaymentMethod::Bolt12 +#[cfg(feature = "mint")] +impl From for MintQuoteBolt12Response { + fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt12Response { + MintQuoteBolt12Response { + quote: mint_quote.id, + request: mint_quote.request, + expiry: Some(mint_quote.expiry), + amount_paid: mint_quote.amount_paid, + amount_issued: mint_quote.amount_issued, + single_use: mint_quote.single_use, + } } } diff --git a/crates/cdk/src/nuts/nut21.rs b/crates/cdk/src/nuts/nut21.rs new file mode 100644 index 000000000..30af3ac58 --- /dev/null +++ b/crates/cdk/src/nuts/nut21.rs @@ -0,0 +1,77 @@ +//! Bolt12 +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::nut05::MeltRequestTrait; +use super::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs}; +use crate::Amount; + +/// NUT18 Error +#[derive(Debug, Error)] +pub enum Error { + /// Unknown Quote State + #[error("Unknown quote state")] + UnknownState, + /// Amount overflow + #[error("Amount Overflow")] + AmountOverflow, +} + +/// Melt quote request [NUT-18] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltQuoteBolt12Request { + /// Bolt12 invoice to be paid + pub request: String, + /// Unit wallet would like to pay with + pub unit: CurrencyUnit, + /// Payment Options + pub amount: Option, +} + +/// Melt Bolt12 Request [NUT-18] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltBolt12Request { + /// Quote ID + pub quote: String, + /// Proofs + pub inputs: Proofs, + /// Blinded Message that can be used to return change [NUT-08] + /// Amount field of BlindedMessages `SHOULD` be set to zero + pub outputs: Option>, +} + +impl MeltRequestTrait for MeltBolt12Request { + type Err = Error; + + fn get_quote_id(&self) -> &str { + &self.quote + } + + fn get_inputs(&self) -> &Proofs { + &self.inputs + } + + fn get_outputs(&self) -> &Option> { + &self.outputs + } + + fn inputs_amount(&self) -> Result { + Amount::try_sum(self.inputs.iter().map(|proof| proof.amount)) + .map_err(|_| Error::AmountOverflow) + } + + fn outputs_amount(&self) -> Result { + Amount::try_sum( + self.outputs + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|proof| proof.amount), + ) + .map_err(|_| Error::AmountOverflow) + } + + fn get_payment_method(&self) -> PaymentMethod { + PaymentMethod::Bolt12 + } +} diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/client.rs index d8d4e4b62..5429fbc6d 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -11,13 +11,13 @@ use url::Url; use super::Error; use crate::error::ErrorResponse; use crate::mint_url::MintUrl; -use crate::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, MeltBolt11Request, MeltBolt12Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request, MintBolt11Request, MintBolt11Response, - MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, Proof, - RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, + MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteBolt12Request, + MintQuoteBolt12Response, PreMintSecrets, Proof, RestoreRequest, RestoreResponse, SwapRequest, + SwapResponse, }; use crate::Amount; diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs index 3111b74f5..1341ac3ce 100644 --- a/crates/cdk/src/wallet/mint_bolt12.rs +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -4,9 +4,9 @@ use super::MintQuote; use crate::amount::SplitTarget; use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; -use crate::nuts::nut19::{MintQuoteBolt12Request, MintQuoteBolt12Response}; use crate::nuts::{ - nut12, MintBolt11Request, PaymentMethod, PreMintSecrets, SpendingConditions, State, + nut12, MintBolt11Request, MintQuoteBolt12Request, MintQuoteBolt12Response, PaymentMethod, + PreMintSecrets, SpendingConditions, State, }; use crate::types::ProofInfo; use crate::util::unix_time; From 1a7521503b3bb97bab7c9901f73c2474eb44ac5a Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 18 Nov 2024 10:49:52 +0000 Subject: [PATCH 31/35] feat: pubkey on bolt12 --- crates/cdk-cli/src/sub_commands/mint.rs | 8 +++++--- .../cdk/src/mint/{mint_18.rs => mint_20.rs} | 8 ++++---- crates/cdk/src/mint/mod.rs | 2 +- crates/cdk/src/nuts/nut20.rs | 19 +++++++++++++++---- crates/cdk/src/wallet/mint_bolt12.rs | 4 +++- 5 files changed, 28 insertions(+), 13 deletions(-) rename crates/cdk/src/mint/{mint_18.rs => mint_20.rs} (96%) diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index f6f4329cd..11771fdfd 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -6,7 +6,7 @@ use anyhow::Result; use cdk::amount::SplitTarget; use cdk::cdk_database::{Error, WalletDatabase}; use cdk::mint_url::MintUrl; -use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod}; +use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod, SecretKey}; use cdk::wallet::multi_mint_wallet::WalletKey; use cdk::wallet::{MultiMintWallet, Wallet}; use clap::Args; @@ -57,6 +57,8 @@ pub async fn mint( } }; + let secret_key = SecretKey::generate(); + let method = PaymentMethod::from_str(&sub_command_args.method)?; let quote = match method { @@ -69,8 +71,7 @@ pub async fn mint( .expect("Amount must be defined") .into(), description, - // TODO: Get pubkey - None, + Some(secret_key.public_key()), ) .await? } @@ -81,6 +82,7 @@ pub async fn mint( description, sub_command_args.single_use.unwrap_or(false), sub_command_args.expiry, + secret_key.public_key(), ) .await? } diff --git a/crates/cdk/src/mint/mint_18.rs b/crates/cdk/src/mint/mint_20.rs similarity index 96% rename from crates/cdk/src/mint/mint_18.rs rename to crates/cdk/src/mint/mint_20.rs index 0641b913b..72fcaa9c9 100644 --- a/crates/cdk/src/mint/mint_18.rs +++ b/crates/cdk/src/mint/mint_20.rs @@ -18,6 +18,7 @@ impl Mint { description, single_use, expiry, + pubkey, } = mint_quote_request; let nut18 = &self @@ -68,8 +69,7 @@ impl Mint { Amount::ZERO, single_use, vec![], - // TODO: Add pubkey to request - None, + Some(pubkey), ); tracing::debug!( @@ -82,7 +82,7 @@ impl Mint { self.localstore.add_mint_quote(quote.clone()).await?; - Ok(quote.into()) + Ok(quote.try_into()?) } /// Check mint quote @@ -97,6 +97,6 @@ impl Mint { .await? .ok_or(Error::UnknownQuote)?; - Ok(quote.into()) + Ok(quote.try_into()?) } } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 8d47378f0..42eaf8b53 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -30,7 +30,7 @@ mod check_spendable; mod info; mod keysets; mod melt; -mod mint_18; +mod mint_20; mod mint_nut04; mod start_up_check; mod swap; diff --git a/crates/cdk/src/nuts/nut20.rs b/crates/cdk/src/nuts/nut20.rs index a15529005..a3429c9af 100644 --- a/crates/cdk/src/nuts/nut20.rs +++ b/crates/cdk/src/nuts/nut20.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::nut00::CurrencyUnit; +use super::PublicKey; use crate::Amount; /// NUT04 Error @@ -17,6 +18,9 @@ pub enum Error { /// Amount overflow #[error("Amount overflow")] AmountOverflow, + /// Publickey not defined + #[error("Publickey not defined")] + PublickeyUndefined, } /// Mint quote request [NUT-19] @@ -32,6 +36,8 @@ pub struct MintQuoteBolt12Request { pub single_use: bool, /// Expiry pub expiry: Option, + /// Pubkey + pub pubkey: PublicKey, } /// Mint quote response [NUT-19] @@ -49,18 +55,23 @@ pub struct MintQuoteBolt12Response { pub amount_paid: Amount, /// Amount that has been issued pub amount_issued: Amount, + /// Pubkey + pub pubkey: PublicKey, } #[cfg(feature = "mint")] -impl From for MintQuoteBolt12Response { - fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt12Response { - MintQuoteBolt12Response { +impl TryFrom for MintQuoteBolt12Response { + type Error = Error; + + fn try_from(mint_quote: crate::mint::MintQuote) -> Result { + Ok(MintQuoteBolt12Response { quote: mint_quote.id, request: mint_quote.request, expiry: Some(mint_quote.expiry), amount_paid: mint_quote.amount_paid, amount_issued: mint_quote.amount_issued, single_use: mint_quote.single_use, - } + pubkey: mint_quote.pubkey.ok_or(Error::PublickeyUndefined)?, + }) } } diff --git a/crates/cdk/src/wallet/mint_bolt12.rs b/crates/cdk/src/wallet/mint_bolt12.rs index 60d148320..18c808a73 100644 --- a/crates/cdk/src/wallet/mint_bolt12.rs +++ b/crates/cdk/src/wallet/mint_bolt12.rs @@ -6,7 +6,7 @@ use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ nut12, MintBolt11Request, MintQuoteBolt12Request, MintQuoteBolt12Response, PaymentMethod, - PreMintSecrets, SpendingConditions, State, + PreMintSecrets, PublicKey, SpendingConditions, State, }; use crate::types::ProofInfo; use crate::util::unix_time; @@ -21,6 +21,7 @@ impl Wallet { description: Option, single_use: bool, expiry: Option, + pubkey: PublicKey, ) -> Result { let mint_url = self.mint_url.clone(); let unit = &self.unit; @@ -48,6 +49,7 @@ impl Wallet { description, single_use, expiry, + pubkey, }; let quote_res = self From cd67f63a6ae9c865ec108f584692188a70e6aaf6 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 18 Nov 2024 15:38:48 +0000 Subject: [PATCH 32/35] feat: bolt12 on fake wallet --- crates/cdk-fake-wallet/Cargo.toml | 2 + crates/cdk-fake-wallet/src/bolt12.rs | 204 +++++++++++++++++++++++++++ crates/cdk-fake-wallet/src/lib.rs | 6 + crates/cdk-mintd/src/main.rs | 5 +- 4 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 crates/cdk-fake-wallet/src/bolt12.rs diff --git a/crates/cdk-fake-wallet/Cargo.toml b/crates/cdk-fake-wallet/Cargo.toml index 8d1b5dffe..5ab90eede 100644 --- a/crates/cdk-fake-wallet/Cargo.toml +++ b/crates/cdk-fake-wallet/Cargo.toml @@ -11,6 +11,7 @@ description = "CDK fake ln backend" [dependencies] async-trait = "0.1.74" +anyhow = "1" bitcoin = { version = "0.32.2", default-features = false } cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = ["mint"] } futures = { version = "0.3.28", default-features = false } @@ -22,5 +23,6 @@ serde = "1" serde_json = "1" uuid = { version = "1", features = ["v4"] } lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } +lightning = { version = "0.0.125", default-features = false, features = ["std"]} tokio-stream = "0.1.15" rand = "0.8.5" diff --git a/crates/cdk-fake-wallet/src/bolt12.rs b/crates/cdk-fake-wallet/src/bolt12.rs new file mode 100644 index 000000000..4bfc17e5a --- /dev/null +++ b/crates/cdk-fake-wallet/src/bolt12.rs @@ -0,0 +1,204 @@ +use std::pin::Pin; +use std::str::FromStr; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use anyhow::anyhow; +use async_trait::async_trait; +use bitcoin::key::Secp256k1; +use cdk::amount::{to_unit, Amount}; +use cdk::cdk_lightning::bolt12::{Bolt12Settings, MintBolt12Lightning}; +use cdk::cdk_lightning::{ + self, Bolt12PaymentQuoteResponse, CreateOfferResponse, PayInvoiceResponse, WaitInvoiceResponse, +}; +use cdk::mint; +use cdk::mint::types::PaymentRequest; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt12Request}; +use futures::stream::StreamExt; +use futures::Stream; +use lightning::offers::offer::{Amount as LDKAmount, Offer, OfferBuilder}; +use tokio::time; +use tokio_stream::wrappers::ReceiverStream; +use uuid::Uuid; + +use crate::FakeWallet; + +#[async_trait] +impl MintBolt12Lightning for FakeWallet { + type Err = cdk_lightning::Error; + + fn get_settings(&self) -> Bolt12Settings { + Bolt12Settings { + mint: true, + melt: true, + unit: CurrencyUnit::Sat, + offer_description: true, + } + } + + fn is_wait_invoice_active(&self) -> bool { + self.wait_invoice_is_active.load(Ordering::SeqCst) + } + + fn cancel_wait_invoice(&self) { + self.wait_invoice_cancel_token.cancel() + } + + async fn wait_any_offer( + &self, + ) -> Result + Send>>, Self::Err> { + let receiver = self + .bolt12_receiver + .lock() + .await + .take() + .ok_or(super::Error::NoReceiver)?; + let receiver_stream = ReceiverStream::new(receiver); + self.wait_invoice_is_active.store(true, Ordering::SeqCst); + + Ok(Box::pin(receiver_stream.map(|label| WaitInvoiceResponse { + request_lookup_id: label.clone(), + payment_amount: Amount::ZERO, + unit: CurrencyUnit::Sat, + payment_id: label, + }))) + } + + async fn get_bolt12_payment_quote( + &self, + melt_quote_request: &MeltQuoteBolt12Request, + ) -> Result { + let amount = match melt_quote_request.amount { + Some(amount) => amount, + None => { + let offer = Offer::from_str(&melt_quote_request.request) + .map_err(|_| anyhow!("Invalid offer in request"))?; + + match offer.amount() { + Some(LDKAmount::Bitcoin { amount_msats }) => amount_msats.into(), + None => { + return Err(cdk_lightning::Error::Anyhow(anyhow!( + "Amount not defined in offer or request" + ))) + } + _ => return Err(cdk_lightning::Error::Anyhow(anyhow!("Unsupported unit"))), + } + } + }; + + let relative_fee_reserve = + (self.fee_reserve.percent_fee_reserve * u64::from(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(Bolt12PaymentQuoteResponse { + request_lookup_id: Uuid::new_v4().to_string(), + amount, + fee: fee.into(), + state: cdk::nuts::MeltQuoteState::Unpaid, + invoice: Some("".to_string()), + }) + } + + async fn pay_bolt12_offer( + &self, + melt_quote: mint::MeltQuote, + _amount: Option, + _max_fee_amount: Option, + ) -> Result { + let bolt12 = &match melt_quote.request { + PaymentRequest::Bolt11 { .. } => return Err(super::Error::WrongRequestType.into()), + PaymentRequest::Bolt12 { offer, invoice: _ } => offer, + }; + + // let description = bolt12.description().to_string(); + + // let status: Option = serde_json::from_str(&description).ok(); + + // let mut payment_states = self.payment_states.lock().await; + // let payment_status = status + // .clone() + // .map(|s| s.pay_invoice_state) + // .unwrap_or(MeltQuoteState::Paid); + + // let checkout_going_status = status + // .clone() + // .map(|s| s.check_payment_state) + // .unwrap_or(MeltQuoteState::Paid); + + // payment_states.insert(payment_hash.clone(), checkout_going_status); + + // if let Some(description) = status { + // if description.check_err { + // let mut fail = self.failed_payment_check.lock().await; + // fail.insert(payment_hash.clone()); + // } + + // if description.pay_err { + // return Err(Error::UnknownInvoice.into()); + // } + // } + + Ok(PayInvoiceResponse { + payment_preimage: Some("".to_string()), + payment_lookup_id: bolt12.to_string(), + status: super::MeltQuoteState::Paid, + total_spent: melt_quote.amount, + unit: melt_quote.unit, + }) + } + + async fn create_bolt12_offer( + &self, + amount: Option, + unit: &CurrencyUnit, + description: String, + unix_expiry: u64, + _single_use: bool, + ) -> Result { + let secret_key = bitcoin::secp256k1::SecretKey::new(&mut rand::thread_rng()); + + let secp_ctx = Secp256k1::new(); + + let offer_builder = OfferBuilder::new(secret_key.public_key(&secp_ctx)) + .description(description) + .absolute_expiry(Duration::from_secs(unix_expiry)); + + let offer_builder = match amount { + Some(amount) => { + let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; + offer_builder.amount_msats(amount.into()) + } + None => offer_builder, + }; + + let offer = offer_builder.build().unwrap(); + + let offer_string = offer.to_string(); + + let sender = self.bolt12_sender.clone(); + + let duration = time::Duration::from_secs(self.payment_delay); + + tokio::spawn(async move { + // Wait for the random delay to elapse + time::sleep(duration).await; + + // Send the message after waiting for the specified duration + if sender.send(offer_string.clone()).await.is_err() { + tracing::error!("Failed to send label: {}", offer_string); + } + }); + + Ok(CreateOfferResponse { + request_lookup_id: offer.to_string(), + request: offer, + expiry: Some(unix_expiry), + }) + } +} diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index f226cc97d..1389bf955 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -34,6 +34,7 @@ use tokio::time; use tokio_stream::wrappers::ReceiverStream; use tokio_util::sync::CancellationToken; +mod bolt12; pub mod error; /// Fake Wallet @@ -42,6 +43,8 @@ pub struct FakeWallet { fee_reserve: FeeReserve, sender: tokio::sync::mpsc::Sender, receiver: Arc>>>, + bolt12_sender: tokio::sync::mpsc::Sender, + bolt12_receiver: Arc>>>, payment_states: Arc>>, failed_payment_check: Arc>>, payment_delay: u64, @@ -58,11 +61,14 @@ impl FakeWallet { payment_delay: u64, ) -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(8); + let (bolt12_sender, bolt12_receiver) = tokio::sync::mpsc::channel(8); Self { fee_reserve, sender, receiver: Arc::new(Mutex::new(Some(receiver))), + bolt12_sender, + bolt12_receiver: Arc::new(Mutex::new(Some(bolt12_receiver))), payment_states: Arc::new(Mutex::new(payment_states)), failed_payment_check: Arc::new(Mutex::new(fail_payment_check)), payment_delay, diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 9f53aa345..5253685bb 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -214,9 +214,8 @@ async fn main() -> anyhow::Result<()> { mint_builder = mint_builder.add_ln_backend(unit.clone(), mint_melt_limits, fake.clone()); - // TODO: Bolt12 for fake - // mint_builder = - // mint_builder.add_bolt12_ln_backend(unit, mint_melt_limits, fake.clone()); + mint_builder = + mint_builder.add_bolt12_ln_backend(unit, mint_melt_limits, fake.clone()); } } }; From 31158c875a9f625f5e81f4f3317101f4215122c7 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Wed, 20 Nov 2024 11:41:32 +0000 Subject: [PATCH 33/35] feat: update regtest --- crates/cdk-integration-tests/Cargo.toml | 2 +- .../cdk-integration-tests/src/init_regtest.rs | 55 ++++++++++++------- crates/cdk-integration-tests/src/main.rs | 5 ++ crates/cdk-integration-tests/tests/regtest.rs | 3 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index 8753408c1..c46f0da9c 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -33,7 +33,7 @@ uuid = { version = "1", features = ["v4"] } serde = "1" serde_json = "1" # ln-regtest-rs = { path = "../../../../ln-regtest-rs" } -ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "1d88d3d0b" } +ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "aabd1971a45" } lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } tracing = { version = "0.1", default-features = false, features = [ "attributes", diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 9550b4404..2b206df57 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -15,9 +15,10 @@ use cdk_cln::Cln as CdkCln; use ln_regtest_rs::bitcoin_client::BitcoinClient; use ln_regtest_rs::bitcoind::Bitcoind; use ln_regtest_rs::cln::Clnd; -use ln_regtest_rs::cln_client::ClnClient; +use ln_regtest_rs::ln_client::ClnClient; +use ln_regtest_rs::ln_client::LightningClient; +use ln_regtest_rs::ln_client::LndClient; use ln_regtest_rs::lnd::Lnd; -use ln_regtest_rs::lnd_client::LndClient; use tokio::sync::Notify; use tower_http::cors::CorsLayer; use tracing_subscriber::EnvFilter; @@ -28,8 +29,8 @@ const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333"; const BITCOIN_RPC_USER: &str = "testuser"; const BITCOIN_RPC_PASS: &str = "testpass"; const CLN_ADDR: &str = "127.0.0.1:19846"; -const LND_ADDR: &str = "0.0.0.0:18444"; -const LND_RPC_ADDR: &str = "https://127.0.0.1:10009"; +const LND_ADDR: &str = "0.0.0.0:18449"; +const LND_RPC_ADDR: &str = "localhost:10009"; const BITCOIN_DIR: &str = "bitcoin"; const CLN_DIR: &str = "cln"; @@ -116,6 +117,7 @@ pub async fn init_lnd() -> Lnd { get_bitcoin_dir(), get_lnd_dir(), LND_ADDR.parse().unwrap(), + LND_RPC_ADDR.to_string(), BITCOIN_RPC_USER.to_string(), BITCOIN_RPC_PASS.to_string(), ZMQ_RAW_BLOCK.to_string(), @@ -127,7 +129,12 @@ pub async fn init_lnd_client() -> Result { let lnd_dir = get_lnd_dir(); let cert_file = lnd_dir.join("tls.cert"); let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); - LndClient::new(LND_RPC_ADDR.parse().unwrap(), cert_file, macaroon_file).await + LndClient::new( + format!("https://{}", LND_RPC_ADDR).parse().unwrap(), + cert_file, + macaroon_file, + ) + .await } pub async fn create_cln_backend(cln_client: &ClnClient) -> Result { @@ -248,16 +255,20 @@ where Ok(()) } -pub async fn fund_ln( +pub async fn fund_ln( bitcoin_client: &BitcoinClient, - cln_client: &ClnClient, - lnd_client: &LndClient, -) -> Result<()> { - let lnd_address = lnd_client.get_new_address().await?; + cln_client: &C1, + lnd_client: &C2, +) -> Result<()> +where + C1: LightningClient, + C2: LightningClient, +{ + let lnd_address = lnd_client.get_new_onchain_address().await?; bitcoin_client.send_to_address(&lnd_address, 2_000_000)?; - let cln_address = cln_client.get_new_address().await?; + let cln_address = cln_client.get_new_onchain_address().await?; bitcoin_client.send_to_address(&cln_address, 2_000_000)?; let mining_address = bitcoin_client.get_new_address()?; @@ -269,19 +280,23 @@ pub async fn fund_ln( Ok(()) } -pub async fn open_channel( +pub async fn open_channel( bitcoin_client: &BitcoinClient, - cln_client: &ClnClient, - lnd_client: &LndClient, -) -> Result<()> { - let cln_info = cln_client.get_info().await?; + cln_client: &C1, + lnd_client: &C2, +) -> Result<()> +where + C1: LightningClient, + C2: LightningClient, +{ + let cln_info = cln_client.get_connect_info().await?; - let cln_pubkey = cln_info.id; - let cln_address = "127.0.0.1"; - let cln_port = 19846; + let cln_pubkey = cln_info.pubkey; + let cln_address = cln_info.address; + let cln_port = cln_info.port; lnd_client - .connect(cln_pubkey.to_string(), cln_address.to_string(), cln_port) + .connect_peer(cln_pubkey.to_string(), cln_address.to_string(), cln_port) .await .unwrap(); diff --git a/crates/cdk-integration-tests/src/main.rs b/crates/cdk-integration-tests/src/main.rs index 5cf76c4da..2faec334b 100644 --- a/crates/cdk-integration-tests/src/main.rs +++ b/crates/cdk-integration-tests/src/main.rs @@ -8,6 +8,7 @@ use cdk_integration_tests::init_regtest::{ }; use cdk_redb::MintRedbDatabase; use cdk_sqlite::MintSqliteDatabase; +use ln_regtest_rs::ln_client::LightningClient; #[tokio::main] async fn main() -> Result<()> { @@ -26,11 +27,15 @@ async fn main() -> Result<()> { let cln_client = init_cln_client().await?; + cln_client.wait_chain_sync().await.unwrap(); + let mut lnd = init_lnd().await; lnd.start_lnd().unwrap(); let lnd_client = init_lnd_client().await.unwrap(); + lnd_client.wait_chain_sync().await.unwrap(); + fund_ln(&bitcoin_client, &cln_client, &lnd_client) .await .unwrap(); diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 4f390ec01..9f53307a7 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -18,6 +18,7 @@ use cdk_integration_tests::init_regtest::{ }; use futures::{SinkExt, StreamExt}; use lightning_invoice::Bolt11Invoice; +use ln_regtest_rs::ln_client::LightningClient; use ln_regtest_rs::InvoiceStatus; use serde_json::json; use tokio::time::{sleep, timeout}; @@ -323,7 +324,7 @@ async fn test_regtest_internal_payment() -> Result<()> { let cln_client = init_cln_client().await?; let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?; let check_paid = cln_client - .check_incoming_invoice(payment_hash.payment_hash().to_string()) + .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) .await?; match check_paid { From da744153f89228c04bd3ecb70b8c0218fed6690a Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 21 Nov 2024 19:10:11 +0000 Subject: [PATCH 34/35] refactor: add second cln node --- .../cdk-integration-tests/src/init_regtest.rs | 74 +++++-------------- crates/cdk-integration-tests/src/main.rs | 67 +++++++++++++---- crates/cdk-integration-tests/tests/regtest.rs | 8 +- misc/itests.sh | 3 +- 4 files changed, 79 insertions(+), 73 deletions(-) diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 2b206df57..55d6cdcfd 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -14,26 +14,22 @@ use cdk::types::QuoteTTL; use cdk_cln::Cln as CdkCln; use ln_regtest_rs::bitcoin_client::BitcoinClient; use ln_regtest_rs::bitcoind::Bitcoind; -use ln_regtest_rs::cln::Clnd; use ln_regtest_rs::ln_client::ClnClient; use ln_regtest_rs::ln_client::LightningClient; use ln_regtest_rs::ln_client::LndClient; use ln_regtest_rs::lnd::Lnd; use tokio::sync::Notify; use tower_http::cors::CorsLayer; -use tracing_subscriber::EnvFilter; - -const BITCOIND_ADDR: &str = "127.0.0.1:18443"; -const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332"; -const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333"; -const BITCOIN_RPC_USER: &str = "testuser"; -const BITCOIN_RPC_PASS: &str = "testpass"; -const CLN_ADDR: &str = "127.0.0.1:19846"; + +pub const BITCOIND_ADDR: &str = "127.0.0.1:18443"; +pub const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332"; +pub const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333"; +pub const BITCOIN_RPC_USER: &str = "testuser"; +pub const BITCOIN_RPC_PASS: &str = "testpass"; const LND_ADDR: &str = "0.0.0.0:18449"; const LND_RPC_ADDR: &str = "localhost:10009"; const BITCOIN_DIR: &str = "bitcoin"; -const CLN_DIR: &str = "cln"; const LND_DIR: &str = "lnd"; pub fn get_mint_addr() -> String { @@ -86,26 +82,12 @@ pub fn init_bitcoin_client() -> Result { ) } -pub fn get_cln_dir() -> PathBuf { - let dir = get_temp_dir().join(CLN_DIR); +pub fn get_cln_dir(name: &str) -> PathBuf { + let dir = get_temp_dir().join(name); std::fs::create_dir_all(&dir).unwrap(); dir } -pub fn init_cln() -> Clnd { - Clnd::new( - get_bitcoin_dir(), - get_cln_dir(), - CLN_ADDR.to_string().parse().unwrap(), - BITCOIN_RPC_USER.to_string(), - BITCOIN_RPC_PASS.to_string(), - ) -} - -pub async fn init_cln_client() -> Result { - ClnClient::new(get_cln_dir(), None).await -} - pub fn get_lnd_dir() -> PathBuf { let dir = get_temp_dir().join(LND_DIR); std::fs::create_dir_all(&dir).unwrap(); @@ -192,24 +174,11 @@ where Ok(mint) } -pub async fn start_cln_mint(addr: &str, port: u16, database: D) -> Result<()> +pub async fn start_cln_mint(cln_path: PathBuf, addr: &str, port: u16, database: D) -> Result<()> where D: MintDatabase + Send + Sync + 'static, { - let default_filter = "debug"; - - let sqlx_filter = "sqlx=warn"; - let hyper_filter = "hyper=warn"; - - let env_filter = EnvFilter::new(format!( - "{},{},{}", - default_filter, sqlx_filter, hyper_filter - )); - - // Parse input - tracing_subscriber::fmt().with_env_filter(env_filter).init(); - - let cln_client = init_cln_client().await?; + let cln_client = ClnClient::new(cln_path, None).await?; let cln_backend = create_cln_backend(&cln_client).await?; @@ -255,27 +224,20 @@ where Ok(()) } -pub async fn fund_ln( - bitcoin_client: &BitcoinClient, - cln_client: &C1, - lnd_client: &C2, -) -> Result<()> +pub async fn fund_ln(bitcoin_client: &BitcoinClient, ln_client: &C) -> Result<()> where - C1: LightningClient, - C2: LightningClient, + C: LightningClient, { - let lnd_address = lnd_client.get_new_onchain_address().await?; + let ln_address = ln_client.get_new_onchain_address().await?; - bitcoin_client.send_to_address(&lnd_address, 2_000_000)?; + bitcoin_client.send_to_address(&ln_address, 2_000_000)?; - let cln_address = cln_client.get_new_onchain_address().await?; - bitcoin_client.send_to_address(&cln_address, 2_000_000)?; + ln_client.wait_chain_sync().await?; - let mining_address = bitcoin_client.get_new_address()?; - bitcoin_client.generate_blocks(&mining_address, 200)?; + let mine_to_address = bitcoin_client.get_new_address()?; + bitcoin_client.generate_blocks(&mine_to_address, 10)?; - cln_client.wait_chain_sync().await?; - lnd_client.wait_chain_sync().await?; + ln_client.wait_chain_sync().await?; Ok(()) } diff --git a/crates/cdk-integration-tests/src/main.rs b/crates/cdk-integration-tests/src/main.rs index 2faec334b..d62698363 100644 --- a/crates/cdk-integration-tests/src/main.rs +++ b/crates/cdk-integration-tests/src/main.rs @@ -3,15 +3,30 @@ use std::env; use anyhow::Result; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk_integration_tests::init_regtest::{ - fund_ln, get_temp_dir, init_bitcoin_client, init_bitcoind, init_cln, init_cln_client, init_lnd, - init_lnd_client, open_channel, start_cln_mint, + fund_ln, get_bitcoin_dir, get_cln_dir, get_temp_dir, init_bitcoin_client, init_bitcoind, + init_lnd, init_lnd_client, open_channel, start_cln_mint, BITCOIN_RPC_PASS, BITCOIN_RPC_USER, }; use cdk_redb::MintRedbDatabase; use cdk_sqlite::MintSqliteDatabase; -use ln_regtest_rs::ln_client::LightningClient; +use ln_regtest_rs::{ + cln::Clnd, + ln_client::{ClnClient, LightningClient}, +}; +use tracing_subscriber::EnvFilter; + +const CLN_ADDR: &str = "127.0.0.1:19846"; +const CLN_TWO_ADDR: &str = "127.0.0.1:19847"; #[tokio::main] async fn main() -> Result<()> { + let default_filter = "debug"; + + let sqlx_filter = "sqlx=warn,h2=warn,hyper=warn"; + + let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter)); + + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + let mut bitcoind = init_bitcoind(); bitcoind.start_bitcoind()?; @@ -22,13 +37,39 @@ async fn main() -> Result<()> { let new_add = bitcoin_client.get_new_address()?; bitcoin_client.generate_blocks(&new_add, 200).unwrap(); - let mut clnd = init_cln(); + let cln_one_dir = get_cln_dir("one"); + let mut clnd = Clnd::new( + get_bitcoin_dir(), + cln_one_dir.clone(), + CLN_ADDR.into(), + BITCOIN_RPC_USER.to_string(), + BITCOIN_RPC_PASS.to_string(), + ); clnd.start_clnd()?; - let cln_client = init_cln_client().await?; + let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; cln_client.wait_chain_sync().await.unwrap(); + fund_ln(&bitcoin_client, &cln_client).await.unwrap(); + + // Create second cln + let cln_two_dir = get_cln_dir("two"); + let mut clnd_two = Clnd::new( + get_bitcoin_dir(), + cln_two_dir.clone(), + CLN_TWO_ADDR.into(), + BITCOIN_RPC_USER.to_string(), + BITCOIN_RPC_PASS.to_string(), + ); + clnd_two.start_clnd()?; + + let cln_two_client = ClnClient::new(cln_two_dir.clone(), None).await?; + + cln_client.wait_chain_sync().await.unwrap(); + + fund_ln(&bitcoin_client, &cln_two_client).await.unwrap(); + let mut lnd = init_lnd().await; lnd.start_lnd().unwrap(); @@ -36,9 +77,7 @@ async fn main() -> Result<()> { lnd_client.wait_chain_sync().await.unwrap(); - fund_ln(&bitcoin_client, &cln_client, &lnd_client) - .await - .unwrap(); + fund_ln(&bitcoin_client, &lnd_client).await.unwrap(); open_channel(&bitcoin_client, &cln_client, &lnd_client) .await @@ -49,18 +88,20 @@ async fn main() -> Result<()> { let mint_db_kind = env::var("MINT_DATABASE")?; + let db_path = get_temp_dir().join("mint"); + match mint_db_kind.as_str() { "MEMORY" => { - start_cln_mint(addr, port, MintMemoryDatabase::default()).await?; + start_cln_mint(cln_one_dir, addr, port, MintMemoryDatabase::default()).await?; } "SQLITE" => { - let sqlite_db = MintSqliteDatabase::new(&get_temp_dir().join("mint")).await?; + let sqlite_db = MintSqliteDatabase::new(&db_path).await?; sqlite_db.migrate().await; - start_cln_mint(addr, port, sqlite_db).await?; + start_cln_mint(cln_one_dir, addr, port, sqlite_db).await?; } "REDB" => { - let redb_db = MintRedbDatabase::new(&get_temp_dir().join("mint")).unwrap(); - start_cln_mint(addr, port, redb_db).await?; + let redb_db = MintRedbDatabase::new(&db_path).unwrap(); + start_cln_mint(cln_one_dir, addr, port, redb_db).await?; } _ => panic!("Unknown mint db type: {}", mint_db_kind), }; diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 9f53307a7..fc3211cca 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -14,11 +14,11 @@ use cdk::nuts::{ use cdk::wallet::client::{HttpClient, HttpClientMethods}; use cdk::wallet::Wallet; use cdk_integration_tests::init_regtest::{ - get_mint_url, get_mint_ws_url, init_cln_client, init_lnd_client, + get_cln_dir, get_mint_url, get_mint_ws_url, init_lnd_client, }; use futures::{SinkExt, StreamExt}; use lightning_invoice::Bolt11Invoice; -use ln_regtest_rs::ln_client::LightningClient; +use ln_regtest_rs::ln_client::{ClnClient, LightningClient}; use ln_regtest_rs::InvoiceStatus; use serde_json::json; use tokio::time::{sleep, timeout}; @@ -321,7 +321,9 @@ async fn test_regtest_internal_payment() -> Result<()> { .await .unwrap(); - let cln_client = init_cln_client().await?; + let cln_one_dir = get_cln_dir("one"); + let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?; let check_paid = cln_client .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) diff --git a/misc/itests.sh b/misc/itests.sh index 50eb7f7fb..5676a9613 100755 --- a/misc/itests.sh +++ b/misc/itests.sh @@ -15,7 +15,8 @@ cleanup() { # Kill processes lncli --lnddir="$cdk_itests/lnd" --network=regtest stop - lightning-cli --regtest --lightning-dir="$cdk_itests/cln/" stop + lightning-cli --regtest --lightning-dir="$cdk_itests/one/" stop + lightning-cli --regtest --lightning-dir="$cdk_itests/two/" stop bitcoin-cli --datadir="$cdk_itests/bitcoin" -rpcuser=testuser -rpcpassword=testpass -rpcport=18443 stop # Remove the temporary directory From cc4a63bd2bdd7c9b1bd592bb4f1c9bd03d6c9d3f Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 21 Nov 2024 22:50:30 +0000 Subject: [PATCH 35/35] fix: mint builder for bolt12 --- crates/cdk-integration-tests/Cargo.toml | 2 +- .../src/init_fake_wallet.rs | 2 +- .../cdk-integration-tests/src/init_regtest.rs | 86 +++++++++++-------- crates/cdk-integration-tests/tests/regtest.rs | 27 ++++++ crates/cdk/src/cdk_database/mint_memory.rs | 2 - crates/cdk/src/mint/builder.rs | 21 ++--- crates/cdk/src/mint/mint_20.rs | 4 +- crates/cdk/src/nuts/nut04.rs | 10 +-- crates/cdk/src/nuts/nut05.rs | 9 +- crates/cdk/src/nuts/nut06.rs | 22 ++--- 10 files changed, 105 insertions(+), 80 deletions(-) diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index c46f0da9c..1ea94b322 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -33,7 +33,7 @@ uuid = { version = "1", features = ["v4"] } serde = "1" serde_json = "1" # ln-regtest-rs = { path = "../../../../ln-regtest-rs" } -ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "aabd1971a45" } +ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "3a542821f" } lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } tracing = { version = "0.1", default-features = false, features = [ "attributes", diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index e6c47e7c8..980dea3c0 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -45,7 +45,7 @@ where ln_backends.insert(CurrencyUnit::Sat, Arc::new(fake_wallet)); - let mint = create_mint(database, ln_backends.clone()).await?; + let mint = create_mint(database, ln_backends.clone(), HashMap::new()).await?; let cache_ttl = 3600; let cache_tti = 3600; let mint_arc = Arc::new(mint); diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 55d6cdcfd..c5d260152 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -7,16 +7,14 @@ use anyhow::Result; use axum::Router; use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; +use cdk::cdk_lightning::bolt12::MintBolt12Lightning; use cdk::cdk_lightning::MintLightning; -use cdk::mint::{FeeReserve, Mint}; -use cdk::nuts::{CurrencyUnit, MintInfo}; -use cdk::types::QuoteTTL; +use cdk::mint::{FeeReserve, Mint, MintBuilder, MintMeltLimits}; +use cdk::nuts::CurrencyUnit; use cdk_cln::Cln as CdkCln; use ln_regtest_rs::bitcoin_client::BitcoinClient; use ln_regtest_rs::bitcoind::Bitcoind; -use ln_regtest_rs::ln_client::ClnClient; -use ln_regtest_rs::ln_client::LightningClient; -use ln_regtest_rs::ln_client::LndClient; +use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient}; use ln_regtest_rs::lnd::Lnd; use tokio::sync::Notify; use tower_http::cors::CorsLayer; @@ -136,40 +134,40 @@ pub async fn create_mint( CurrencyUnit, Arc + Sync + Send>, >, + bolt12_ln_backends: HashMap< + CurrencyUnit, + Arc + Sync + Send>, + >, ) -> Result where D: MintDatabase + Send + Sync + 'static, { - let nuts = cdk::nuts::Nuts::new() - .nut07(true) - .nut08(true) - .nut09(true) - .nut10(true) - .nut11(true) - .nut12(true) - .nut14(true); + let mnemonic = Mnemonic::generate(12)?; - let mint_info = MintInfo::new().nuts(nuts); + let mut mint_builder = MintBuilder::new() + .with_localstore(Arc::new(database)) + .with_seed(mnemonic.to_seed_normalized("").to_vec()) + .with_quote_ttl(10000, 10000) + .with_mint_url(get_mint_url()); + + let mint_melt_limits = MintMeltLimits { + mint_min: 1.into(), + mint_max: 10_000.into(), + melt_min: 1.into(), + melt_max: 10_000.into(), + }; - let mnemonic = Mnemonic::generate(12)?; + for (unit, ln_backend) in ln_backends { + println!("11 {}", unit); + mint_builder = mint_builder.add_ln_backend(unit, mint_melt_limits, ln_backend); + } - let mut supported_units: HashMap = HashMap::new(); - supported_units.insert(CurrencyUnit::Sat, (0, 32)); - - let quote_ttl = QuoteTTL::new(10000, 10000); - - let mint = Mint::new( - &get_mint_url(), - &mnemonic.to_seed_normalized(""), - mint_info, - quote_ttl, - Arc::new(database), - ln_backends, - HashMap::new(), - supported_units, - HashMap::new(), - ) - .await?; + for (unit, ln_backend) in bolt12_ln_backends { + println!("12 {}", unit); + mint_builder = mint_builder.add_bolt12_ln_backend(unit, mint_melt_limits, ln_backend); + } + + let mint = mint_builder.build().await?; Ok(mint) } @@ -187,9 +185,18 @@ where Arc + Sync + Send>, > = HashMap::new(); - ln_backends.insert(CurrencyUnit::Sat, Arc::new(cln_backend)); + let cln_arc = Arc::new(cln_backend); + + ln_backends.insert(CurrencyUnit::Sat, cln_arc.clone()); + + let mut bolt12_ln_backends: HashMap< + CurrencyUnit, + Arc + Sync + Send>, + > = HashMap::new(); + + bolt12_ln_backends.insert(CurrencyUnit::Sat, cln_arc.clone()); - let mint = create_mint(database, ln_backends.clone()).await?; + let mint = create_mint(database, ln_backends.clone(), bolt12_ln_backends.clone()).await?; let cache_time_to_live = 3600; let cache_time_to_idle = 3600; let mint_arc = Arc::new(mint); @@ -198,7 +205,7 @@ where Arc::clone(&mint_arc), cache_time_to_live, cache_time_to_idle, - false, + true, ) .await .unwrap(); @@ -216,6 +223,13 @@ where async move { mint.wait_for_paid_invoices(shutdown).await } }); + let mint = Arc::clone(&mint_arc); + + tokio::spawn({ + let shutdown = Arc::clone(&shutdown); + async move { mint.wait_for_paid_offers(shutdown).await } + }); + println!("Staring Axum server"); axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap()) .serve(mint_service.into_make_service()) diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index fc3211cca..60b9bc03c 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -7,6 +7,7 @@ use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::WalletMemoryDatabase; +use cdk::nuts::SecretKey; use cdk::nuts::{ CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload, PreMintSecrets, State, @@ -397,3 +398,29 @@ async fn test_cached_mint() -> Result<()> { assert!(response == response1); Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_bolt12_mint() -> Result<()> { + let cln_one_dir = get_cln_dir("two"); + let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + + let seed = Mnemonic::generate(12)?.to_seed_normalized(""); + + let wallet = Wallet::new( + &get_mint_url(), + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &seed, + None, + )?; + + let secret_key = SecretKey::generate(); + + let q = wallet + .mint_bolt12_quote(None, None, false, None, secret_key.public_key()) + .await?; + + // TODO: Need to update ln-regtest-rs to pay offer and complete this test + + Ok(()) +} diff --git a/crates/cdk/src/cdk_database/mint_memory.rs b/crates/cdk/src/cdk_database/mint_memory.rs index 3b4e1e79e..1cc2d1603 100644 --- a/crates/cdk/src/cdk_database/mint_memory.rs +++ b/crates/cdk/src/cdk_database/mint_memory.rs @@ -371,8 +371,6 @@ impl MintDatabase for MintMemoryDatabase { if let Some(quote_id) = quote_id { let mut current_quote_signatures = self.quote_signatures.write().await; current_quote_signatures.insert(quote_id.clone(), blind_signatures.to_vec()); - let t = current_quote_signatures.get("e_id); - println!("after insert: {:?}", t); } Ok(()) diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 16f99b14c..753ff858f 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -194,12 +194,14 @@ impl MintBuilder { description: true, }; - let mut nut18_settings = self.mint_info.nuts.nut18.unwrap_or_default(); + let mut nut20_settings = self.mint_info.nuts.nut20.unwrap_or_default(); - nut18_settings.methods.push(mint_method_settings); - nut18_settings.disabled = false; + tracing::warn!("{:?}", nut20_settings); - self.mint_info.nuts.nut18 = Some(nut18_settings); + nut20_settings.methods.push(mint_method_settings); + nut20_settings.disabled = false; + + self.mint_info.nuts.nut20 = Some(nut20_settings); } // If the backend supports melting we add it to info signalling @@ -211,11 +213,11 @@ impl MintBuilder { max_amount: Some(limits.melt_max), }; - let mut nut19_settings = self.mint_info.nuts.nut19.unwrap_or_default(); - nut19_settings.methods.push(melt_method_settings); - nut19_settings.disabled = false; + let mut nut21_settings = self.mint_info.nuts.nut21.unwrap_or_default(); + nut21_settings.methods.push(melt_method_settings); + nut21_settings.disabled = false; - self.mint_info.nuts.nut19 = Some(nut19_settings); + self.mint_info.nuts.nut21 = Some(nut21_settings); } ln.insert(unit.clone(), ln_backend); @@ -258,8 +260,7 @@ impl MintBuilder { .clone() .ok_or(anyhow!("Localstore not set"))?, self.ln.clone().ok_or(anyhow!("Ln backends not set"))?, - // TODO: bolt12 - HashMap::new(), + self.bolt12_backends.clone().unwrap_or(HashMap::new()), self.supported_units.clone(), HashMap::new(), ) diff --git a/crates/cdk/src/mint/mint_20.rs b/crates/cdk/src/mint/mint_20.rs index 72fcaa9c9..c65f41c9e 100644 --- a/crates/cdk/src/mint/mint_20.rs +++ b/crates/cdk/src/mint/mint_20.rs @@ -24,7 +24,7 @@ impl Mint { let nut18 = &self .mint_info .nuts - .nut18 + .nut20 .as_ref() .ok_or(Error::UnsupportedUnit)?; @@ -33,7 +33,7 @@ impl Mint { } let ln = self.bolt12_backends.get(&unit).ok_or_else(|| { - tracing::info!("Bolt11 mint request for unsupported unit"); + tracing::info!("Bolt12 mint request for unsupported unit"); Error::UnitUnsupported })?; diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 0202c2d76..2cd65c0a0 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -200,16 +200,8 @@ impl Settings { 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)), - description: true, - }; - Settings { - methods: vec![bolt11_mint], + methods: vec![], disabled: false, } } diff --git a/crates/cdk/src/nuts/nut05.rs b/crates/cdk/src/nuts/nut05.rs index e8236ba8c..86eb1f641 100644 --- a/crates/cdk/src/nuts/nut05.rs +++ b/crates/cdk/src/nuts/nut05.rs @@ -347,15 +347,8 @@ pub struct Settings { 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], + methods: vec![], disabled: false, } } diff --git a/crates/cdk/src/nuts/nut06.rs b/crates/cdk/src/nuts/nut06.rs index 750cf7d60..ce62cec06 100644 --- a/crates/cdk/src/nuts/nut06.rs +++ b/crates/cdk/src/nuts/nut06.rs @@ -238,14 +238,14 @@ pub struct Nuts { /// NUT17 Settings #[cfg(feature = "mint")] pub nut17: Option, - /// NUT04 Settings - #[serde(rename = "18")] + /// NUT20 Settings + #[serde(rename = "20")] #[serde(skip_serializing_if = "Option::is_none")] - pub nut18: Option, - /// NUT05 Settings - #[serde(rename = "19")] + pub nut20: Option, + /// NUT21 Settings + #[serde(rename = "21")] #[serde(skip_serializing_if = "Option::is_none")] - pub nut19: Option, + pub nut21: Option, } impl Nuts { @@ -337,17 +337,17 @@ impl Nuts { } /// Nut18 settings - pub fn nut18(self, nut04_settings: nut04::Settings) -> Self { + pub fn nut20(self, nut04_settings: nut04::Settings) -> Self { Self { - nut18: Some(nut04_settings), + nut20: Some(nut04_settings), ..self } } - /// Nut19 settings - pub fn nut19(self, nut05_settings: nut05::Settings) -> Self { + /// Nut21 settings + pub fn nut21(self, nut05_settings: nut05::Settings) -> Self { Self { - nut19: Some(nut05_settings), + nut21: Some(nut05_settings), ..self } }