Skip to content

Commit bc545fc

Browse files
authored
Merge pull request cashubtc#874 from thesimplekid/bolt12_2
Bolt12 2
2 parents 34e91dc + ae6c107 commit bc545fc

86 files changed

Lines changed: 6304 additions & 1941 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
6161
cbor-diag = "0.1.12"
6262
futures = { version = "0.3.28", default-features = false, features = ["async-await"] }
6363
lightning-invoice = { version = "0.33.0", features = ["serde", "std"] }
64+
lightning = { version = "0.1.2", default-features = false, features = ["std"]}
6465
serde = { version = "1", features = ["derive"] }
6566
serde_json = "1"
6667
thiserror = { version = "2" }

crates/cashu/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ciborium.workspace = true
2626
once_cell.workspace = true
2727
serde.workspace = true
2828
lightning-invoice.workspace = true
29+
lightning.workspace = true
2930
thiserror.workspace = true
3031
tracing.workspace = true
3132
url.workspace = true

crates/cashu/src/amount.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::cmp::Ordering;
66
use std::fmt;
77
use std::str::FromStr;
88

9+
use lightning::offers::offer::Offer;
910
use serde::{Deserialize, Serialize};
1011
use thiserror::Error;
1112

@@ -26,6 +27,12 @@ pub enum Error {
2627
/// Invalid amount
2728
#[error("Invalid Amount: {0}")]
2829
InvalidAmount(String),
30+
/// Amount undefined
31+
#[error("Amount undefined")]
32+
AmountUndefined,
33+
/// Utf8 parse error
34+
#[error(transparent)]
35+
Utf8ParseError(#[from] std::string::FromUtf8Error),
2936
}
3037

3138
/// Amount can be any unit
@@ -181,6 +188,24 @@ impl Amount {
181188
) -> Result<Amount, Error> {
182189
to_unit(self.0, current_unit, target_unit)
183190
}
191+
192+
/// Convert to i64
193+
pub fn to_i64(self) -> Option<i64> {
194+
if self.0 <= i64::MAX as u64 {
195+
Some(self.0 as i64)
196+
} else {
197+
None
198+
}
199+
}
200+
201+
/// Create from i64, returning None if negative
202+
pub fn from_i64(value: i64) -> Option<Self> {
203+
if value >= 0 {
204+
Some(Amount(value as u64))
205+
} else {
206+
None
207+
}
208+
}
184209
}
185210

186211
impl Default for Amount {
@@ -273,6 +298,27 @@ impl std::ops::Div for Amount {
273298
}
274299
}
275300

301+
/// Convert offer to amount in unit
302+
pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result<Amount, Error> {
303+
let offer_amount = offer.amount().ok_or(Error::AmountUndefined)?;
304+
305+
let (amount, currency) = match offer_amount {
306+
lightning::offers::offer::Amount::Bitcoin { amount_msats } => {
307+
(amount_msats, CurrencyUnit::Msat)
308+
}
309+
lightning::offers::offer::Amount::Currency {
310+
iso4217_code,
311+
amount,
312+
} => (
313+
amount,
314+
CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?)
315+
.map_err(|_| Error::CannotConvertUnits)?,
316+
),
317+
};
318+
319+
to_unit(amount, &currency, unit).map_err(|_err| Error::CannotConvertUnits)
320+
}
321+
276322
/// Kinds of targeting that are supported
277323
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
278324
pub enum SplitTarget {

crates/cashu/src/nuts/auth/nut21.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ pub enum RoutePath {
149149
/// Mint Blind Auth
150150
#[serde(rename = "/v1/auth/blind/mint")]
151151
MintBlindAuth,
152+
/// Bolt12 Mint Quote
153+
#[serde(rename = "/v1/mint/quote/bolt12")]
154+
MintQuoteBolt12,
155+
/// Bolt12 Mint
156+
#[serde(rename = "/v1/mint/bolt12")]
157+
MintBolt12,
158+
/// Bolt12 Melt Quote
159+
#[serde(rename = "/v1/melt/quote/bolt12")]
160+
MeltQuoteBolt12,
161+
/// Bolt12 Quote
162+
#[serde(rename = "/v1/melt/bolt12")]
163+
MeltBolt12,
152164
}
153165

154166
/// Returns [`RoutePath`]s that match regex
@@ -195,6 +207,8 @@ mod tests {
195207
assert!(paths.contains(&RoutePath::Checkstate));
196208
assert!(paths.contains(&RoutePath::Restore));
197209
assert!(paths.contains(&RoutePath::MintBlindAuth));
210+
assert!(paths.contains(&RoutePath::MintQuoteBolt12));
211+
assert!(paths.contains(&RoutePath::MintBolt12));
198212
}
199213

200214
#[test]
@@ -203,13 +217,17 @@ mod tests {
203217
let paths = matching_route_paths("^/v1/mint/.*").unwrap();
204218

205219
// Should match only mint paths
206-
assert_eq!(paths.len(), 2);
220+
assert_eq!(paths.len(), 4);
207221
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
208222
assert!(paths.contains(&RoutePath::MintBolt11));
223+
assert!(paths.contains(&RoutePath::MintQuoteBolt12));
224+
assert!(paths.contains(&RoutePath::MintBolt12));
209225

210226
// Should not match other paths
211227
assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
212228
assert!(!paths.contains(&RoutePath::MeltBolt11));
229+
assert!(!paths.contains(&RoutePath::MeltQuoteBolt12));
230+
assert!(!paths.contains(&RoutePath::MeltBolt12));
213231
assert!(!paths.contains(&RoutePath::Swap));
214232
}
215233

@@ -219,9 +237,11 @@ mod tests {
219237
let paths = matching_route_paths(".*/quote/.*").unwrap();
220238

221239
// Should match only quote paths
222-
assert_eq!(paths.len(), 2);
240+
assert_eq!(paths.len(), 4);
223241
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
224242
assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
243+
assert!(paths.contains(&RoutePath::MintQuoteBolt12));
244+
assert!(paths.contains(&RoutePath::MeltQuoteBolt12));
225245

226246
// Should not match non-quote paths
227247
assert!(!paths.contains(&RoutePath::MintBolt11));
@@ -336,12 +356,14 @@ mod tests {
336356
"https://example.com/.well-known/openid-configuration"
337357
);
338358
assert_eq!(settings.client_id, "client123");
339-
assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
359+
assert_eq!(settings.protected_endpoints.len(), 5); // 3 mint paths + 1 swap path
340360

341361
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
342362
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
343363
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
344364
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
365+
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
366+
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
345367
]);
346368

347369
let deserlized_protected = settings.protected_endpoints.into_iter().collect();

crates/cashu/src/nuts/auth/nut22.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,14 @@ mod tests {
330330
let settings: Settings = serde_json::from_str(json).unwrap();
331331

332332
assert_eq!(settings.bat_max_mint, 5);
333-
assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
333+
assert_eq!(settings.protected_endpoints.len(), 5); // 4 mint paths + 1 swap path
334334

335335
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
336336
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
337337
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
338338
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
339+
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
340+
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
339341
]);
340342

341343
let deserialized_protected = settings.protected_endpoints.into_iter().collect();

crates/cashu/src/nuts/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub mod nut18;
2424
pub mod nut19;
2525
pub mod nut20;
2626
pub mod nut23;
27+
pub mod nut24;
2728

2829
#[cfg(feature = "auth")]
2930
mod auth;
@@ -67,3 +68,4 @@ pub use nut23::{
6768
MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
6869
MintQuoteBolt11Response, QuoteState as MintQuoteState,
6970
};
71+
pub use nut24::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};

crates/cashu/src/nuts/nut00/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,13 +641,14 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
641641
}
642642

643643
/// Payment Method
644-
#[non_exhaustive]
645644
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
646645
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
647646
pub enum PaymentMethod {
648647
/// Bolt11 payment type
649648
#[default]
650649
Bolt11,
650+
/// Bolt12
651+
Bolt12,
651652
/// Custom
652653
Custom(String),
653654
}
@@ -657,6 +658,7 @@ impl FromStr for PaymentMethod {
657658
fn from_str(value: &str) -> Result<Self, Self::Err> {
658659
match value.to_lowercase().as_str() {
659660
"bolt11" => Ok(Self::Bolt11),
661+
"bolt12" => Ok(Self::Bolt12),
660662
c => Ok(Self::Custom(c.to_string())),
661663
}
662664
}
@@ -666,6 +668,7 @@ impl fmt::Display for PaymentMethod {
666668
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667669
match self {
668670
PaymentMethod::Bolt11 => write!(f, "bolt11"),
671+
PaymentMethod::Bolt12 => write!(f, "bolt12"),
669672
PaymentMethod::Custom(p) => write!(f, "{p}"),
670673
}
671674
}

crates/cashu/src/nuts/nut04.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,16 @@ impl Settings {
291291
.position(|settings| &settings.method == method && &settings.unit == unit)
292292
.map(|index| self.methods.remove(index))
293293
}
294+
295+
/// Supported nut04 methods
296+
pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
297+
self.methods.iter().map(|a| &a.method).collect()
298+
}
299+
300+
/// Supported nut04 units
301+
pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
302+
self.methods.iter().map(|s| &s.unit).collect()
303+
}
294304
}
295305

296306
#[cfg(test)]

crates/cashu/src/nuts/nut05.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
105105

106106
// Basic implementation without trait bounds
107107
impl<Q> MeltRequest<Q> {
108+
/// Quote Id
109+
pub fn quote_id(&self) -> &Q {
110+
&self.quote
111+
}
112+
108113
/// Get inputs (proofs)
109114
pub fn inputs(&self) -> &Proofs {
110115
&self.inputs
@@ -132,7 +137,7 @@ impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
132137
}
133138

134139
/// Total [`Amount`] of [`Proofs`]
135-
pub fn proofs_amount(&self) -> Result<Amount, Error> {
140+
pub fn inputs_amount(&self) -> Result<Amount, Error> {
136141
Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
137142
.map_err(|_| Error::AmountOverflow)
138143
}
@@ -355,6 +360,18 @@ pub struct Settings {
355360
pub disabled: bool,
356361
}
357362

363+
impl Settings {
364+
/// Supported nut05 methods
365+
pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
366+
self.methods.iter().map(|a| &a.method).collect()
367+
}
368+
369+
/// Supported nut05 units
370+
pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
371+
self.methods.iter().map(|s| &s.unit).collect()
372+
}
373+
}
374+
358375
#[cfg(test)]
359376
mod tests {
360377
use serde_json::{from_str, json, to_string};

crates/cashu/src/nuts/nut17/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use super::PublicKey;
99
use crate::nuts::{
1010
CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
1111
};
12+
use crate::MintQuoteBolt12Response;
1213

1314
pub mod ws;
1415

@@ -69,6 +70,21 @@ impl SupportedMethods {
6970
commands,
7071
}
7172
}
73+
74+
/// Create [`SupportedMethods`] for Bolt12 with all supported commands
75+
pub fn default_bolt12(unit: CurrencyUnit) -> Self {
76+
let commands = vec![
77+
WsCommand::Bolt12MintQuote,
78+
WsCommand::Bolt12MeltQuote,
79+
WsCommand::ProofState,
80+
];
81+
82+
Self {
83+
method: PaymentMethod::Bolt12,
84+
unit,
85+
commands,
86+
}
87+
}
7288
}
7389

7490
/// WebSocket commands supported by the Cashu mint
@@ -82,11 +98,23 @@ pub enum WsCommand {
8298
/// Command to request a Lightning payment for melting tokens
8399
#[serde(rename = "bolt11_melt_quote")]
84100
Bolt11MeltQuote,
101+
/// Websocket support for Bolt12 Mint Quote
102+
#[serde(rename = "bolt12_mint_quote")]
103+
Bolt12MintQuote,
104+
/// Websocket support for Bolt12 Melt Quote
105+
#[serde(rename = "bolt12_melt_quote")]
106+
Bolt12MeltQuote,
85107
/// Command to check the state of a proof
86108
#[serde(rename = "proof_state")]
87109
ProofState,
88110
}
89111

112+
impl<T> From<MintQuoteBolt12Response<T>> for NotificationPayload<T> {
113+
fn from(mint_quote: MintQuoteBolt12Response<T>) -> NotificationPayload<T> {
114+
NotificationPayload::MintQuoteBolt12Response(mint_quote)
115+
}
116+
}
117+
90118
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91119
#[serde(bound = "T: Serialize + DeserializeOwned")]
92120
#[serde(untagged)]
@@ -98,6 +126,8 @@ pub enum NotificationPayload<T> {
98126
MeltQuoteBolt11Response(MeltQuoteBolt11Response<T>),
99127
/// Mint Quote Bolt11 Response
100128
MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
129+
/// Mint Quote Bolt12 Response
130+
MintQuoteBolt12Response(MintQuoteBolt12Response<T>),
101131
}
102132

103133
impl<T> From<ProofState> for NotificationPayload<T> {
@@ -128,6 +158,10 @@ pub enum Notification {
128158
MeltQuoteBolt11(Uuid),
129159
/// MintQuote id is an Uuid
130160
MintQuoteBolt11(Uuid),
161+
/// MintQuote id is an Uuid
162+
MintQuoteBolt12(Uuid),
163+
/// MintQuote id is an Uuid
164+
MeltQuoteBolt12(Uuid),
131165
}
132166

133167
/// Kind

0 commit comments

Comments
 (0)