mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-04 20:55:59 +01:00
Merge branch 'main' into main
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -6,6 +6,21 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- cdk-common: New `Event` enum for payment event handling with `PaymentReceived` variant ([thesimplekid]).
|
||||
- cdk-common: Added `payment_method` field to `MeltQuote` struct for tracking payment method type ([thesimplekid]).
|
||||
- cdk-sql-common: Database migration to add `payment_method` column to melt_quote table for SQLite and PostgreSQL ([thesimplekid]).
|
||||
|
||||
### Changed
|
||||
- cdk-common: Refactored `MintPayment` trait method `wait_any_incoming_payment` to `wait_payment_event` with event-driven architecture ([thesimplekid]).
|
||||
- cdk-common: Updated `wait_payment_event` return type to stream `Event` enum instead of `WaitPaymentResponse` directly ([thesimplekid]).
|
||||
- cdk: Updated mint payment handling to process payment events through new `Event` enum pattern ([thesimplekid]).
|
||||
- cashu: Updated BOLT12 payment method specification from NUT-24 to NUT-25 ([thesimplekid]).
|
||||
- cdk: Updated BOLT12 import references from nut24 to nut25 module ([thesimplekid]).
|
||||
|
||||
### Fixied
|
||||
- cdk: Wallet melt track and use payment method from quote for BOLT11/BOLT12 routing ([thesimplekid]).
|
||||
|
||||
## [0.12.0](https://github.com/cashubtc/cdk/releases/tag/v0.12.0)
|
||||
|
||||
### Summary
|
||||
|
||||
@@ -87,6 +87,7 @@ gossip_source_type = "rgs"
|
||||
| [21][21] | Clear Authentication | :heavy_check_mark: |
|
||||
| [22][22] | Blind Authentication | :heavy_check_mark: |
|
||||
| [23][23] | Payment Method: BOLT11 | :heavy_check_mark: |
|
||||
| [25][25] | Payment Method: BOLT12 | :heavy_check_mark: |
|
||||
|
||||
|
||||
## License
|
||||
@@ -126,3 +127,4 @@ Please see the [development guide](DEVELOPMENT.md).
|
||||
[21]: https://github.com/cashubtc/nuts/blob/main/21.md
|
||||
[22]: https://github.com/cashubtc/nuts/blob/main/22.md
|
||||
[23]: https://github.com/cashubtc/nuts/blob/main/23.md
|
||||
[25]: https://github.com/cashubtc/nuts/blob/main/25.md
|
||||
|
||||
@@ -16,6 +16,9 @@ pub use self::mint_url::MintUrl;
|
||||
pub use self::nuts::*;
|
||||
pub use self::util::SECP256K1;
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
pub mod quote_id;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! ensure_cdk {
|
||||
@@ -25,107 +28,3 @@ macro_rules! ensure_cdk {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
/// Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility.
|
||||
pub mod quote_id {
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::base64::engine::general_purpose;
|
||||
use bitcoin::base64::Engine as _;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Invalid UUID
|
||||
#[derive(Debug, Error)]
|
||||
pub enum QuoteIdError {
|
||||
/// UUID Error
|
||||
#[error("invalid UUID: {0}")]
|
||||
Uuid(#[from] uuid::Error),
|
||||
/// Invalid base64
|
||||
#[error("invalid base64")]
|
||||
Base64,
|
||||
/// Invalid quote ID
|
||||
#[error("neither a valid UUID nor a valid base64 string")]
|
||||
InvalidQuoteId,
|
||||
}
|
||||
|
||||
/// Mint Quote ID
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
#[serde(untagged)]
|
||||
pub enum QuoteId {
|
||||
/// (Nutshell) base64 quote ID
|
||||
BASE64(String),
|
||||
/// UUID quote ID
|
||||
UUID(Uuid),
|
||||
}
|
||||
|
||||
impl QuoteId {
|
||||
/// Create a new UUID-based MintQuoteId
|
||||
pub fn new_uuid() -> Self {
|
||||
Self::UUID(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for QuoteId {
|
||||
fn from(uuid: Uuid) -> Self {
|
||||
Self::UUID(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for QuoteId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
QuoteId::BASE64(s) => write!(f, "{}", s),
|
||||
QuoteId::UUID(u) => write!(f, "{}", u),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for QuoteId {
|
||||
type Err = QuoteIdError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Try UUID first
|
||||
if let Ok(u) = Uuid::parse_str(s) {
|
||||
return Ok(QuoteId::UUID(u));
|
||||
}
|
||||
|
||||
// Try base64: decode, then re-encode and compare to ensure canonical form
|
||||
// Use the standard (URL/filename safe or standard) depending on your needed alphabet.
|
||||
// Here we use standard base64.
|
||||
match general_purpose::URL_SAFE.decode(s) {
|
||||
Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())),
|
||||
Err(_) => Err(QuoteIdError::InvalidQuoteId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for QuoteId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Deserialize as plain string first
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
// Try UUID first
|
||||
if let Ok(u) = Uuid::parse_str(&s) {
|
||||
return Ok(QuoteId::UUID(u));
|
||||
}
|
||||
|
||||
if general_purpose::URL_SAFE.decode(&s).is_ok() {
|
||||
return Ok(QuoteId::BASE64(s));
|
||||
}
|
||||
|
||||
// Neither matched — return a helpful error
|
||||
Err(de::Error::custom(format!(
|
||||
"QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}",
|
||||
Uuid::nil(),
|
||||
s
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub mod nut18;
|
||||
pub mod nut19;
|
||||
pub mod nut20;
|
||||
pub mod nut23;
|
||||
pub mod nut24;
|
||||
pub mod nut25;
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
mod auth;
|
||||
@@ -68,4 +68,4 @@ pub use nut23::{
|
||||
MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
|
||||
MintQuoteBolt11Response, QuoteState as MintQuoteState,
|
||||
};
|
||||
pub use nut24::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||
pub use nut25::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! <https://github.com/cashubtc/nuts/blob/main/13.md>
|
||||
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
|
||||
use bitcoin::secp256k1::hashes::{hmac, sha512, Hash, HashEngine, HmacEngine};
|
||||
use bitcoin::secp256k1::hashes::{hmac, sha256, Hash, HashEngine, HmacEngine};
|
||||
use bitcoin::{secp256k1, Network};
|
||||
use thiserror::Error;
|
||||
use tracing::instrument;
|
||||
@@ -66,14 +66,14 @@ impl Secret {
|
||||
|
||||
fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
|
||||
let mut message = Vec::new();
|
||||
message.extend_from_slice(b"Cashu_KDF_HMAC_SHA512");
|
||||
message.extend_from_slice(b"Cashu_KDF_HMAC_SHA256");
|
||||
message.extend_from_slice(&keyset_id.to_bytes());
|
||||
message.extend_from_slice(&(counter as u64).to_be_bytes());
|
||||
message.extend_from_slice(b"\x00");
|
||||
|
||||
let mut engine = HmacEngine::<sha512::Hash>::new(seed);
|
||||
let mut engine = HmacEngine::<sha256::Hash>::new(seed);
|
||||
engine.input(&message);
|
||||
let hmac_result = hmac::Hmac::<sha512::Hash>::from_engine(engine);
|
||||
let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
|
||||
let result_bytes = hmac_result.to_byte_array();
|
||||
|
||||
Ok(Self::new(hex::encode(&result_bytes[..32])))
|
||||
@@ -101,14 +101,14 @@ impl SecretKey {
|
||||
|
||||
fn derive(seed: &[u8; 64], keyset_id: Id, counter: u32) -> Result<Self, Error> {
|
||||
let mut message = Vec::new();
|
||||
message.extend_from_slice(b"Cashu_KDF_HMAC_SHA512");
|
||||
message.extend_from_slice(b"Cashu_KDF_HMAC_SHA256");
|
||||
message.extend_from_slice(&keyset_id.to_bytes());
|
||||
message.extend_from_slice(&(counter as u64).to_be_bytes());
|
||||
message.extend_from_slice(b"\x01");
|
||||
|
||||
let mut engine = HmacEngine::<sha512::Hash>::new(seed);
|
||||
let mut engine = HmacEngine::<sha256::Hash>::new(seed);
|
||||
engine.input(&message);
|
||||
let hmac_result = hmac::Hmac::<sha512::Hash>::from_engine(engine);
|
||||
let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
|
||||
let result_bytes = hmac_result.to_byte_array();
|
||||
|
||||
Ok(Self::from(secp256k1::SecretKey::from_slice(
|
||||
@@ -316,26 +316,26 @@ mod tests {
|
||||
|
||||
// Test with a v2 keyset ID (33 bytes, starting with "01")
|
||||
let keyset_id =
|
||||
Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
|
||||
Id::from_str("012e23479a0029432eaad0d2040c09be53bab592d5cbf1d55e0dd26c9495951b30")
|
||||
.unwrap();
|
||||
|
||||
// Expected secrets derived using the new derivation
|
||||
let test_secrets = [
|
||||
"f24ca2e4e5c8e1e8b43e3d0d9e9d4c2a1b6a5e9f8c7b3d2e1f0a9b8c7d6e5f4a",
|
||||
"8b7e5f9a4d3c2b1e7f6a5d9c8b4e3f2a6b5c9d8e7f4a3b2e1f5a9c8d7b6e4f3",
|
||||
"e9f8c7b6a5d4c3b2a1f9e8d7c6b5a4d3c2b1f0e9d8c7b6a5f4e3d2c1b0a9f8e7",
|
||||
"a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2",
|
||||
"d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6",
|
||||
"ba250bf927b1df5dd0a07c543be783a4349a7f99904acd3406548402d3484118",
|
||||
"3a6423fe56abd5e74ec9d22a91ee110cd2ce45a7039901439d62e5534d3438c1",
|
||||
"843484a75b78850096fac5b513e62854f11d57491cf775a6fd2edf4e583ae8c0",
|
||||
"3600608d5cf8197374f060cfbcff134d2cd1fb57eea68cbcf2fa6917c58911b6",
|
||||
"717fce9cc6f9ea060d20dd4e0230af4d63f3894cc49dd062fd99d033ea1ac1dd",
|
||||
];
|
||||
|
||||
for (i, _test_secret) in test_secrets.iter().enumerate() {
|
||||
for (i, test_secret) in test_secrets.iter().enumerate() {
|
||||
let secret = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
|
||||
// Note: The actual expected values would need to be computed from a reference implementation
|
||||
// For now, we just verify the derivation works and produces consistent results
|
||||
assert_eq!(secret.to_string().len(), 64); // Should be 32 bytes = 64 hex chars
|
||||
|
||||
// Test deterministic derivation: same inputs should produce same outputs
|
||||
let secret2 = Secret::from_seed(&seed, keyset_id, i.try_into().unwrap()).unwrap();
|
||||
let secret2 = Secret::from_str(test_secret).unwrap();
|
||||
assert_eq!(secret, secret2);
|
||||
}
|
||||
}
|
||||
@@ -349,18 +349,26 @@ mod tests {
|
||||
|
||||
// Test with a v2 keyset ID (33 bytes, starting with "01")
|
||||
let keyset_id =
|
||||
Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
|
||||
Id::from_str("012e23479a0029432eaad0d2040c09be53bab592d5cbf1d55e0dd26c9495951b30")
|
||||
.unwrap();
|
||||
|
||||
for i in 0..5 {
|
||||
let secret_key = SecretKey::from_seed(&seed, keyset_id, i).unwrap();
|
||||
let test_secret_keys = [
|
||||
"4f8b32a54aed811b692a665ed296b4c1fc2f37a8be4006379e95063a76693745",
|
||||
"c4b8412ee644067007423480c9e556385b71ffdff0f340bc16a95c0534fe0e01",
|
||||
"ceff40983441c40acaf77d2a8ddffd5c1c84391fb9fd0dc4607c186daab1c829",
|
||||
"41ad26b840fb62d29b2318a82f1d9cd40dc0f1e58183cc57562f360a32fdfad6",
|
||||
"fb986a9c76758593b0e2d1a5172ade977c858d87111a220e16c292a9347abf81",
|
||||
];
|
||||
|
||||
for (i, test_secret) in test_secret_keys.iter().enumerate() {
|
||||
let secret_key = SecretKey::from_seed(&seed, keyset_id, i as u32).unwrap();
|
||||
|
||||
// Verify the secret key is valid (32 bytes)
|
||||
let secret_bytes = secret_key.secret_bytes();
|
||||
assert_eq!(secret_bytes.len(), 32);
|
||||
|
||||
// Test deterministic derivation
|
||||
let secret_key2 = SecretKey::from_seed(&seed, keyset_id, i).unwrap();
|
||||
let secret_key2 = SecretKey::from_str(test_secret).unwrap();
|
||||
assert_eq!(secret_key, secret_key2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ mod tests {
|
||||
assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
|
||||
|
||||
let t = request.transports.first().clone().unwrap();
|
||||
let t = request.transports.first().unwrap();
|
||||
assert_eq!(&transport, t);
|
||||
|
||||
// Test serialization and deserialization
|
||||
|
||||
100
crates/cashu/src/quote_id.rs
Normal file
100
crates/cashu/src/quote_id.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
//! Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility.
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::base64::engine::general_purpose;
|
||||
use bitcoin::base64::Engine as _;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Invalid UUID
|
||||
#[derive(Debug, Error)]
|
||||
pub enum QuoteIdError {
|
||||
/// UUID Error
|
||||
#[error("invalid UUID: {0}")]
|
||||
Uuid(#[from] uuid::Error),
|
||||
/// Invalid base64
|
||||
#[error("invalid base64")]
|
||||
Base64,
|
||||
/// Invalid quote ID
|
||||
#[error("neither a valid UUID nor a valid base64 string")]
|
||||
InvalidQuoteId,
|
||||
}
|
||||
|
||||
/// Mint Quote ID
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
#[serde(untagged)]
|
||||
pub enum QuoteId {
|
||||
/// (Nutshell) base64 quote ID
|
||||
BASE64(String),
|
||||
/// UUID quote ID
|
||||
UUID(Uuid),
|
||||
}
|
||||
|
||||
impl QuoteId {
|
||||
/// Create a new UUID-based MintQuoteId
|
||||
pub fn new_uuid() -> Self {
|
||||
Self::UUID(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for QuoteId {
|
||||
fn from(uuid: Uuid) -> Self {
|
||||
Self::UUID(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for QuoteId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
QuoteId::BASE64(s) => write!(f, "{}", s),
|
||||
QuoteId::UUID(u) => write!(f, "{}", u.hyphenated()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for QuoteId {
|
||||
type Err = QuoteIdError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Try UUID first
|
||||
if let Ok(u) = Uuid::parse_str(s) {
|
||||
return Ok(QuoteId::UUID(u));
|
||||
}
|
||||
|
||||
// Try base64: decode, then re-encode and compare to ensure canonical form
|
||||
// Use the standard (URL/filename safe or standard) depending on your needed alphabet.
|
||||
// Here we use standard base64.
|
||||
match general_purpose::URL_SAFE.decode(s) {
|
||||
Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())),
|
||||
Err(_) => Err(QuoteIdError::InvalidQuoteId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for QuoteId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Deserialize as plain string first
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
// Try UUID first
|
||||
if let Ok(u) = Uuid::parse_str(&s) {
|
||||
return Ok(QuoteId::UUID(u));
|
||||
}
|
||||
|
||||
if general_purpose::URL_SAFE.decode(&s).is_ok() {
|
||||
return Ok(QuoteId::BASE64(s));
|
||||
}
|
||||
|
||||
// Neither matched — return a helpful error
|
||||
Err(de::Error::custom(format!(
|
||||
"QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}",
|
||||
Uuid::nil(),
|
||||
s
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use cdk_common::common::FeeReserve;
|
||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||
use cdk_common::payment::{
|
||||
self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
|
||||
CreateIncomingPaymentResponse, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
|
||||
CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
|
||||
OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, WaitPaymentResponse,
|
||||
};
|
||||
use cdk_common::util::{hex, unix_time};
|
||||
@@ -89,9 +89,9 @@ impl MintPayment for Cln {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
|
||||
tracing::info!(
|
||||
"CLN: Starting wait_any_incoming_payment with socket: {:?}",
|
||||
self.rpc_socket
|
||||
@@ -243,8 +243,9 @@ impl MintPayment for Cln {
|
||||
payment_id: payment_hash.to_string()
|
||||
};
|
||||
tracing::info!("CLN: Created WaitPaymentResponse with amount {} msats", amount_msats.msat());
|
||||
let event = Event::PaymentReceived(response);
|
||||
|
||||
break Some((response, (cln_client, last_pay_idx, cancel_token, is_active)));
|
||||
break Some((event, (cln_client, last_pay_idx, cancel_token, is_active)));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("CLN: Error fetching invoice: {e}");
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::collections::HashMap;
|
||||
use async_trait::async_trait;
|
||||
use cashu::quote_id::QuoteId;
|
||||
use cashu::{Amount, MintInfo};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::Error;
|
||||
use crate::common::QuoteTTL;
|
||||
@@ -89,7 +88,7 @@ pub trait QuotesTransaction<'a> {
|
||||
/// Get [`mint::MeltQuote`] and lock it for update in this transaction
|
||||
async fn get_melt_quote(
|
||||
&mut self,
|
||||
quote_id: &Uuid,
|
||||
quote_id: &QuoteId,
|
||||
) -> Result<Option<mint::MeltQuote>, Self::Err>;
|
||||
/// Add [`mint::MeltQuote`]
|
||||
async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err>;
|
||||
@@ -111,7 +110,7 @@ pub trait QuotesTransaction<'a> {
|
||||
payment_proof: Option<String>,
|
||||
) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>;
|
||||
/// Remove [`mint::MeltQuote`]
|
||||
async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
|
||||
async fn remove_melt_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err>;
|
||||
/// Get all [`MintMintQuote`]s and lock it for update in this transaction
|
||||
async fn get_mint_quote_by_request(
|
||||
&mut self,
|
||||
@@ -165,7 +164,11 @@ pub trait ProofsTransaction<'a> {
|
||||
///
|
||||
/// Adds proofs to the database. The database should error if the proof already exits, with a
|
||||
/// `AttemptUpdateSpentProof` if the proof is already spent or a `Duplicate` error otherwise.
|
||||
async fn add_proofs(&mut self, proof: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err>;
|
||||
async fn add_proofs(
|
||||
&mut self,
|
||||
proof: Proofs,
|
||||
quote_id: Option<QuoteId>,
|
||||
) -> Result<(), Self::Err>;
|
||||
/// Updates the proofs to a given states and return the previous states
|
||||
async fn update_proofs_states(
|
||||
&mut self,
|
||||
@@ -177,7 +180,7 @@ pub trait ProofsTransaction<'a> {
|
||||
async fn remove_proofs(
|
||||
&mut self,
|
||||
ys: &[PublicKey],
|
||||
quote_id: Option<Uuid>,
|
||||
quote_id: Option<QuoteId>,
|
||||
) -> Result<(), Self::Err>;
|
||||
}
|
||||
|
||||
@@ -190,7 +193,10 @@ pub trait ProofsDatabase {
|
||||
/// Get [`Proofs`] by ys
|
||||
async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err>;
|
||||
/// Get ys by quote id
|
||||
async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err>;
|
||||
async fn get_proof_ys_by_quote_id(
|
||||
&self,
|
||||
quote_id: &QuoteId,
|
||||
) -> Result<Vec<PublicKey>, Self::Err>;
|
||||
/// Get [`Proofs`] state
|
||||
async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err>;
|
||||
/// Get [`Proofs`] by state
|
||||
|
||||
@@ -87,7 +87,7 @@ where
|
||||
{
|
||||
let keyset_id = setup_keyset(&db).await;
|
||||
|
||||
let quote_id = Uuid::max();
|
||||
let quote_id = QuoteId::new_uuid();
|
||||
|
||||
let proofs = vec![
|
||||
Proof {
|
||||
@@ -110,7 +110,9 @@ where
|
||||
|
||||
// Add proofs to database
|
||||
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||
tx.add_proofs(proofs.clone(), Some(quote_id)).await.unwrap();
|
||||
tx.add_proofs(proofs.clone(), Some(quote_id.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(tx.commit().await.is_ok());
|
||||
|
||||
let proofs_from_db = db.get_proofs_by_ys(&[proofs[0].c, proofs[1].c]).await;
|
||||
|
||||
@@ -295,9 +295,9 @@ pub trait MintPayment {
|
||||
|
||||
/// Listen for invoices to be paid to the mint
|
||||
/// Returns a stream of request_lookup_id once invoices are paid
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err>;
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
|
||||
|
||||
/// Is wait invoice active
|
||||
fn is_wait_invoice_active(&self) -> bool;
|
||||
@@ -318,6 +318,13 @@ pub trait MintPayment {
|
||||
) -> Result<MakePaymentResponse, Self::Err>;
|
||||
}
|
||||
|
||||
/// An event emitted which should be handled by the mint
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum Event {
|
||||
/// A payment has been received.
|
||||
PaymentReceived(WaitPaymentResponse),
|
||||
}
|
||||
|
||||
/// Wait any invoice response
|
||||
#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
|
||||
pub struct WaitPaymentResponse {
|
||||
|
||||
@@ -84,6 +84,9 @@ pub struct MeltQuote {
|
||||
pub expiry: u64,
|
||||
/// Payment preimage
|
||||
pub payment_preimage: Option<String>,
|
||||
/// Payment method
|
||||
#[serde(default)]
|
||||
pub payment_method: PaymentMethod,
|
||||
}
|
||||
|
||||
impl MintQuote {
|
||||
|
||||
@@ -27,7 +27,7 @@ use cdk_common::common::FeeReserve;
|
||||
use cdk_common::ensure_cdk;
|
||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||
use cdk_common::payment::{
|
||||
self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
|
||||
self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
|
||||
MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
|
||||
PaymentQuoteResponse, WaitPaymentResponse,
|
||||
};
|
||||
@@ -295,9 +295,9 @@ impl MintPayment for FakeWallet {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
|
||||
tracing::info!("Starting stream for fake invoices");
|
||||
let receiver = self
|
||||
.receiver
|
||||
@@ -309,11 +309,14 @@ impl MintPayment for FakeWallet {
|
||||
let unit = self.unit.clone();
|
||||
let receiver_stream = ReceiverStream::new(receiver);
|
||||
Ok(Box::pin(receiver_stream.map(
|
||||
move |(request_lookup_id, payment_amount, payment_id)| WaitPaymentResponse {
|
||||
payment_identifier: request_lookup_id.clone(),
|
||||
payment_amount,
|
||||
unit: unit.clone(),
|
||||
payment_id,
|
||||
move |(request_lookup_id, payment_amount, payment_id)| {
|
||||
let wait_response = WaitPaymentResponse {
|
||||
payment_identifier: request_lookup_id.clone(),
|
||||
payment_amount,
|
||||
unit: unit.clone(),
|
||||
payment_id,
|
||||
};
|
||||
Event::PaymentReceived(wait_response)
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use bip39::Mnemonic;
|
||||
use cashu::amount::SplitTarget;
|
||||
use cashu::nut23::Amountless;
|
||||
use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods};
|
||||
use cdk::wallet::{HttpClient, MintConnector, Wallet};
|
||||
use cashu::{Amount, CurrencyUnit, MintRequest, MintUrl, PreMintSecrets, ProofsMethods};
|
||||
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletBuilder};
|
||||
use cdk_integration_tests::get_mint_url_from_env;
|
||||
use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
|
||||
use cdk_sqlite::wallet::memory;
|
||||
@@ -97,13 +98,16 @@ async fn test_regtest_bolt12_mint() {
|
||||
/// - Tests the functionality of reusing a quote for multiple payments
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
&get_mint_url_from_env(),
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(memory::empty().await?),
|
||||
Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
let mint_url = MintUrl::from_str(&get_mint_url_from_env())?;
|
||||
|
||||
let wallet = WalletBuilder::new()
|
||||
.mint_url(mint_url)
|
||||
.unit(CurrencyUnit::Sat)
|
||||
.localstore(Arc::new(memory::empty().await?))
|
||||
.seed(Mnemonic::generate(12)?.to_seed_normalized(""))
|
||||
.target_proof_count(3)
|
||||
.use_http_subscription()
|
||||
.build()?;
|
||||
|
||||
let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
|
||||
|
||||
@@ -120,7 +124,7 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -136,7 +140,7 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -187,7 +191,7 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
|
||||
quote_one.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -206,7 +210,7 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
|
||||
quote_two.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -283,7 +287,7 @@ async fn test_regtest_bolt12_melt() -> Result<()> {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ async fn test_mint_with_auth() {
|
||||
quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
|
||||
@@ -114,7 +114,7 @@ async fn test_happy_mint_melt_round_trip() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -236,7 +236,7 @@ async fn test_happy_mint() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -284,7 +284,7 @@ async fn test_restore() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -364,7 +364,7 @@ async fn test_fake_melt_change_in_quote() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -434,7 +434,7 @@ async fn test_pay_invoice_twice() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
|
||||
@@ -10,7 +10,7 @@ async fn test_ldk_node_mint_info() -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Make a request to the info endpoint
|
||||
let response = client.get(&format!("{}/v1/info", mint_url)).send().await?;
|
||||
let response = client.get(format!("{}/v1/info", mint_url)).send().await?;
|
||||
|
||||
// Check that we got a successful response
|
||||
assert_eq!(response.status(), 200);
|
||||
@@ -44,7 +44,7 @@ async fn test_ldk_node_mint_quote() -> Result<()> {
|
||||
|
||||
// Make a request to create a mint quote
|
||||
let response = client
|
||||
.post(&format!("{}/v1/mint/quote/bolt11", mint_url))
|
||||
.post(format!("{}/v1/mint/quote/bolt11", mint_url))
|
||||
.json("e_request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -56,7 +56,7 @@ async fn test_internal_payment() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -88,7 +88,7 @@ async fn test_internal_payment() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -236,7 +236,7 @@ async fn test_multimint_melt() {
|
||||
quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -252,7 +252,7 @@ async fn test_multimint_melt() {
|
||||
quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
|
||||
@@ -32,7 +32,7 @@ async fn test_swap() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
@@ -92,7 +92,7 @@ async fn test_fake_melt_change_in_quote() {
|
||||
mint_quote.clone(),
|
||||
SplitTarget::default(),
|
||||
None,
|
||||
tokio::time::Duration::from_secs(15),
|
||||
tokio::time::Duration::from_secs(60),
|
||||
)
|
||||
.await
|
||||
.expect("payment");
|
||||
|
||||
@@ -15,7 +15,7 @@ async-trait.workspace = true
|
||||
axum.workspace = true
|
||||
cdk-common = { workspace = true, features = ["mint"] }
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -29,6 +29,3 @@ tower-http.workspace = true
|
||||
rust-embed = "8.5.0"
|
||||
serde_urlencoded = "0.7"
|
||||
urlencoding = "2.1"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -823,9 +823,9 @@ impl MintPayment for CdkLdkNode {
|
||||
/// Listen for invoices to be paid to the mint
|
||||
/// Returns a stream of request_lookup_id once invoices are paid
|
||||
#[instrument(skip(self))]
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
|
||||
tracing::info!("Starting stream for invoices - wait_any_incoming_payment called");
|
||||
|
||||
// Set active flag to indicate stream is active
|
||||
@@ -839,10 +839,10 @@ impl MintPayment for CdkLdkNode {
|
||||
// Transform the String stream into a WaitPaymentResponse stream
|
||||
let response_stream = BroadcastStream::new(receiver.resubscribe());
|
||||
|
||||
// Map the stream to handle BroadcastStreamRecvError
|
||||
// Map the stream to handle BroadcastStreamRecvError and wrap in Event
|
||||
let response_stream = response_stream.filter_map(|result| async move {
|
||||
match result {
|
||||
Ok(payment) => Some(payment),
|
||||
Ok(payment) => Some(cdk_common::payment::Event::PaymentReceived(payment)),
|
||||
Err(err) => {
|
||||
tracing::warn!("Error in broadcast stream: {}", err);
|
||||
None
|
||||
|
||||
@@ -213,7 +213,7 @@ pub async fn post_open_channel(
|
||||
}
|
||||
|
||||
pub async fn close_channel_page(
|
||||
State(_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
query: Query<HashMap<String, String>>,
|
||||
) -> Result<Html<String>, StatusCode> {
|
||||
let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
|
||||
@@ -229,24 +229,40 @@ pub async fn close_channel_page(
|
||||
return Ok(Html(layout("Close Channel Error", content).into_string()));
|
||||
}
|
||||
|
||||
// Get channel information for amount display
|
||||
let channels = state.node.inner.list_channels();
|
||||
let channel = channels
|
||||
.iter()
|
||||
.find(|c| c.user_channel_id.0.to_string() == channel_id);
|
||||
|
||||
let content = form_card(
|
||||
"Close Channel",
|
||||
html! {
|
||||
p { "Are you sure you want to close this channel?" }
|
||||
div class="info-item" {
|
||||
span class="info-label" { "User Channel ID:" }
|
||||
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) }
|
||||
p style="margin-bottom: 1.5rem;" { "Are you sure you want to close this channel?" }
|
||||
|
||||
// Channel details in consistent format
|
||||
div class="channel-details" {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "User Channel ID" }
|
||||
span class="detail-value-amount" { (channel_id) }
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Node ID" }
|
||||
span class="detail-value-amount" { (node_id) }
|
||||
}
|
||||
@if let Some(ch) = channel {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Channel Amount" }
|
||||
span class="detail-value-amount" { (format_sats_as_btc(ch.channel_value_sats)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="info-item" {
|
||||
span class="info-label" { "Node ID:" }
|
||||
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
|
||||
}
|
||||
form method="post" action="/channels/close" style="margin-top: 1rem;" {
|
||||
|
||||
form method="post" action="/channels/close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
|
||||
input type="hidden" name="channel_id" value=(channel_id) {}
|
||||
input type="hidden" name="node_id" value=(node_id) {}
|
||||
button type="submit" style="background: #dc3545;" { "Close Channel" }
|
||||
" "
|
||||
a href="/balance" { button type="button" { "Cancel" } }
|
||||
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button type="submit" class="button-destructive" { "Close Channel" }
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -255,7 +271,7 @@ pub async fn close_channel_page(
|
||||
}
|
||||
|
||||
pub async fn force_close_channel_page(
|
||||
State(_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
query: Query<HashMap<String, String>>,
|
||||
) -> Result<Html<String>, StatusCode> {
|
||||
let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
|
||||
@@ -273,32 +289,48 @@ pub async fn force_close_channel_page(
|
||||
));
|
||||
}
|
||||
|
||||
// Get channel information for amount display
|
||||
let channels = state.node.inner.list_channels();
|
||||
let channel = channels
|
||||
.iter()
|
||||
.find(|c| c.user_channel_id.0.to_string() == channel_id);
|
||||
|
||||
let content = form_card(
|
||||
"Force Close Channel",
|
||||
html! {
|
||||
div style="border: 2px solid #d63384; background-color: rgba(214, 51, 132, 0.1); padding: 1rem; margin-bottom: 1rem; border-radius: 0.5rem;" {
|
||||
h4 style="color: #d63384; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
|
||||
p style="color: #d63384; margin: 0; font-size: 0.9rem;" {
|
||||
div style="border: 2px solid #f97316; background-color: rgba(249, 115, 22, 0.1); padding: 1rem; margin-bottom: 1rem; border-radius: 0.5rem;" {
|
||||
h4 style="color: #f97316; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
|
||||
p style="color: #f97316; margin: 0; font-size: 0.9rem;" {
|
||||
"Force close should NOT be used if normal close is preferred. "
|
||||
"Force close will immediately broadcast the latest commitment transaction and may result in delayed fund recovery. "
|
||||
"Only use this if the channel counterparty is unresponsive or there are other issues preventing normal closure."
|
||||
}
|
||||
}
|
||||
p { "Are you sure you want to force close this channel?" }
|
||||
div class="info-item" {
|
||||
span class="info-label" { "User Channel ID:" }
|
||||
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) }
|
||||
p style="margin-bottom: 1.5rem;" { "Are you sure you want to force close this channel?" }
|
||||
|
||||
// Channel details in consistent format
|
||||
div class="channel-details" {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "User Channel ID" }
|
||||
span class="detail-value-amount" { (channel_id) }
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Node ID" }
|
||||
span class="detail-value-amount" { (node_id) }
|
||||
}
|
||||
@if let Some(ch) = channel {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Channel Amount" }
|
||||
span class="detail-value-amount" { (format_sats_as_btc(ch.channel_value_sats)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="info-item" {
|
||||
span class="info-label" { "Node ID:" }
|
||||
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
|
||||
}
|
||||
form method="post" action="/channels/force-close" style="margin-top: 1rem;" {
|
||||
|
||||
form method="post" action="/channels/force-close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
|
||||
input type="hidden" name="channel_id" value=(channel_id) {}
|
||||
input type="hidden" name="node_id" value=(node_id) {}
|
||||
button type="submit" style="background: #d63384;" { "Force Close Channel" }
|
||||
" "
|
||||
a href="/balance" { button type="button" { "Cancel" } }
|
||||
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button type="submit" class="button-destructive" { "Force Close Channel" }
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::http::StatusCode;
|
||||
use axum::response::Html;
|
||||
use maud::html;
|
||||
|
||||
use crate::web::handlers::AppState;
|
||||
use crate::web::handlers::utils::AppState;
|
||||
use crate::web::templates::{format_sats_as_btc, layout};
|
||||
|
||||
pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||
@@ -26,18 +26,35 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
|
||||
|
||||
// Quick Actions section - matching dashboard style
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
|
||||
a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Open Channel" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Open Channel Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning Network channel to connect with another node." }
|
||||
a href="/channels/open" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Open Channel" }
|
||||
}
|
||||
}
|
||||
a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Create Invoice" }
|
||||
|
||||
// Create Invoice Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments from other users or services." }
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Create Invoice" }
|
||||
}
|
||||
}
|
||||
a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
|
||||
|
||||
// Make Payment Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices. BOLT 11 & 12 supported." }
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Make Payment" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,18 +90,35 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
|
||||
|
||||
// Quick Actions section - matching dashboard style
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
|
||||
a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Open Channel" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Open Channel Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning channel by connecting with another node." }
|
||||
a href="/channels/open" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Open Channel" }
|
||||
}
|
||||
}
|
||||
a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Create Invoice" }
|
||||
|
||||
// Create Invoice Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments." }
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Create Invoice" }
|
||||
}
|
||||
}
|
||||
a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
|
||||
|
||||
// Make Payment Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices." }
|
||||
a href="/payments/send" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Make Payment" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,57 +146,72 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
}
|
||||
}
|
||||
|
||||
div class="card" {
|
||||
h2 { "Channel Details" }
|
||||
// Channel Details header (outside card)
|
||||
h2 class="section-header" { "Channel Details" }
|
||||
|
||||
// Channels list
|
||||
@for channel in &channels {
|
||||
div class="channel-item" {
|
||||
div class="channel-header" {
|
||||
span class="channel-id" { "Channel ID: " (channel.channel_id.to_string()) }
|
||||
// Channels list
|
||||
@for (index, channel) in channels.iter().enumerate() {
|
||||
@let node_id = channel.counterparty_node_id.to_string();
|
||||
@let channel_number = index + 1;
|
||||
|
||||
div class="channel-box" {
|
||||
// Channel number as prominent header
|
||||
div class="channel-alias" { (format!("Channel {}", channel_number)) }
|
||||
|
||||
// Channel details in left-aligned format
|
||||
div class="channel-details" {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Channel ID" }
|
||||
span class="detail-value-amount" { (channel.channel_id.to_string()) }
|
||||
}
|
||||
@if let Some(short_channel_id) = channel.short_channel_id {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Short Channel ID" }
|
||||
span class="detail-value-amount" { (short_channel_id.to_string()) }
|
||||
}
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Node ID" }
|
||||
span class="detail-value-amount" { (node_id) }
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Status" }
|
||||
@if channel.is_usable {
|
||||
span class="status-badge status-active" { "Active" }
|
||||
} @else {
|
||||
span class="status-badge status-inactive" { "Inactive" }
|
||||
}
|
||||
}
|
||||
div class="info-item" {
|
||||
span class="info-label" { "Counterparty" }
|
||||
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel.counterparty_node_id.to_string()) }
|
||||
}
|
||||
|
||||
// Balance information cards (keeping existing style)
|
||||
div class="balance-info" {
|
||||
div class="balance-item" {
|
||||
div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
|
||||
div class="balance-label" { "Outbound" }
|
||||
}
|
||||
@if let Some(short_channel_id) = channel.short_channel_id {
|
||||
div class="info-item" {
|
||||
span class="info-label" { "Short Channel ID" }
|
||||
span class="info-value" { (short_channel_id.to_string()) }
|
||||
}
|
||||
div class="balance-item" {
|
||||
div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
|
||||
div class="balance-label" { "Inbound" }
|
||||
}
|
||||
div class="balance-info" {
|
||||
div class="balance-item" {
|
||||
div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
|
||||
div class="balance-label" { "Outbound" }
|
||||
}
|
||||
div class="balance-item" {
|
||||
div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
|
||||
div class="balance-label" { "Inbound" }
|
||||
}
|
||||
div class="balance-item" {
|
||||
div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
|
||||
div class="balance-label" { "Total" }
|
||||
}
|
||||
div class="balance-item" {
|
||||
div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
|
||||
div class="balance-label" { "Total" }
|
||||
}
|
||||
@if channel.is_usable {
|
||||
div style="margin-top: 1rem; display: flex; gap: 0.5rem;" {
|
||||
a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
|
||||
button style="background: #dc3545;" { "Close Channel" }
|
||||
}
|
||||
a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
|
||||
button style="background: #d63384;" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
@if channel.is_usable {
|
||||
div class="channel-actions" {
|
||||
a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
|
||||
button class="button-secondary" { "Close Channel" }
|
||||
}
|
||||
a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
|
||||
button class="button-destructive" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,15 +79,26 @@ pub async fn onchain_page(
|
||||
let mut content = html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||
|
||||
// Quick Actions section - matching dashboard style
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
|
||||
a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Receive Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Receive Bitcoin" }
|
||||
}
|
||||
}
|
||||
a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
|
||||
|
||||
// Send Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Send Bitcoin" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,15 +124,26 @@ pub async fn onchain_page(
|
||||
content = html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||
|
||||
// Quick Actions section - matching dashboard style
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
|
||||
a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Receive Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Receive Bitcoin" }
|
||||
}
|
||||
}
|
||||
a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
|
||||
|
||||
// Send Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Send Bitcoin" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +163,7 @@ pub async fn onchain_page(
|
||||
}
|
||||
input type="hidden" id="send_action" name="send_action" value="send" {}
|
||||
div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
|
||||
a href="/onchain" { button type="button" { "Cancel" } }
|
||||
a href="/onchain" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
button type="submit" onclick="document.getElementById('send_action').value='send'" { "Send Payment" }
|
||||
button type="submit" onclick="document.getElementById('send_action').value='send_all'; document.getElementById('amount_sat').value=''" { "Send All" }
|
||||
@@ -171,15 +193,26 @@ pub async fn onchain_page(
|
||||
content = html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||
|
||||
// Quick Actions section - matching dashboard style
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
|
||||
a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Receive Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Receive Bitcoin" }
|
||||
}
|
||||
}
|
||||
a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
|
||||
button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
|
||||
|
||||
// Send Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Send Bitcoin" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +224,7 @@ pub async fn onchain_page(
|
||||
form method="post" action="/onchain/new-address" {
|
||||
p style="margin-bottom: 2rem;" { "Click the button below to generate a new Bitcoin address for receiving on-chain payments." }
|
||||
div style="display: flex; justify-content: space-between; gap: 1rem;" {
|
||||
a href="/onchain" { button type="button" { "Cancel" } }
|
||||
a href="/onchain" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button class="button-primary" type="submit" { "Generate New Address" }
|
||||
}
|
||||
}
|
||||
@@ -345,7 +378,7 @@ pub async fn onchain_confirm_page(
|
||||
div class="card" {
|
||||
div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
|
||||
a href="/onchain?action=send" {
|
||||
button type="button" class="button-secondary" { "← Cancel" }
|
||||
button type="button" class="button-secondary" { "Cancel" }
|
||||
}
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
a href=(confirmation_url) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ pub fn payment_list_item(
|
||||
let status_class = match status {
|
||||
"Succeeded" => "status-active",
|
||||
"Failed" => "status-inactive",
|
||||
"Pending" => "status-badge",
|
||||
"Pending" => "status-pending",
|
||||
_ => "status-badge",
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 43 KiB |
@@ -15,7 +15,7 @@ use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
|
||||
use cdk_common::common::FeeReserve;
|
||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||
use cdk_common::payment::{
|
||||
self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
|
||||
self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
|
||||
MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
|
||||
PaymentQuoteResponse, WaitPaymentResponse,
|
||||
};
|
||||
@@ -155,9 +155,9 @@ impl MintPayment for LNbits {
|
||||
self.wait_invoice_cancel_token.cancel()
|
||||
}
|
||||
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
|
||||
let api = self.lnbits_api.clone();
|
||||
let cancel_token = self.wait_invoice_cancel_token.clone();
|
||||
let is_active = Arc::clone(&self.wait_invoice_is_active);
|
||||
@@ -179,7 +179,7 @@ impl MintPayment for LNbits {
|
||||
msg_option = receiver.recv() => {
|
||||
Self::process_message(msg_option, &api, &is_active)
|
||||
.await
|
||||
.map(|response| (response, (api, cancel_token, is_active)))
|
||||
.map(|response| (Event::PaymentReceived(response), (api, cancel_token, is_active)))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ use cdk_common::bitcoin::hashes::Hash;
|
||||
use cdk_common::common::FeeReserve;
|
||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||
use cdk_common::payment::{
|
||||
self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
|
||||
self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
|
||||
MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
|
||||
PaymentQuoteResponse, WaitPaymentResponse,
|
||||
};
|
||||
@@ -137,9 +137,9 @@ impl MintPayment for Lnd {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
|
||||
let mut lnd_client = self.lnd_client.clone();
|
||||
|
||||
let stream_req = lnrpc::InvoiceSubscription {
|
||||
@@ -195,7 +195,8 @@ impl MintPayment for Lnd {
|
||||
};
|
||||
tracing::info!("LND: Created WaitPaymentResponse with amount {} msat",
|
||||
msg.amt_paid_msat);
|
||||
Some((wait_response, (stream, cancel_token, is_active)))
|
||||
let event = Event::PaymentReceived(wait_response);
|
||||
Some((event, (stream, cancel_token, is_active)))
|
||||
} else { None }
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -263,9 +263,9 @@ impl MintPayment for PaymentProcessorClient {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn wait_any_incoming_payment(
|
||||
async fn wait_payment_event(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||
) -> Result<Pin<Box<dyn Stream<Item = cdk_common::payment::Event> + Send>>, Self::Err> {
|
||||
self.wait_incoming_payment_stream_is_active
|
||||
.store(true, Ordering::SeqCst);
|
||||
tracing::debug!("Client waiting for payment");
|
||||
@@ -288,7 +288,9 @@ impl MintPayment for PaymentProcessorClient {
|
||||
.filter_map(|item| async {
|
||||
match item {
|
||||
Ok(value) => match value.try_into() {
|
||||
Ok(payment_response) => Some(payment_response),
|
||||
Ok(payment_response) => Some(cdk_common::payment::Event::PaymentReceived(
|
||||
payment_response,
|
||||
)),
|
||||
Err(e) => {
|
||||
tracing::error!("Error converting payment response: {}", e);
|
||||
None
|
||||
|
||||
@@ -401,19 +401,23 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
||||
ln.cancel_wait_invoice();
|
||||
break;
|
||||
}
|
||||
result = ln.wait_any_incoming_payment() => {
|
||||
result = ln.wait_payment_event() => {
|
||||
match result {
|
||||
Ok(mut stream) => {
|
||||
while let Some(payment_response) = stream.next().await {
|
||||
match tx.send(Result::<_, Status>::Ok(payment_response.into()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// Response was queued to be sent to client
|
||||
}
|
||||
Err(item) => {
|
||||
tracing::error!("Error adding incoming payment to stream: {}", item);
|
||||
break;
|
||||
while let Some(event) = stream.next().await {
|
||||
match event {
|
||||
cdk_common::payment::Event::PaymentReceived(payment_response) => {
|
||||
match tx.send(Result::<_, Status>::Ok(payment_response.into()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// Response was queued to be sent to client
|
||||
}
|
||||
Err(item) => {
|
||||
tracing::error!("Error adding incoming payment to stream: {}", item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ license.workspace = true
|
||||
homepage = "https://github.com/cashubtc/cdk"
|
||||
repository = "https://github.com/cashubtc/cdk.git"
|
||||
rust-version.workspace = true # MSRV
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
@@ -32,4 +31,5 @@ uuid.workspace = true
|
||||
tokio-postgres = "0.7.13"
|
||||
futures-util = "0.3.31"
|
||||
postgres-native-tls = "0.5.1"
|
||||
native-tls = "0.2"
|
||||
once_cell.workspace = true
|
||||
|
||||
@@ -10,6 +10,8 @@ use cdk_sql_common::pool::{DatabaseConfig, DatabasePool};
|
||||
use cdk_sql_common::stmt::{Column, Statement};
|
||||
use cdk_sql_common::{SQLMintDatabase, SQLWalletDatabase};
|
||||
use db::{pg_batch, pg_execute, pg_fetch_all, pg_fetch_one, pg_pluck};
|
||||
use native_tls::TlsConnector;
|
||||
use postgres_native_tls::MakeTlsConnector;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::time::timeout;
|
||||
use tokio_postgres::{connect, Client, Error as PgError, NoTls};
|
||||
@@ -25,6 +27,11 @@ pub enum SslMode {
|
||||
NoTls(NoTls),
|
||||
NativeTls(postgres_native_tls::MakeTlsConnector),
|
||||
}
|
||||
const SSLMODE_VERIFY_FULL: &str = "sslmode=verify-full";
|
||||
const SSLMODE_VERIFY_CA: &str = "sslmode=verify-ca";
|
||||
const SSLMODE_PREFER: &str = "sslmode=prefer";
|
||||
const SSLMODE_ALLOW: &str = "sslmode=allow";
|
||||
const SSLMODE_REQUIRE: &str = "sslmode=require";
|
||||
|
||||
impl Default for SslMode {
|
||||
fn default() -> Self {
|
||||
@@ -61,10 +68,44 @@ impl DatabaseConfig for PgConfig {
|
||||
}
|
||||
|
||||
impl From<&str> for PgConfig {
|
||||
fn from(value: &str) -> Self {
|
||||
fn from(conn_str: &str) -> Self {
|
||||
fn build_tls(accept_invalid_certs: bool, accept_invalid_hostnames: bool) -> SslMode {
|
||||
let mut builder = TlsConnector::builder();
|
||||
if accept_invalid_certs {
|
||||
builder.danger_accept_invalid_certs(true);
|
||||
}
|
||||
if accept_invalid_hostnames {
|
||||
builder.danger_accept_invalid_hostnames(true);
|
||||
}
|
||||
|
||||
match builder.build() {
|
||||
Ok(connector) => {
|
||||
let make_tls_connector = MakeTlsConnector::new(connector);
|
||||
SslMode::NativeTls(make_tls_connector)
|
||||
}
|
||||
Err(_) => SslMode::NoTls(NoTls {}),
|
||||
}
|
||||
}
|
||||
|
||||
let tls = if conn_str.contains(SSLMODE_VERIFY_FULL) {
|
||||
// Strict TLS: valid certs and hostnames required
|
||||
build_tls(false, false)
|
||||
} else if conn_str.contains(SSLMODE_VERIFY_CA) {
|
||||
// Verify CA, but allow invalid hostnames
|
||||
build_tls(false, true)
|
||||
} else if conn_str.contains(SSLMODE_PREFER)
|
||||
|| conn_str.contains(SSLMODE_ALLOW)
|
||||
|| conn_str.contains(SSLMODE_REQUIRE)
|
||||
{
|
||||
// Lenient TLS for preferred/allow/require: accept invalid certs and hostnames
|
||||
build_tls(true, true)
|
||||
} else {
|
||||
SslMode::NoTls(NoTls {})
|
||||
};
|
||||
|
||||
PgConfig {
|
||||
url: value.to_owned(),
|
||||
tls: Default::default(),
|
||||
url: conn_str.to_owned(),
|
||||
tls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,4 @@ tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
lightning-invoice.workspace = true
|
||||
uuid.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
||||
@@ -37,7 +37,6 @@ use cdk_common::{
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use migrations::MIGRATIONS;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::migrate;
|
||||
use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
|
||||
@@ -170,7 +169,7 @@ where
|
||||
async fn add_proofs(
|
||||
&mut self,
|
||||
proofs: Proofs,
|
||||
quote_id: Option<Uuid>,
|
||||
quote_id: Option<QuoteId>,
|
||||
) -> Result<(), Self::Err> {
|
||||
let current_time = unix_time();
|
||||
|
||||
@@ -213,7 +212,7 @@ where
|
||||
proof.witness.map(|w| serde_json::to_string(&w).unwrap()),
|
||||
)
|
||||
.bind("state", "UNSPENT".to_string())
|
||||
.bind("quote_id", quote_id.map(|q| q.hyphenated().to_string()))
|
||||
.bind("quote_id", quote_id.clone().map(|q| q.to_string()))
|
||||
.bind("created_time", current_time as i64)
|
||||
.execute(&self.inner)
|
||||
.await?;
|
||||
@@ -254,7 +253,7 @@ where
|
||||
async fn remove_proofs(
|
||||
&mut self,
|
||||
ys: &[PublicKey],
|
||||
_quote_id: Option<Uuid>,
|
||||
_quote_id: Option<QuoteId>,
|
||||
) -> Result<(), Self::Err> {
|
||||
if ys.is_empty() {
|
||||
return Ok(());
|
||||
@@ -328,13 +327,7 @@ where
|
||||
quote_id=:quote_id
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.fetch_all(conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -363,13 +356,7 @@ FROM mint_quote_issued
|
||||
WHERE quote_id=:quote_id
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.fetch_all(conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -591,13 +578,7 @@ where
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.fetch_one(&self.inner)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
@@ -632,13 +613,7 @@ where
|
||||
"#,
|
||||
)?
|
||||
.bind("amount_paid", new_amount_paid.to_i64())
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
@@ -653,13 +628,7 @@ where
|
||||
VALUES (:quote_id, :payment_id, :amount, :timestamp)
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.bind("payment_id", payment_id)
|
||||
.bind("amount", amount_paid.to_i64())
|
||||
.bind("timestamp", unix_time() as i64)
|
||||
@@ -688,13 +657,7 @@ where
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.fetch_one(&self.inner)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
@@ -722,13 +685,7 @@ where
|
||||
"#,
|
||||
)?
|
||||
.bind("amount_issued", new_amount_issued.to_i64())
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
@@ -744,13 +701,7 @@ INSERT INTO mint_quote_issued
|
||||
VALUES (:quote_id, :amount, :timestamp);
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.bind("amount", amount_issued.to_i64())
|
||||
.bind("timestamp", current_time as i64)
|
||||
.execute(&self.inner)
|
||||
@@ -792,13 +743,7 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
|
||||
async fn remove_mint_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
|
||||
query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
|
||||
.bind(
|
||||
"id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -861,10 +806,7 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)?
|
||||
.bind("new_req_id", new_request_lookup_id.to_string())
|
||||
.bind("new_kind",new_request_lookup_id.kind() )
|
||||
.bind("id", match quote_id {
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
})
|
||||
.bind("id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -900,13 +842,7 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
AND state != :state
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("id", quote_id.to_string())
|
||||
.bind("state", state.to_string())
|
||||
.fetch_one(&self.inner)
|
||||
.await?
|
||||
@@ -920,22 +856,13 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
.bind("state", state.to_string())
|
||||
.bind("paid_time", current_time as i64)
|
||||
.bind("payment_preimage", payment_proof)
|
||||
.bind("id", match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
})
|
||||
.bind("id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await
|
||||
} else {
|
||||
query(r#"UPDATE melt_quote SET state = :state WHERE id = :id"#)?
|
||||
.bind("state", state.to_string())
|
||||
.bind(
|
||||
"id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await
|
||||
};
|
||||
@@ -954,14 +881,14 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
Ok((old_state, quote))
|
||||
}
|
||||
|
||||
async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err> {
|
||||
async fn remove_melt_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
|
||||
query(
|
||||
r#"
|
||||
DELETE FROM melt_quote
|
||||
WHERE id=?
|
||||
"#,
|
||||
)?
|
||||
.bind("id", quote_id.as_hyphenated().to_string())
|
||||
.bind("id", quote_id.to_string())
|
||||
.execute(&self.inner)
|
||||
.await?;
|
||||
|
||||
@@ -993,13 +920,7 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("id", quote_id.to_string())
|
||||
.fetch_one(&self.inner)
|
||||
.await?
|
||||
.map(|row| sql_row_to_mint_quote(row, payments, issuance))
|
||||
@@ -1008,7 +929,7 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
|
||||
async fn get_melt_quote(
|
||||
&mut self,
|
||||
quote_id: &Uuid,
|
||||
quote_id: &QuoteId,
|
||||
) -> Result<Option<mint::MeltQuote>, Self::Err> {
|
||||
Ok(query(
|
||||
r#"
|
||||
@@ -1033,7 +954,7 @@ VALUES (:quote_id, :amount, :timestamp);
|
||||
id=:id
|
||||
"#,
|
||||
)?
|
||||
.bind("id", quote_id.as_hyphenated().to_string())
|
||||
.bind("id", quote_id.to_string())
|
||||
.fetch_one(&self.inner)
|
||||
.await?
|
||||
.map(sql_row_to_melt_quote)
|
||||
@@ -1157,13 +1078,7 @@ where
|
||||
mint_quote
|
||||
WHERE id = :id"#,
|
||||
)?
|
||||
.bind(
|
||||
"id",
|
||||
match quote_id {
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
},
|
||||
)
|
||||
.bind("id", quote_id.to_string())
|
||||
.fetch_one(&*conn)
|
||||
.await?
|
||||
.map(|row| sql_row_to_mint_quote(row, payments, issuance))
|
||||
@@ -1319,13 +1234,7 @@ where
|
||||
id=:id
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"id",
|
||||
match quote_id {
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
},
|
||||
)
|
||||
.bind("id", quote_id.to_string())
|
||||
.fetch_one(&*conn)
|
||||
.await?
|
||||
.map(sql_row_to_melt_quote)
|
||||
@@ -1406,7 +1315,10 @@ where
|
||||
Ok(ys.iter().map(|y| proofs.remove(y)).collect())
|
||||
}
|
||||
|
||||
async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err> {
|
||||
async fn get_proof_ys_by_quote_id(
|
||||
&self,
|
||||
quote_id: &QuoteId,
|
||||
) -> Result<Vec<PublicKey>, Self::Err> {
|
||||
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
|
||||
Ok(query(
|
||||
r#"
|
||||
@@ -1422,7 +1334,7 @@ where
|
||||
quote_id = :quote_id
|
||||
"#,
|
||||
)?
|
||||
.bind("quote_id", quote_id.as_hyphenated().to_string())
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.fetch_all(&*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -1661,13 +1573,7 @@ where
|
||||
quote_id=:quote_id
|
||||
"#,
|
||||
)?
|
||||
.bind(
|
||||
"quote_id",
|
||||
match quote_id {
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
QuoteId::UUID(u) => u.as_hyphenated().to_string(),
|
||||
},
|
||||
)
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
.fetch_all(&*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/// Auto-generated by build.rs
|
||||
pub static MIGRATIONS: &[(&str, &str, &str)] = &[
|
||||
("postgres", "1_initial.sql", include_str!(r#"./migrations/postgres/1_initial.sql"#)),
|
||||
("postgres", "20250831215438_melt_quote_method.sql", include_str!(r#"./migrations/postgres/20250831215438_melt_quote_method.sql"#)),
|
||||
("sqlite", "1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
|
||||
("sqlite", "20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
|
||||
("sqlite", "20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
|
||||
@@ -22,4 +23,5 @@ pub static MIGRATIONS: &[(&str, &str, &str)] = &[
|
||||
("sqlite", "20250707093445_bolt12.sql", include_str!(r#"./migrations/sqlite/20250707093445_bolt12.sql"#)),
|
||||
("sqlite", "20250729111701_keyset_v2_u32.sql", include_str!(r#"./migrations/sqlite/20250729111701_keyset_v2_u32.sql"#)),
|
||||
("sqlite", "20250812084621_keyset_plus_one.sql", include_str!(r#"./migrations/sqlite/20250812084621_keyset_plus_one.sql"#)),
|
||||
("sqlite", "20250831215438_melt_quote_method.sql", include_str!(r#"./migrations/sqlite/20250831215438_melt_quote_method.sql"#)),
|
||||
];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE melt_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'bolt11';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE melt_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'bolt11';
|
||||
@@ -156,7 +156,8 @@ where
|
||||
fee_reserve,
|
||||
state,
|
||||
expiry,
|
||||
payment_preimage
|
||||
payment_preimage,
|
||||
payment_method
|
||||
FROM
|
||||
melt_quote
|
||||
"#,
|
||||
@@ -551,6 +552,9 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
state,
|
||||
expiry,
|
||||
secret_key
|
||||
payment_method,
|
||||
amount_issued,
|
||||
amount_paid
|
||||
FROM
|
||||
mint_quote
|
||||
"#,
|
||||
@@ -579,16 +583,17 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
query(
|
||||
r#"
|
||||
INSERT INTO melt_quote
|
||||
(id, unit, amount, request, fee_reserve, state, expiry)
|
||||
(id, unit, amount, request, fee_reserve, state, expiry, payment_method)
|
||||
VALUES
|
||||
(:id, :unit, :amount, :request, :fee_reserve, :state, :expiry)
|
||||
(:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_method)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
unit = excluded.unit,
|
||||
amount = excluded.amount,
|
||||
request = excluded.request,
|
||||
fee_reserve = excluded.fee_reserve,
|
||||
state = excluded.state,
|
||||
expiry = excluded.expiry
|
||||
expiry = excluded.expiry,
|
||||
payment_method = excluded.payment_method
|
||||
;
|
||||
"#,
|
||||
)?
|
||||
@@ -599,6 +604,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
.bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
|
||||
.bind("state", quote.state.to_string())
|
||||
.bind("expiry", quote.expiry as i64)
|
||||
.bind("payment_method", quote.payment_method.to_string())
|
||||
.execute(&*conn)
|
||||
.await?;
|
||||
|
||||
@@ -618,7 +624,8 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
fee_reserve,
|
||||
state,
|
||||
expiry,
|
||||
payment_preimage
|
||||
payment_preimage,
|
||||
payment_method
|
||||
FROM
|
||||
melt_quote
|
||||
WHERE
|
||||
@@ -1124,13 +1131,17 @@ fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
|
||||
fee_reserve,
|
||||
state,
|
||||
expiry,
|
||||
payment_preimage
|
||||
payment_preimage,
|
||||
row_method
|
||||
) = row
|
||||
);
|
||||
|
||||
let amount: u64 = column_as_number!(amount);
|
||||
let fee_reserve: u64 = column_as_number!(fee_reserve);
|
||||
|
||||
let payment_method =
|
||||
PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
|
||||
|
||||
Ok(wallet::MeltQuote {
|
||||
id: column_as_string!(id),
|
||||
amount: Amount::from(amount),
|
||||
@@ -1140,6 +1151,7 @@ fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
|
||||
state: column_as_string!(state, MeltQuoteState::from_str),
|
||||
expiry: column_as_number!(expiry),
|
||||
payment_preimage: column_as_nullable_string!(payment_preimage),
|
||||
payment_method,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,11 @@ required-features = ["wallet", "bip353"]
|
||||
[[example]]
|
||||
name = "mint-token-bolt12-with-stream"
|
||||
required-features = ["wallet"]
|
||||
|
||||
[[example]]
|
||||
name = "mint-token-bolt12-with-custom-http"
|
||||
required-features = ["wallet"]
|
||||
|
||||
[[example]]
|
||||
name = "mint-token-bolt12"
|
||||
required-features = ["wallet"]
|
||||
@@ -118,6 +123,7 @@ tracing-subscriber.workspace = true
|
||||
criterion = "0.6.0"
|
||||
reqwest = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
ureq = { version = "3.1.0", features = ["json"] }
|
||||
|
||||
|
||||
[[bench]]
|
||||
|
||||
161
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
Normal file
161
crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use cdk::error::Error;
|
||||
use cdk::nuts::nut00::ProofsMethods;
|
||||
use cdk::nuts::CurrencyUnit;
|
||||
use cdk::wallet::{BaseHttpClient, HttpTransport, SendOptions, WalletBuilder};
|
||||
use cdk::{Amount, StreamExt};
|
||||
use cdk_common::mint_url::MintUrl;
|
||||
use cdk_common::AuthToken;
|
||||
use cdk_sqlite::wallet::memory;
|
||||
use rand::random;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use ureq::config::Config;
|
||||
use ureq::Agent;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CustomHttp {
|
||||
agent: Agent,
|
||||
}
|
||||
|
||||
impl Default for CustomHttp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
agent: Agent::new_with_config(
|
||||
Config::builder()
|
||||
.timeout_global(Some(Duration::from_secs(5)))
|
||||
.no_delay(true)
|
||||
.user_agent("Custom HTTP Transport")
|
||||
.build(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
impl HttpTransport for CustomHttp {
|
||||
fn with_proxy(
|
||||
&mut self,
|
||||
_proxy: Url,
|
||||
_host_matcher: Option<&str>,
|
||||
_accept_invalid_certs: bool,
|
||||
) -> Result<(), Error> {
|
||||
panic!("Not supported");
|
||||
}
|
||||
|
||||
async fn http_get<R>(&self, url: Url, _auth: Option<AuthToken>) -> Result<R, Error>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
self.agent
|
||||
.get(url.as_str())
|
||||
.call()
|
||||
.map_err(|e| Error::HttpError(None, e.to_string()))?
|
||||
.body_mut()
|
||||
.read_json()
|
||||
.map_err(|e| Error::HttpError(None, e.to_string()))
|
||||
}
|
||||
|
||||
/// HTTP Post request
|
||||
async fn http_post<P, R>(
|
||||
&self,
|
||||
url: Url,
|
||||
_auth_token: Option<AuthToken>,
|
||||
payload: &P,
|
||||
) -> Result<R, Error>
|
||||
where
|
||||
P: Serialize + ?Sized + Send + Sync,
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
self.agent
|
||||
.post(url.as_str())
|
||||
.send_json(payload)
|
||||
.map_err(|e| Error::HttpError(None, e.to_string()))?
|
||||
.body_mut()
|
||||
.read_json()
|
||||
.map_err(|e| Error::HttpError(None, e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
type CustomConnector = BaseHttpClient<CustomHttp>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let default_filter = "debug";
|
||||
|
||||
let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn,rustls=warn";
|
||||
|
||||
let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
|
||||
|
||||
// Parse input
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
// Initialize the memory store for the wallet
|
||||
let localstore = Arc::new(memory::empty().await?);
|
||||
|
||||
// Generate a random seed for the wallet
|
||||
let seed = random::<[u8; 64]>();
|
||||
|
||||
// Define the mint URL and currency unit
|
||||
let mint_url = "https://fake.thesimplekid.dev";
|
||||
let unit = CurrencyUnit::Sat;
|
||||
let amount = Amount::from(10);
|
||||
|
||||
let mint_url = MintUrl::from_str(mint_url)?;
|
||||
#[cfg(feature = "auth")]
|
||||
let http_client = CustomConnector::new(mint_url.clone(), None);
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let http_client = CustomConnector::new(mint_url.clone());
|
||||
|
||||
// Create a new wallet
|
||||
let wallet = WalletBuilder::new()
|
||||
.mint_url(mint_url)
|
||||
.unit(unit)
|
||||
.localstore(localstore)
|
||||
.seed(seed)
|
||||
.target_proof_count(3)
|
||||
.client(http_client)
|
||||
.build()?;
|
||||
|
||||
let quotes = vec![
|
||||
wallet.mint_bolt12_quote(None, None).await?,
|
||||
wallet.mint_bolt12_quote(None, None).await?,
|
||||
wallet.mint_bolt12_quote(None, None).await?,
|
||||
];
|
||||
|
||||
let mut stream = wallet.mints_proof_stream(quotes, Default::default(), None);
|
||||
|
||||
let stop = stream.get_cancel_token();
|
||||
|
||||
let mut processed = 0;
|
||||
|
||||
while let Some(proofs) = stream.next().await {
|
||||
let (mint_quote, proofs) = proofs?;
|
||||
|
||||
// Mint the received amount
|
||||
let receive_amount = proofs.total_amount()?;
|
||||
tracing::info!("Received {} from mint {}", receive_amount, mint_quote.id);
|
||||
|
||||
// Send a token with the specified amount
|
||||
let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
|
||||
let token = prepared_send.confirm(None).await?;
|
||||
tracing::info!("Token: {}", token);
|
||||
|
||||
processed += 1;
|
||||
|
||||
if processed == 3 {
|
||||
stop.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Stopped the loop after {} quotes being minted", processed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -536,16 +536,20 @@ impl Mint {
|
||||
processor.cancel_wait_invoice();
|
||||
break;
|
||||
}
|
||||
result = processor.wait_any_incoming_payment() => {
|
||||
result = processor.wait_payment_event() => {
|
||||
match result {
|
||||
Ok(mut stream) => {
|
||||
while let Some(request_lookup_id) = stream.next().await {
|
||||
if let Err(e) = Self::handle_payment_notification(
|
||||
&localstore,
|
||||
&pubsub_manager,
|
||||
request_lookup_id,
|
||||
).await {
|
||||
tracing::warn!("Payment notification error: {:?}", e);
|
||||
while let Some(event) = stream.next().await {
|
||||
match event {
|
||||
cdk_common::payment::Event::PaymentReceived(wait_payment_response) => {
|
||||
if let Err(e) = Self::handle_payment_notification(
|
||||
&localstore,
|
||||
&pubsub_manager,
|
||||
wait_payment_response,
|
||||
).await {
|
||||
tracing::warn!("Payment notification error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ pub struct Manager<T, I, F>
|
||||
where
|
||||
T: Indexable<Type = I> + Clone + Send + Sync + 'static,
|
||||
I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
|
||||
{
|
||||
indexes: IndexTree<T, I>,
|
||||
on_new_subscription: Option<F>,
|
||||
on_new_subscription: Option<Arc<F>>,
|
||||
unsubscription_sender: mpsc::Sender<(SubId, Vec<Index<I>>)>,
|
||||
active_subscriptions: Arc<AtomicUsize>,
|
||||
background_subscription_remover: Option<JoinHandle<()>>,
|
||||
@@ -54,7 +54,7 @@ impl<T, I, F> Default for Manager<T, I, F>
|
||||
where
|
||||
T: Indexable<Type = I> + Clone + Send + Sync + 'static,
|
||||
I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = mpsc::channel(DEFAULT_REMOVE_SIZE);
|
||||
@@ -79,11 +79,11 @@ impl<T, I, F> From<F> for Manager<T, I, F>
|
||||
where
|
||||
T: Indexable<Type = I> + Clone + Send + Sync + 'static,
|
||||
I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
let mut manager: Self = Default::default();
|
||||
manager.on_new_subscription = Some(value);
|
||||
manager.on_new_subscription = Some(Arc::new(value));
|
||||
manager
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ impl<T, I, F> Manager<T, I, F>
|
||||
where
|
||||
T: Indexable<Type = I> + Clone + Send + Sync + 'static,
|
||||
I: PartialOrd + Clone + Debug + Ord + Send + Sync + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
/// Broadcast an event to all listeners
|
||||
@@ -143,32 +143,45 @@ where
|
||||
indexes: Vec<Index<I>>,
|
||||
) -> ActiveSubscription<T, I> {
|
||||
let (sender, receiver) = mpsc::channel(10);
|
||||
if let Some(on_new_subscription) = self.on_new_subscription.as_ref() {
|
||||
match on_new_subscription
|
||||
.on_new_subscription(&indexes.iter().map(|x| x.deref()).collect::<Vec<_>>())
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
for event in events {
|
||||
let _ = sender.try_send((sub_id.clone(), event));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::info!(
|
||||
"Failed to get initial state for subscription: {:?}, {}",
|
||||
sub_id,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut index_storage = self.indexes.write().await;
|
||||
// Subscribe to events as soon as possible
|
||||
for index in indexes.clone() {
|
||||
index_storage.insert(index, sender.clone());
|
||||
}
|
||||
drop(index_storage);
|
||||
|
||||
if let Some(on_new_subscription) = self.on_new_subscription.clone() {
|
||||
// After we're subscribed already, fetch the current status of matching events. It is
|
||||
// down in another thread to return right away
|
||||
let indexes_for_worker = indexes.clone();
|
||||
let sub_id_for_worker = sub_id.clone();
|
||||
tokio::spawn(async move {
|
||||
match on_new_subscription
|
||||
.on_new_subscription(
|
||||
&indexes_for_worker
|
||||
.iter()
|
||||
.map(|x| x.deref())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
for event in events {
|
||||
let _ = sender.try_send((sub_id_for_worker.clone(), event));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::info!(
|
||||
"Failed to get initial state for subscription: {:?}, {}",
|
||||
sub_id_for_worker,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.active_subscriptions
|
||||
.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
|
||||
@@ -232,7 +245,7 @@ impl<T, I, F> Drop for Manager<T, I, F>
|
||||
where
|
||||
T: Indexable<Type = I> + Clone + Send + Sync + 'static,
|
||||
I: Clone + Debug + PartialOrd + Ord + Send + Sync + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + 'static,
|
||||
F: OnNewSubscription<Index = I, Event = T> + Send + Sync + 'static,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if let Some(handler) = self.background_subscription_remover.take() {
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct WalletBuilder {
|
||||
#[cfg(feature = "auth")]
|
||||
auth_wallet: Option<AuthWallet>,
|
||||
seed: Option<[u8; 64]>,
|
||||
use_http_subscription: bool,
|
||||
client: Option<Arc<dyn MintConnector + Send + Sync>>,
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ impl Default for WalletBuilder {
|
||||
auth_wallet: None,
|
||||
seed: None,
|
||||
client: None,
|
||||
use_http_subscription: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +52,19 @@ impl WalletBuilder {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Use HTTP for wallet subscriptions to mint events
|
||||
pub fn use_http_subscription(mut self) -> Self {
|
||||
self.use_http_subscription = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// If WS is preferred (with fallback to HTTP is it is not supported by the mint) for the wallet
|
||||
/// subscriptions to mint events
|
||||
pub fn prefer_ws_subscription(mut self) -> Self {
|
||||
self.use_http_subscription = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the mint URL
|
||||
pub fn mint_url(mut self, mint_url: MintUrl) -> Self {
|
||||
self.mint_url = Some(mint_url);
|
||||
@@ -150,7 +165,7 @@ impl WalletBuilder {
|
||||
auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),
|
||||
seed,
|
||||
client: client.clone(),
|
||||
subscription: SubscriptionManager::new(client),
|
||||
subscription: SubscriptionManager::new(client, self.use_http_subscription),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cdk_common::nut04::MintMethodOptions;
|
||||
use cdk_common::nut24::MintQuoteBolt12Request;
|
||||
use cdk_common::nut25::MintQuoteBolt12Request;
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||
use cdk_common::{Proofs, SecretKey};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::str::FromStr;
|
||||
|
||||
use cdk_common::amount::SplitTarget;
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||
use cdk_common::PaymentMethod;
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -87,6 +88,7 @@ impl Wallet {
|
||||
state: quote_res.state,
|
||||
expiry: quote_res.expiry,
|
||||
payment_preimage: quote_res.payment_preimage,
|
||||
payment_method: PaymentMethod::Bolt11,
|
||||
};
|
||||
|
||||
self.localstore.add_melt_quote(quote.clone()).await?;
|
||||
@@ -183,7 +185,13 @@ impl Wallet {
|
||||
Some(premint_secrets.blinded_messages()),
|
||||
);
|
||||
|
||||
let melt_response = self.client.post_melt(request).await;
|
||||
let melt_response = match quote_info.payment_method {
|
||||
cdk_common::PaymentMethod::Bolt11 => self.client.post_melt(request).await,
|
||||
cdk_common::PaymentMethod::Bolt12 => self.client.post_melt_bolt12(request).await,
|
||||
cdk_common::PaymentMethod::Custom(_) => {
|
||||
return Err(Error::UnsupportedPaymentMethod);
|
||||
}
|
||||
};
|
||||
|
||||
let melt_response = match melt_response {
|
||||
Ok(melt_response) => melt_response,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
|
||||
use cdk_common::amount::amount_for_offer;
|
||||
use cdk_common::wallet::MeltQuote;
|
||||
use cdk_common::PaymentMethod;
|
||||
use lightning::offers::offer::Offer;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -57,6 +58,7 @@ impl Wallet {
|
||||
state: quote_res.state,
|
||||
expiry: quote_res.expiry,
|
||||
payment_preimage: quote_res.payment_preimage,
|
||||
payment_method: PaymentMethod::Bolt12,
|
||||
};
|
||||
|
||||
self.localstore.add_melt_quote(quote.clone()).await?;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//! HTTP Mint client with pluggable transport
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock as StdRwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -6,17 +7,15 @@ use async_trait::async_trait;
|
||||
use cdk_common::{nut19, MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||
#[cfg(feature = "auth")]
|
||||
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
|
||||
use reqwest::{Client, IntoUrl};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
#[cfg(feature = "auth")]
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use url::Url;
|
||||
|
||||
use super::transport::Transport;
|
||||
use super::{Error, MintConnector};
|
||||
use crate::error::ErrorResponse;
|
||||
use crate::mint_url::MintUrl;
|
||||
#[cfg(feature = "auth")]
|
||||
use crate::nuts::nut22::MintAuthRequest;
|
||||
@@ -29,119 +28,30 @@ use crate::nuts::{
|
||||
#[cfg(feature = "auth")]
|
||||
use crate::wallet::auth::{AuthMintConnector, AuthWallet};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct HttpClientCore {
|
||||
inner: Client,
|
||||
}
|
||||
|
||||
impl HttpClientCore {
|
||||
fn new() -> Self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
Self {
|
||||
inner: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn client(&self) -> &Client {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
async fn http_get<U: IntoUrl + Send, R: DeserializeOwned>(
|
||||
&self,
|
||||
url: U,
|
||||
auth: Option<AuthToken>,
|
||||
) -> Result<R, Error> {
|
||||
let mut request = self.client().get(url);
|
||||
|
||||
if let Some(auth) = auth {
|
||||
request = request.header(auth.header_key(), auth.to_string());
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
serde_json::from_str::<R>(&response).map_err(|err| {
|
||||
tracing::warn!("Http Response error: {}", err);
|
||||
match ErrorResponse::from_json(&response) {
|
||||
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
||||
Err(err) => err.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn http_post<U: IntoUrl + Send, P: Serialize + ?Sized, R: DeserializeOwned>(
|
||||
&self,
|
||||
url: U,
|
||||
auth_token: Option<AuthToken>,
|
||||
payload: &P,
|
||||
) -> Result<R, Error> {
|
||||
let mut request = self.client().post(url).json(&payload);
|
||||
|
||||
if let Some(auth) = auth_token {
|
||||
request = request.header(auth.header_key(), auth.to_string());
|
||||
}
|
||||
|
||||
let response = request.send().await.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let response = response.text().await.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
serde_json::from_str::<R>(&response).map_err(|err| {
|
||||
tracing::warn!("Http Response error: {}", err);
|
||||
match ErrorResponse::from_json(&response) {
|
||||
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
||||
Err(err) => err.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type Cache = (u64, HashSet<(nut19::Method, nut19::Path)>);
|
||||
|
||||
/// Http Client
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HttpClient {
|
||||
core: HttpClientCore,
|
||||
pub struct HttpClient<T>
|
||||
where
|
||||
T: Transport + Send + Sync + 'static,
|
||||
{
|
||||
transport: Arc<T>,
|
||||
mint_url: MintUrl,
|
||||
cache_support: Arc<StdRwLock<Cache>>,
|
||||
#[cfg(feature = "auth")]
|
||||
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
impl<T> HttpClient<T>
|
||||
where
|
||||
T: Transport + Send + Sync + 'static,
|
||||
{
|
||||
/// Create new [`HttpClient`]
|
||||
#[cfg(feature = "auth")]
|
||||
pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
|
||||
Self {
|
||||
core: HttpClientCore::new(),
|
||||
transport: T::default().into(),
|
||||
mint_url,
|
||||
auth_wallet: Arc::new(RwLock::new(auth_wallet)),
|
||||
cache_support: Default::default(),
|
||||
@@ -152,7 +62,7 @@ impl HttpClient {
|
||||
/// Create new [`HttpClient`]
|
||||
pub fn new(mint_url: MintUrl) -> Self {
|
||||
Self {
|
||||
core: HttpClientCore::new(),
|
||||
transport: T::default().into(),
|
||||
cache_support: Default::default(),
|
||||
mint_url,
|
||||
}
|
||||
@@ -176,7 +86,6 @@ impl HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Create new [`HttpClient`] with a proxy for specific TLDs.
|
||||
/// Specifying `None` for `host_matcher` will use the proxy for all
|
||||
/// requests.
|
||||
@@ -186,32 +95,11 @@ impl HttpClient {
|
||||
host_matcher: Option<&str>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<Self, Error> {
|
||||
let regex = host_matcher
|
||||
.map(regex::Regex::new)
|
||||
.transpose()
|
||||
.map_err(|e| Error::Custom(e.to_string()))?;
|
||||
let client = reqwest::Client::builder()
|
||||
.proxy(reqwest::Proxy::custom(move |url| {
|
||||
if let Some(matcher) = regex.as_ref() {
|
||||
if let Some(host) = url.host_str() {
|
||||
if matcher.is_match(host) {
|
||||
return Some(proxy.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}))
|
||||
.danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
let mut transport = T::default();
|
||||
transport.with_proxy(proxy, host_matcher, accept_invalid_certs)?;
|
||||
|
||||
Ok(Self {
|
||||
core: HttpClientCore { inner: client },
|
||||
transport: transport.into(),
|
||||
mint_url,
|
||||
#[cfg(feature = "auth")]
|
||||
auth_wallet: Arc::new(RwLock::new(None)),
|
||||
@@ -231,7 +119,7 @@ impl HttpClient {
|
||||
payload: &P,
|
||||
) -> Result<R, Error>
|
||||
where
|
||||
P: Serialize + ?Sized,
|
||||
P: Serialize + ?Sized + Send + Sync,
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
let started = Instant::now();
|
||||
@@ -259,8 +147,12 @@ impl HttpClient {
|
||||
})?;
|
||||
|
||||
let result = match method {
|
||||
nut19::Method::Get => self.core.http_get(url, auth_token.clone()).await,
|
||||
nut19::Method::Post => self.core.http_post(url, auth_token.clone(), payload).await,
|
||||
nut19::Method::Get => self.transport.http_get(url, auth_token.clone()).await,
|
||||
nut19::Method::Post => {
|
||||
self.transport
|
||||
.http_post(url, auth_token.clone(), payload)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
@@ -291,15 +183,18 @@ impl HttpClient {
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl MintConnector for HttpClient {
|
||||
impl<T> MintConnector for HttpClient<T>
|
||||
where
|
||||
T: Transport + Send + Sync + 'static,
|
||||
{
|
||||
/// Get Active Mint Keys [NUT-01]
|
||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||
async fn get_mint_keys(&self) -> Result<Vec<KeySet>, Error> {
|
||||
let url = self.mint_url.join_paths(&["v1", "keys"])?;
|
||||
|
||||
Ok(self
|
||||
.core
|
||||
.http_get::<_, KeysResponse>(url, None)
|
||||
.transport
|
||||
.http_get::<KeysResponse>(url, None)
|
||||
.await?
|
||||
.keysets)
|
||||
}
|
||||
@@ -311,7 +206,7 @@ impl MintConnector for HttpClient {
|
||||
.mint_url
|
||||
.join_paths(&["v1", "keys", &keyset_id.to_string()])?;
|
||||
|
||||
let keys_response = self.core.http_get::<_, KeysResponse>(url, None).await?;
|
||||
let keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
|
||||
|
||||
Ok(keys_response.keysets.first().unwrap().clone())
|
||||
}
|
||||
@@ -320,7 +215,7 @@ impl MintConnector for HttpClient {
|
||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||
async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
|
||||
let url = self.mint_url.join_paths(&["v1", "keysets"])?;
|
||||
self.core.http_get(url, None).await
|
||||
self.transport.http_get(url, None).await
|
||||
}
|
||||
|
||||
/// Mint Quote [NUT-04]
|
||||
@@ -341,7 +236,7 @@ impl MintConnector for HttpClient {
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
|
||||
self.core.http_post(url, auth_token, &request).await
|
||||
self.transport.http_post(url, auth_token, &request).await
|
||||
}
|
||||
|
||||
/// Mint Quote status
|
||||
@@ -361,7 +256,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_get(url, auth_token).await
|
||||
self.transport.http_get(url, auth_token).await
|
||||
}
|
||||
|
||||
/// Mint Tokens [NUT-04]
|
||||
@@ -399,7 +294,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_post(url, auth_token, &request).await
|
||||
self.transport.http_post(url, auth_token, &request).await
|
||||
}
|
||||
|
||||
/// Melt Quote Status
|
||||
@@ -419,7 +314,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_get(url, auth_token).await
|
||||
self.transport.http_get(url, auth_token).await
|
||||
}
|
||||
|
||||
/// Melt [NUT-05]
|
||||
@@ -467,7 +362,7 @@ impl MintConnector for HttpClient {
|
||||
/// Helper to get mint info
|
||||
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
||||
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
||||
let info: MintInfo = self.core.http_get(url, None).await?;
|
||||
let info: MintInfo = self.transport.http_get(url, None).await?;
|
||||
|
||||
if let Ok(mut cache_support) = self.cache_support.write() {
|
||||
*cache_support = (
|
||||
@@ -509,7 +404,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_post(url, auth_token, &request).await
|
||||
self.transport.http_post(url, auth_token, &request).await
|
||||
}
|
||||
|
||||
/// Restore request [NUT-13]
|
||||
@@ -523,7 +418,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_post(url, auth_token, &request).await
|
||||
self.transport.http_post(url, auth_token, &request).await
|
||||
}
|
||||
|
||||
/// Mint Quote Bolt12 [NUT-23]
|
||||
@@ -544,7 +439,7 @@ impl MintConnector for HttpClient {
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
|
||||
self.core.http_post(url, auth_token, &request).await
|
||||
self.transport.http_post(url, auth_token, &request).await
|
||||
}
|
||||
|
||||
/// Mint Quote Bolt12 status
|
||||
@@ -564,7 +459,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_get(url, auth_token).await
|
||||
self.transport.http_get(url, auth_token).await
|
||||
}
|
||||
|
||||
/// Melt Quote Bolt12 [NUT-23]
|
||||
@@ -583,7 +478,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_post(url, auth_token, &request).await
|
||||
self.transport.http_post(url, auth_token, &request).await
|
||||
}
|
||||
|
||||
/// Melt Quote Bolt12 Status [NUT-23]
|
||||
@@ -603,7 +498,7 @@ impl MintConnector for HttpClient {
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
let auth_token = None;
|
||||
self.core.http_get(url, auth_token).await
|
||||
self.transport.http_get(url, auth_token).await
|
||||
}
|
||||
|
||||
/// Melt Bolt12 [NUT-23]
|
||||
@@ -632,18 +527,24 @@ impl MintConnector for HttpClient {
|
||||
/// Http Client
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(feature = "auth")]
|
||||
pub struct AuthHttpClient {
|
||||
core: HttpClientCore,
|
||||
pub struct AuthHttpClient<T>
|
||||
where
|
||||
T: Transport + Send + Sync + 'static,
|
||||
{
|
||||
transport: Arc<T>,
|
||||
mint_url: MintUrl,
|
||||
cat: Arc<RwLock<AuthToken>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
impl AuthHttpClient {
|
||||
impl<T> AuthHttpClient<T>
|
||||
where
|
||||
T: Transport + Send + Sync + 'static,
|
||||
{
|
||||
/// Create new [`AuthHttpClient`]
|
||||
pub fn new(mint_url: MintUrl, cat: Option<AuthToken>) -> Self {
|
||||
Self {
|
||||
core: HttpClientCore::new(),
|
||||
transport: T::default().into(),
|
||||
mint_url,
|
||||
cat: Arc::new(RwLock::new(
|
||||
cat.unwrap_or(AuthToken::ClearAuth("".to_string())),
|
||||
@@ -655,7 +556,10 @@ impl AuthHttpClient {
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
#[cfg(feature = "auth")]
|
||||
impl AuthMintConnector for AuthHttpClient {
|
||||
impl<T> AuthMintConnector for AuthHttpClient<T>
|
||||
where
|
||||
T: Transport + Send + Sync + 'static,
|
||||
{
|
||||
async fn get_auth_token(&self) -> Result<AuthToken, Error> {
|
||||
Ok(self.cat.read().await.clone())
|
||||
}
|
||||
@@ -668,7 +572,7 @@ impl AuthMintConnector for AuthHttpClient {
|
||||
/// Get Mint Info [NUT-06]
|
||||
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
||||
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
||||
let mint_info: MintInfo = self.core.http_get::<_, MintInfo>(url, None).await?;
|
||||
let mint_info: MintInfo = self.transport.http_get::<MintInfo>(url, None).await?;
|
||||
|
||||
Ok(mint_info)
|
||||
}
|
||||
@@ -680,7 +584,7 @@ impl AuthMintConnector for AuthHttpClient {
|
||||
self.mint_url
|
||||
.join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?;
|
||||
|
||||
let mut keys_response = self.core.http_get::<_, KeysResponse>(url, None).await?;
|
||||
let mut keys_response = self.transport.http_get::<KeysResponse>(url, None).await?;
|
||||
|
||||
let keyset = keys_response
|
||||
.keysets
|
||||
@@ -698,14 +602,14 @@ impl AuthMintConnector for AuthHttpClient {
|
||||
.mint_url
|
||||
.join_paths(&["v1", "auth", "blind", "keysets"])?;
|
||||
|
||||
self.core.http_get(url, None).await
|
||||
self.transport.http_get(url, None).await
|
||||
}
|
||||
|
||||
/// Mint Tokens [NUT-22]
|
||||
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||
async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
|
||||
let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
|
||||
self.core
|
||||
self.transport
|
||||
.http_post(url, Some(self.cat.read().await.clone()), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ use crate::nuts::{
|
||||
#[cfg(feature = "auth")]
|
||||
use crate::wallet::AuthWallet;
|
||||
|
||||
mod http_client;
|
||||
pub mod http_client;
|
||||
pub mod transport;
|
||||
|
||||
/// Auth HTTP Client with async transport
|
||||
#[cfg(feature = "auth")]
|
||||
pub use http_client::AuthHttpClient;
|
||||
pub use http_client::HttpClient;
|
||||
pub type AuthHttpClient = http_client::AuthHttpClient<transport::Async>;
|
||||
/// Http Client with async transport
|
||||
pub type HttpClient = http_client::HttpClient<transport::Async>;
|
||||
|
||||
/// Interface that connects a wallet to a mint. Typically represents an [HttpClient].
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
|
||||
182
crates/cdk/src/wallet/mint_connector/transport.rs
Normal file
182
crates/cdk/src/wallet/mint_connector/transport.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
//! HTTP Transport trait with a default implementation
|
||||
use std::fmt::Debug;
|
||||
|
||||
use cdk_common::AuthToken;
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
|
||||
use super::Error;
|
||||
use crate::error::ErrorResponse;
|
||||
|
||||
/// Expected HTTP Transport
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
pub trait Transport: Default + Send + Sync + Debug + Clone {
|
||||
/// Make the transport to use a given proxy
|
||||
fn with_proxy(
|
||||
&mut self,
|
||||
proxy: Url,
|
||||
host_matcher: Option<&str>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// HTTP Get request
|
||||
async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
|
||||
where
|
||||
R: DeserializeOwned;
|
||||
|
||||
/// HTTP Post request
|
||||
async fn http_post<P, R>(
|
||||
&self,
|
||||
url: Url,
|
||||
auth_token: Option<AuthToken>,
|
||||
payload: &P,
|
||||
) -> Result<R, Error>
|
||||
where
|
||||
P: Serialize + ?Sized + Send + Sync,
|
||||
R: DeserializeOwned;
|
||||
}
|
||||
|
||||
/// Async transport for Http
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Async {
|
||||
inner: Client,
|
||||
}
|
||||
|
||||
impl Default for Async {
|
||||
fn default() -> Self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
Self {
|
||||
inner: Client::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
impl Transport for Async {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn with_proxy(
|
||||
&mut self,
|
||||
_proxy: Url,
|
||||
_host_matcher: Option<&str>,
|
||||
_accept_invalid_certs: bool,
|
||||
) -> Result<(), Error> {
|
||||
panic!("Not supported in wasm");
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn with_proxy(
|
||||
&mut self,
|
||||
proxy: Url,
|
||||
host_matcher: Option<&str>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<(), Error> {
|
||||
let regex = host_matcher
|
||||
.map(regex::Regex::new)
|
||||
.transpose()
|
||||
.map_err(|e| Error::Custom(e.to_string()))?;
|
||||
self.inner = reqwest::Client::builder()
|
||||
.proxy(reqwest::Proxy::custom(move |url| {
|
||||
if let Some(matcher) = regex.as_ref() {
|
||||
if let Some(host) = url.host_str() {
|
||||
if matcher.is_match(host) {
|
||||
return Some(proxy.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}))
|
||||
.danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn http_get<R>(&self, url: Url, auth: Option<AuthToken>) -> Result<R, Error>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
let mut request = self.inner.get(url);
|
||||
|
||||
if let Some(auth) = auth {
|
||||
request = request.header(auth.header_key(), auth.to_string());
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
serde_json::from_str::<R>(&response).map_err(|err| {
|
||||
tracing::warn!("Http Response error: {}", err);
|
||||
match ErrorResponse::from_json(&response) {
|
||||
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
||||
Err(err) => err.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn http_post<P, R>(
|
||||
&self,
|
||||
url: Url,
|
||||
auth_token: Option<AuthToken>,
|
||||
payload: &P,
|
||||
) -> Result<R, Error>
|
||||
where
|
||||
P: Serialize + ?Sized + Send + Sync,
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
let mut request = self.inner.post(url).json(&payload);
|
||||
|
||||
if let Some(auth) = auth_token {
|
||||
request = request.header(auth.header_key(), auth.to_string());
|
||||
}
|
||||
|
||||
let response = request.send().await.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let response = response.text().await.map_err(|e| {
|
||||
Error::HttpError(
|
||||
e.status().map(|status_code| status_code.as_u16()),
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
serde_json::from_str::<R>(&response).map_err(|err| {
|
||||
tracing::warn!("Http Response error: {}", err);
|
||||
match ErrorResponse::from_json(&response) {
|
||||
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
||||
Err(err) => err.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,10 @@ pub use auth::{AuthMintConnector, AuthWallet};
|
||||
pub use builder::WalletBuilder;
|
||||
pub use cdk_common::wallet as types;
|
||||
#[cfg(feature = "auth")]
|
||||
pub use mint_connector::http_client::AuthHttpClient as BaseAuthHttpClient;
|
||||
pub use mint_connector::http_client::HttpClient as BaseHttpClient;
|
||||
pub use mint_connector::transport::Transport as HttpTransport;
|
||||
#[cfg(feature = "auth")]
|
||||
pub use mint_connector::AuthHttpClient;
|
||||
pub use mint_connector::{HttpClient, MintConnector};
|
||||
pub use multi_mint_wallet::MultiMintWallet;
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use cdk_common::MintQuoteBolt12Response;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio::time;
|
||||
|
||||
@@ -15,6 +16,7 @@ use crate::Wallet;
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
enum UrlType {
|
||||
Mint(String),
|
||||
MintBolt12(String),
|
||||
Melt(String),
|
||||
PublicKey(nut01::PublicKey),
|
||||
}
|
||||
@@ -22,6 +24,7 @@ enum UrlType {
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum AnyState {
|
||||
MintQuoteState(nut23::QuoteState),
|
||||
MintBolt12QuoteState(MintQuoteBolt12Response<String>),
|
||||
MeltQuoteState(nut05::QuoteState),
|
||||
PublicKey(nut07::State),
|
||||
Empty,
|
||||
@@ -67,7 +70,12 @@ async fn convert_subscription(
|
||||
}
|
||||
}
|
||||
Kind::Bolt12MintQuote => {
|
||||
for id in sub.1.filters.iter().map(|id| UrlType::Mint(id.clone())) {
|
||||
for id in sub
|
||||
.1
|
||||
.filters
|
||||
.iter()
|
||||
.map(|id| UrlType::MintBolt12(id.clone()))
|
||||
{
|
||||
subscribed_to.insert(id, (sub.0.clone(), sub.1.id.clone(), AnyState::Empty));
|
||||
}
|
||||
}
|
||||
@@ -98,6 +106,18 @@ pub async fn http_main<S: IntoIterator<Item = SubId>>(
|
||||
for (url, (sender, _, last_state)) in subscribed_to.iter_mut() {
|
||||
tracing::debug!("Polling: {:?}", url);
|
||||
match url {
|
||||
UrlType::MintBolt12(id) => {
|
||||
let response = http_client.get_mint_quote_bolt12_status(id).await;
|
||||
if let Ok(response) = response {
|
||||
if *last_state == AnyState::MintBolt12QuoteState(response.clone()) {
|
||||
continue;
|
||||
}
|
||||
*last_state = AnyState::MintBolt12QuoteState(response.clone());
|
||||
if let Err(err) = sender.try_send(NotificationPayload::MintQuoteBolt12Response(response)) {
|
||||
tracing::error!("Error sending mint quote response: {:?}", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
UrlType::Mint(id) => {
|
||||
|
||||
let response = http_client.get_mint_quote_status(id).await;
|
||||
|
||||
@@ -48,14 +48,16 @@ type WsSubscriptionBody = (mpsc::Sender<NotificationPayload>, Params);
|
||||
pub struct SubscriptionManager {
|
||||
all_connections: Arc<RwLock<HashMap<MintUrl, SubscriptionClient>>>,
|
||||
http_client: Arc<dyn MintConnector + Send + Sync>,
|
||||
prefer_http: bool,
|
||||
}
|
||||
|
||||
impl SubscriptionManager {
|
||||
/// Create a new subscription manager
|
||||
pub fn new(http_client: Arc<dyn MintConnector + Send + Sync>) -> Self {
|
||||
pub fn new(http_client: Arc<dyn MintConnector + Send + Sync>, prefer_http: bool) -> Self {
|
||||
Self {
|
||||
all_connections: Arc::new(RwLock::new(HashMap::new())),
|
||||
http_client,
|
||||
prefer_http,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +95,12 @@ impl SubscriptionManager {
|
||||
))]
|
||||
let is_ws_support = false;
|
||||
|
||||
let is_ws_support = if self.prefer_http {
|
||||
false
|
||||
} else {
|
||||
is_ws_support
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"Connect to {:?} to subscribe. WebSocket is supported ({})",
|
||||
mint_url,
|
||||
|
||||
@@ -18,25 +18,6 @@ use crate::Wallet;
|
||||
|
||||
const MAX_ATTEMPT_FALLBACK_HTTP: usize = 10;
|
||||
|
||||
async fn fallback_to_http<S: IntoIterator<Item = SubId>>(
|
||||
initial_state: S,
|
||||
http_client: Arc<dyn MintConnector + Send + Sync>,
|
||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||
new_subscription_recv: mpsc::Receiver<SubId>,
|
||||
on_drop: mpsc::Receiver<SubId>,
|
||||
wallet: Arc<Wallet>,
|
||||
) {
|
||||
http_main(
|
||||
initial_state,
|
||||
http_client,
|
||||
subscriptions,
|
||||
new_subscription_recv,
|
||||
on_drop,
|
||||
wallet,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn ws_main(
|
||||
http_client: Arc<dyn MintConnector + Send + Sync>,
|
||||
@@ -72,7 +53,8 @@ pub async fn ws_main(
|
||||
tracing::error!(
|
||||
"Could not connect to server after {MAX_ATTEMPT_FALLBACK_HTTP} attempts, falling back to HTTP-subscription client"
|
||||
);
|
||||
return fallback_to_http(
|
||||
|
||||
return http_main(
|
||||
active_subscriptions.into_keys(),
|
||||
http_client,
|
||||
subscriptions,
|
||||
@@ -169,17 +151,19 @@ pub async fn ws_main(
|
||||
WsMessageOrResponse::ErrorResponse(error) => {
|
||||
tracing::error!("Received error from server: {:?}", error);
|
||||
if subscription_requests.contains(&error.id) {
|
||||
// If the server sends an error response to a subscription request, we should
|
||||
// fallback to HTTP.
|
||||
// TODO: Add some retry before giving up to HTTP.
|
||||
return fallback_to_http(
|
||||
tracing::error!(
|
||||
"Falling back to HTTP client"
|
||||
);
|
||||
|
||||
return http_main(
|
||||
active_subscriptions.into_keys(),
|
||||
http_client,
|
||||
subscriptions,
|
||||
new_subscription_recv,
|
||||
on_drop,
|
||||
wallet
|
||||
).await;
|
||||
wallet,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -23,11 +23,11 @@
|
||||
"rust-analyzer-src": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755585599,
|
||||
"narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=",
|
||||
"lastModified": 1756622179,
|
||||
"narHash": "sha256-K3CimrAcMhdDYkErd3oiWPZNaoyaGZEuvGrFuDPFMZY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42",
|
||||
"rev": "0abcb15ae6279dcb40a8ae7c1ed980705245cb79",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -93,11 +93,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755922037,
|
||||
"narHash": "sha256-wY1+2JPH0ZZC4BQefoZw/k+3+DowFyfOxv17CN/idKs=",
|
||||
"lastModified": 1756469547,
|
||||
"narHash": "sha256-YvtD2E7MYsQ3r7K9K2G7nCslCKMPShoSEAtbjHLtH0k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b1b3291469652d5a2edb0becc4ef0246fff97a7c",
|
||||
"rev": "41d292bfc37309790f70f4c120b79280ce40af16",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -160,11 +160,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756089517,
|
||||
"narHash": "sha256-KGinVKturJFPrRebgvyUB1BUNqf1y9FN+tSJaTPlnFE=",
|
||||
"lastModified": 1756607787,
|
||||
"narHash": "sha256-ciwAdgtlAN1PCaidWK6RuWsTBL8DVuyDCGM+X3ein5Q=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "44774c8c83cd392c50914f86e1ff75ef8619f1cd",
|
||||
"rev": "f46d294b87ebb9f7124f1ce13aa2a5f5acc0f3eb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
# MSRV stable
|
||||
msrv_toolchain = pkgs.rust-bin.stable."1.85.0".default.override {
|
||||
targets = [ "wasm32-unknown-unknown" ]; # wasm
|
||||
extensions = [
|
||||
"rustfmt"
|
||||
"clippy"
|
||||
"rust-analyzer"
|
||||
];
|
||||
};
|
||||
|
||||
# Nightly used for formatting
|
||||
@@ -114,7 +119,6 @@
|
||||
|
||||
# Common arguments can be set here to avoid repeating them later
|
||||
nativeBuildInputs = [
|
||||
pkgs.rust-analyzer
|
||||
#Add additional build inputs here
|
||||
]
|
||||
++ lib.optionals isDarwin [
|
||||
|
||||
2
justfile
2
justfile
@@ -325,6 +325,7 @@ release m="":
|
||||
"-p cdk-common"
|
||||
"-p cdk-sql-common"
|
||||
"-p cdk-sqlite"
|
||||
"-p cdk-postgres"
|
||||
"-p cdk-redb"
|
||||
"-p cdk-signatory"
|
||||
"-p cdk"
|
||||
@@ -333,6 +334,7 @@ release m="":
|
||||
"-p cdk-cln"
|
||||
"-p cdk-lnd"
|
||||
"-p cdk-lnbits"
|
||||
"-p cdk-ldk-node"
|
||||
"-p cdk-fake-wallet"
|
||||
"-p cdk-payment-processor"
|
||||
"-p cdk-cli"
|
||||
|
||||
Reference in New Issue
Block a user