mint.rs 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. //! Mint types
  2. use std::fmt;
  3. use std::ops::Deref;
  4. use std::str::FromStr;
  5. use bitcoin::bip32::DerivationPath;
  6. use cashu::quote_id::QuoteId;
  7. use cashu::util::unix_time;
  8. use cashu::{
  9. Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
  10. MintQuoteBolt12Response, PaymentMethod, Proofs, State,
  11. };
  12. use lightning::offers::offer::Offer;
  13. use serde::{Deserialize, Serialize};
  14. use tracing::instrument;
  15. use uuid::Uuid;
  16. use crate::nuts::{MeltQuoteState, MintQuoteState};
  17. use crate::payment::PaymentIdentifier;
  18. use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
  19. /// Operation kind for saga persistence
  20. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
  21. #[serde(rename_all = "lowercase")]
  22. pub enum OperationKind {
  23. /// Swap operation
  24. Swap,
  25. /// Mint operation
  26. Mint,
  27. /// Melt operation
  28. Melt,
  29. }
  30. /// A collection of proofs that share a common state.
  31. ///
  32. /// This type enforces the invariant that all proofs in the collection have the same state.
  33. /// The mint never needs to operate on a set of proofs with different states - proofs are
  34. /// always processed together as a unit (e.g., during swap, melt, or mint operations).
  35. ///
  36. /// # Database Layer Responsibility
  37. ///
  38. /// This design shifts the responsibility of ensuring state consistency to the database layer.
  39. /// When the database retrieves proofs via [`get_proofs`](crate::database::mint::ProofsTransaction::get_proofs),
  40. /// it must verify that all requested proofs share the same state and return an error if they don't.
  41. /// This prevents invalid proof sets from propagating through the system.
  42. ///
  43. /// # State Transitions
  44. ///
  45. /// State transitions are validated using [`check_state_transition`](crate::state::check_state_transition)
  46. /// before updating. The database layer then persists the new state for all proofs in a single transaction
  47. /// via [`update_proofs_state`](crate::database::mint::ProofsTransaction::update_proofs_state).
  48. ///
  49. /// # Example
  50. ///
  51. /// ```ignore
  52. /// // Database layer ensures all proofs have the same state
  53. /// let mut proofs = tx.get_proofs(&ys).await?;
  54. ///
  55. /// // Validate the state transition
  56. /// check_state_transition(proofs.state, State::Spent)?;
  57. ///
  58. /// // Persist the state change
  59. /// tx.update_proofs_state(&mut proofs, State::Spent).await?;
  60. /// ```
  61. #[derive(Debug)]
  62. pub struct ProofsWithState {
  63. proofs: Proofs,
  64. /// The current state of the proofs
  65. pub state: State,
  66. }
  67. impl Deref for ProofsWithState {
  68. type Target = Proofs;
  69. fn deref(&self) -> &Self::Target {
  70. &self.proofs
  71. }
  72. }
  73. impl ProofsWithState {
  74. /// Creates a new `ProofsWithState` with the given proofs and their shared state.
  75. ///
  76. /// # Note
  77. ///
  78. /// This constructor assumes all proofs share the given state. It is typically
  79. /// called by the database layer after verifying state consistency.
  80. pub fn new(proofs: Proofs, current_state: State) -> Self {
  81. Self {
  82. proofs,
  83. state: current_state,
  84. }
  85. }
  86. }
  87. impl fmt::Display for OperationKind {
  88. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  89. match self {
  90. OperationKind::Swap => write!(f, "swap"),
  91. OperationKind::Mint => write!(f, "mint"),
  92. OperationKind::Melt => write!(f, "melt"),
  93. }
  94. }
  95. }
  96. impl FromStr for OperationKind {
  97. type Err = Error;
  98. fn from_str(value: &str) -> Result<Self, Self::Err> {
  99. let value = value.to_lowercase();
  100. match value.as_str() {
  101. "swap" => Ok(OperationKind::Swap),
  102. "mint" => Ok(OperationKind::Mint),
  103. "melt" => Ok(OperationKind::Melt),
  104. _ => Err(Error::Custom(format!("Invalid operation kind: {value}"))),
  105. }
  106. }
  107. }
  108. /// States specific to swap saga
  109. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  110. #[serde(rename_all = "snake_case")]
  111. pub enum SwapSagaState {
  112. /// Swap setup complete (proofs added, blinded messages added)
  113. SetupComplete,
  114. /// Outputs signed (signatures generated but not persisted)
  115. Signed,
  116. }
  117. impl fmt::Display for SwapSagaState {
  118. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  119. match self {
  120. SwapSagaState::SetupComplete => write!(f, "setup_complete"),
  121. SwapSagaState::Signed => write!(f, "signed"),
  122. }
  123. }
  124. }
  125. impl FromStr for SwapSagaState {
  126. type Err = Error;
  127. fn from_str(value: &str) -> Result<Self, Self::Err> {
  128. let value = value.to_lowercase();
  129. match value.as_str() {
  130. "setup_complete" => Ok(SwapSagaState::SetupComplete),
  131. "signed" => Ok(SwapSagaState::Signed),
  132. _ => Err(Error::Custom(format!("Invalid swap saga state: {value}"))),
  133. }
  134. }
  135. }
  136. /// States specific to melt saga
  137. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  138. #[serde(rename_all = "snake_case")]
  139. pub enum MeltSagaState {
  140. /// Setup complete (proofs reserved, quote verified)
  141. SetupComplete,
  142. /// Payment attempted to Lightning network (may or may not have succeeded)
  143. PaymentAttempted,
  144. /// TX1 committed (proofs Spent, quote Paid) - change signing + cleanup pending
  145. Finalizing,
  146. }
  147. impl fmt::Display for MeltSagaState {
  148. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  149. match self {
  150. MeltSagaState::SetupComplete => write!(f, "setup_complete"),
  151. MeltSagaState::PaymentAttempted => write!(f, "payment_attempted"),
  152. MeltSagaState::Finalizing => write!(f, "finalizing"),
  153. }
  154. }
  155. }
  156. impl FromStr for MeltSagaState {
  157. type Err = Error;
  158. fn from_str(value: &str) -> Result<Self, Self::Err> {
  159. let value = value.to_lowercase();
  160. match value.as_str() {
  161. "setup_complete" => Ok(MeltSagaState::SetupComplete),
  162. "payment_attempted" => Ok(MeltSagaState::PaymentAttempted),
  163. "finalizing" => Ok(MeltSagaState::Finalizing),
  164. _ => Err(Error::Custom(format!("Invalid melt saga state: {}", value))),
  165. }
  166. }
  167. }
  168. /// Saga state for different operation types
  169. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  170. #[serde(tag = "type", rename_all = "snake_case")]
  171. pub enum SagaStateEnum {
  172. /// Swap saga states
  173. Swap(SwapSagaState),
  174. /// Melt saga states
  175. Melt(MeltSagaState),
  176. // Future: Mint saga states
  177. // Mint(MintSagaState),
  178. }
  179. impl SagaStateEnum {
  180. /// Create from string given operation kind
  181. pub fn new(operation_kind: OperationKind, s: &str) -> Result<Self, Error> {
  182. match operation_kind {
  183. OperationKind::Swap => Ok(SagaStateEnum::Swap(SwapSagaState::from_str(s)?)),
  184. OperationKind::Melt => Ok(SagaStateEnum::Melt(MeltSagaState::from_str(s)?)),
  185. OperationKind::Mint => Err(Error::Custom("Mint saga not implemented yet".to_string())),
  186. }
  187. }
  188. /// Get string representation of the state
  189. pub fn state(&self) -> &str {
  190. match self {
  191. SagaStateEnum::Swap(state) => match state {
  192. SwapSagaState::SetupComplete => "setup_complete",
  193. SwapSagaState::Signed => "signed",
  194. },
  195. SagaStateEnum::Melt(state) => match state {
  196. MeltSagaState::SetupComplete => "setup_complete",
  197. MeltSagaState::PaymentAttempted => "payment_attempted",
  198. MeltSagaState::Finalizing => "finalizing",
  199. },
  200. }
  201. }
  202. }
  203. /// Persisted saga for recovery
  204. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  205. pub struct Saga {
  206. /// Operation ID (correlation key)
  207. pub operation_id: Uuid,
  208. /// Operation kind (swap, mint, melt)
  209. pub operation_kind: OperationKind,
  210. /// Current saga state (operation-specific)
  211. pub state: SagaStateEnum,
  212. /// Quote ID for melt operations (used for payment status lookup during recovery)
  213. /// None for swap operations
  214. pub quote_id: Option<String>,
  215. /// Unix timestamp when saga was created
  216. pub created_at: u64,
  217. /// Unix timestamp when saga was last updated
  218. pub updated_at: u64,
  219. }
  220. impl Saga {
  221. /// Create new swap saga
  222. pub fn new_swap(operation_id: Uuid, state: SwapSagaState) -> Self {
  223. let now = unix_time();
  224. Self {
  225. operation_id,
  226. operation_kind: OperationKind::Swap,
  227. state: SagaStateEnum::Swap(state),
  228. quote_id: None,
  229. created_at: now,
  230. updated_at: now,
  231. }
  232. }
  233. /// Update swap saga state
  234. pub fn update_swap_state(&mut self, new_state: SwapSagaState) {
  235. self.state = SagaStateEnum::Swap(new_state);
  236. self.updated_at = unix_time();
  237. }
  238. /// Create new melt saga
  239. pub fn new_melt(operation_id: Uuid, state: MeltSagaState, quote_id: String) -> Self {
  240. let now = unix_time();
  241. Self {
  242. operation_id,
  243. operation_kind: OperationKind::Melt,
  244. state: SagaStateEnum::Melt(state),
  245. quote_id: Some(quote_id),
  246. created_at: now,
  247. updated_at: now,
  248. }
  249. }
  250. /// Update melt saga state
  251. pub fn update_melt_state(&mut self, new_state: MeltSagaState) {
  252. self.state = SagaStateEnum::Melt(new_state);
  253. self.updated_at = unix_time();
  254. }
  255. }
  256. /// Operation
  257. #[derive(Debug)]
  258. pub struct Operation {
  259. id: Uuid,
  260. kind: OperationKind,
  261. total_issued: Amount,
  262. total_redeemed: Amount,
  263. fee_collected: Amount,
  264. complete_at: Option<u64>,
  265. /// Payment amount (only for melt operations)
  266. payment_amount: Option<Amount>,
  267. /// Payment fee (only for melt operations)
  268. payment_fee: Option<Amount>,
  269. /// Payment method (only for mint/melt operations)
  270. payment_method: Option<PaymentMethod>,
  271. }
  272. impl Operation {
  273. /// New
  274. pub fn new(
  275. id: Uuid,
  276. kind: OperationKind,
  277. total_issued: Amount,
  278. total_redeemed: Amount,
  279. fee_collected: Amount,
  280. complete_at: Option<u64>,
  281. payment_method: Option<PaymentMethod>,
  282. ) -> Self {
  283. Self {
  284. id,
  285. kind,
  286. total_issued,
  287. total_redeemed,
  288. fee_collected,
  289. complete_at,
  290. payment_amount: None,
  291. payment_fee: None,
  292. payment_method,
  293. }
  294. }
  295. /// Mint
  296. pub fn new_mint(total_issued: Amount, payment_method: PaymentMethod) -> Self {
  297. Self {
  298. id: Uuid::new_v4(),
  299. kind: OperationKind::Mint,
  300. total_issued,
  301. total_redeemed: Amount::ZERO,
  302. fee_collected: Amount::ZERO,
  303. complete_at: None,
  304. payment_amount: None,
  305. payment_fee: None,
  306. payment_method: Some(payment_method),
  307. }
  308. }
  309. /// Melt
  310. ///
  311. /// In the context of a melt total_issued refrests to the change
  312. pub fn new_melt(
  313. total_redeemed: Amount,
  314. fee_collected: Amount,
  315. payment_method: PaymentMethod,
  316. ) -> Self {
  317. Self {
  318. id: Uuid::new_v4(),
  319. kind: OperationKind::Melt,
  320. total_issued: Amount::ZERO,
  321. total_redeemed,
  322. fee_collected,
  323. complete_at: None,
  324. payment_amount: None,
  325. payment_fee: None,
  326. payment_method: Some(payment_method),
  327. }
  328. }
  329. /// Swap
  330. pub fn new_swap(total_issued: Amount, total_redeemed: Amount, fee_collected: Amount) -> Self {
  331. Self {
  332. id: Uuid::new_v4(),
  333. kind: OperationKind::Swap,
  334. total_issued,
  335. total_redeemed,
  336. fee_collected,
  337. complete_at: None,
  338. payment_amount: None,
  339. payment_fee: None,
  340. payment_method: None,
  341. }
  342. }
  343. /// Operation id
  344. pub fn id(&self) -> &Uuid {
  345. &self.id
  346. }
  347. /// Operation kind
  348. pub fn kind(&self) -> OperationKind {
  349. self.kind
  350. }
  351. /// Total issued
  352. pub fn total_issued(&self) -> Amount {
  353. self.total_issued
  354. }
  355. /// Total redeemed
  356. pub fn total_redeemed(&self) -> Amount {
  357. self.total_redeemed
  358. }
  359. /// Fee collected
  360. pub fn fee_collected(&self) -> Amount {
  361. self.fee_collected
  362. }
  363. /// Completed time
  364. pub fn completed_at(&self) -> &Option<u64> {
  365. &self.complete_at
  366. }
  367. /// Add change
  368. pub fn add_change(&mut self, change: Amount) {
  369. self.total_issued = change;
  370. }
  371. /// Payment amount (only for melt operations)
  372. pub fn payment_amount(&self) -> Option<Amount> {
  373. self.payment_amount
  374. }
  375. /// Payment fee (only for melt operations)
  376. pub fn payment_fee(&self) -> Option<Amount> {
  377. self.payment_fee
  378. }
  379. /// Set payment details for melt operations
  380. pub fn set_payment_details(&mut self, payment_amount: Amount, payment_fee: Amount) {
  381. self.payment_amount = Some(payment_amount);
  382. self.payment_fee = Some(payment_fee);
  383. }
  384. /// Payment method (only for mint/melt operations)
  385. pub fn payment_method(&self) -> Option<PaymentMethod> {
  386. self.payment_method.clone()
  387. }
  388. }
  389. /// Tracks pending changes made to a [`MintQuote`] that need to be persisted.
  390. ///
  391. /// This struct implements a change-tracking pattern that separates domain logic from
  392. /// persistence concerns. When modifications are made to a `MintQuote` via methods like
  393. /// [`MintQuote::add_payment`] or [`MintQuote::add_issuance`], the changes are recorded
  394. /// here rather than being immediately persisted. The database layer can then call
  395. /// [`MintQuote::take_changes`] to retrieve and persist only the modifications.
  396. ///
  397. /// This approach allows business rule validation to happen in the domain model while
  398. /// keeping the database layer focused purely on persistence.
  399. #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
  400. pub struct MintQuoteChange {
  401. /// New payments added since the quote was loaded or last persisted.
  402. pub payments: Option<Vec<IncomingPayment>>,
  403. /// New issuance amounts recorded since the quote was loaded or last persisted.
  404. pub issuances: Option<Vec<Amount>>,
  405. }
  406. /// Mint Quote Info
  407. #[derive(Debug, Clone, Hash, PartialEq, Eq)]
  408. pub struct MintQuote {
  409. /// Quote id
  410. pub id: QuoteId,
  411. /// Amount of quote
  412. pub amount: Option<Amount<CurrencyUnit>>,
  413. /// Unit of quote
  414. pub unit: CurrencyUnit,
  415. /// Quote payment request e.g. bolt11
  416. pub request: String,
  417. /// Expiration time of quote
  418. pub expiry: u64,
  419. /// Value used by ln backend to look up state of request
  420. pub request_lookup_id: PaymentIdentifier,
  421. /// Pubkey
  422. pub pubkey: Option<PublicKey>,
  423. /// Unix time quote was created
  424. pub created_time: u64,
  425. /// Amount paid (typed for type safety)
  426. amount_paid: Amount<CurrencyUnit>,
  427. /// Amount issued (typed for type safety)
  428. amount_issued: Amount<CurrencyUnit>,
  429. /// Payment of payment(s) that filled quote
  430. pub payments: Vec<IncomingPayment>,
  431. /// Payment Method
  432. pub payment_method: PaymentMethod,
  433. /// Payment of payment(s) that filled quote
  434. pub issuance: Vec<Issuance>,
  435. /// Extra payment-method-specific fields
  436. pub extra_json: Option<serde_json::Value>,
  437. /// Accumulated changes since this quote was loaded or created.
  438. ///
  439. /// This field is not serialized and is used internally to track modifications
  440. /// that need to be persisted. Use [`Self::take_changes`] to extract pending
  441. /// changes for persistence.
  442. changes: Option<MintQuoteChange>,
  443. }
  444. impl MintQuote {
  445. /// Create new [`MintQuote`]
  446. #[allow(clippy::too_many_arguments)]
  447. pub fn new(
  448. id: Option<QuoteId>,
  449. request: String,
  450. unit: CurrencyUnit,
  451. amount: Option<Amount<CurrencyUnit>>,
  452. expiry: u64,
  453. request_lookup_id: PaymentIdentifier,
  454. pubkey: Option<PublicKey>,
  455. amount_paid: Amount<CurrencyUnit>,
  456. amount_issued: Amount<CurrencyUnit>,
  457. payment_method: PaymentMethod,
  458. created_time: u64,
  459. payments: Vec<IncomingPayment>,
  460. issuance: Vec<Issuance>,
  461. extra_json: Option<serde_json::Value>,
  462. ) -> Self {
  463. let id = id.unwrap_or_else(QuoteId::new_uuid);
  464. Self {
  465. id,
  466. amount,
  467. unit: unit.clone(),
  468. request,
  469. expiry,
  470. request_lookup_id,
  471. pubkey,
  472. created_time,
  473. amount_paid,
  474. amount_issued,
  475. payment_method,
  476. payments,
  477. issuance,
  478. extra_json,
  479. changes: None,
  480. }
  481. }
  482. /// Increment the amount paid on the mint quote by a given amount
  483. #[instrument(skip(self))]
  484. pub fn increment_amount_paid(
  485. &mut self,
  486. additional_amount: Amount<CurrencyUnit>,
  487. ) -> Result<Amount, crate::Error> {
  488. self.amount_paid = self
  489. .amount_paid
  490. .checked_add(&additional_amount)
  491. .map_err(|_| crate::Error::AmountOverflow)?;
  492. Ok(Amount::from(self.amount_paid.value()))
  493. }
  494. /// Amount paid
  495. #[instrument(skip(self))]
  496. pub fn amount_paid(&self) -> Amount<CurrencyUnit> {
  497. self.amount_paid.clone()
  498. }
  499. /// Records tokens being issued against this mint quote.
  500. ///
  501. /// This method validates that the issuance doesn't exceed the amount paid, updates
  502. /// the quote's internal state, and records the change for later persistence. The
  503. /// `amount_issued` counter is incremented and the issuance is added to the change
  504. /// tracker for the database layer to persist.
  505. ///
  506. /// # Arguments
  507. ///
  508. /// * `additional_amount` - The amount of tokens being issued.
  509. ///
  510. /// # Returns
  511. ///
  512. /// Returns the new total `amount_issued` after this issuance is recorded.
  513. ///
  514. /// # Errors
  515. ///
  516. /// Returns [`crate::Error::OverIssue`] if the new issued amount would exceed the
  517. /// amount paid (cannot issue more tokens than have been paid for).
  518. ///
  519. /// Returns [`crate::Error::AmountOverflow`] if adding the issuance amount would
  520. /// cause an arithmetic overflow.
  521. #[instrument(skip(self))]
  522. pub fn add_issuance(
  523. &mut self,
  524. additional_amount: Amount<CurrencyUnit>,
  525. ) -> Result<Amount<CurrencyUnit>, crate::Error> {
  526. let new_amount_issued = self
  527. .amount_issued
  528. .checked_add(&additional_amount)
  529. .map_err(|_| crate::Error::AmountOverflow)?;
  530. // Can't issue more than what's been paid
  531. if new_amount_issued > self.amount_paid {
  532. return Err(crate::Error::OverIssue);
  533. }
  534. self.changes
  535. .get_or_insert_default()
  536. .issuances
  537. .get_or_insert_default()
  538. .push(additional_amount.into());
  539. self.amount_issued = new_amount_issued;
  540. Ok(self.amount_issued.clone())
  541. }
  542. /// Amount issued
  543. #[instrument(skip(self))]
  544. pub fn amount_issued(&self) -> Amount<CurrencyUnit> {
  545. self.amount_issued.clone()
  546. }
  547. /// Get state of mint quote
  548. #[instrument(skip(self))]
  549. pub fn state(&self) -> MintQuoteState {
  550. self.compute_quote_state()
  551. }
  552. /// Existing payment ids of a mint quote
  553. pub fn payment_ids(&self) -> Vec<&String> {
  554. self.payments.iter().map(|a| &a.payment_id).collect()
  555. }
  556. /// Amount mintable
  557. /// Returns the amount that is still available for minting.
  558. ///
  559. /// The value is computed as the difference between the total amount that
  560. /// has been paid for this issuance (`self.amount_paid`) and the amount
  561. /// that has already been issued (`self.amount_issued`). In other words,
  562. pub fn amount_mintable(&self) -> Amount<CurrencyUnit> {
  563. self.amount_paid
  564. .checked_sub(&self.amount_issued)
  565. .unwrap_or_else(|_| Amount::new(0, self.unit.clone()))
  566. }
  567. /// Extracts and returns all pending changes, leaving the internal change tracker empty.
  568. ///
  569. /// This method is typically called by the database layer after loading or modifying a quote. It
  570. /// returns any accumulated changes (new payments, issuances) that need to be persisted, and
  571. /// clears the internal change buffer so that subsequent calls return `None` until new
  572. /// modifications are made.
  573. ///
  574. /// Returns `None` if no changes have been made since the last call to this method or since the
  575. /// quote was created/loaded.
  576. pub fn take_changes(&mut self) -> Option<MintQuoteChange> {
  577. self.changes.take()
  578. }
  579. /// Records a new payment received for this mint quote.
  580. ///
  581. /// This method validates the payment, updates the quote's internal state, and records the
  582. /// change for later persistence. The `amount_paid` counter is incremented and the payment is
  583. /// added to the change tracker for the database layer to persist.
  584. ///
  585. /// # Arguments
  586. ///
  587. /// * `amount` - The amount of the payment in the quote's currency unit. * `payment_id` - A
  588. /// unique identifier for this payment (e.g., lightning payment hash). * `time` - Optional Unix
  589. /// timestamp of when the payment was received. If `None`, the current time is used.
  590. ///
  591. /// # Errors
  592. ///
  593. /// Returns [`crate::Error::DuplicatePaymentId`] if a payment with the same ID has already been
  594. /// recorded for this quote.
  595. ///
  596. /// Returns [`crate::Error::AmountOverflow`] if adding the payment amount would cause an
  597. /// arithmetic overflow.
  598. #[instrument(skip(self))]
  599. pub fn add_payment(
  600. &mut self,
  601. amount: Amount<CurrencyUnit>,
  602. payment_id: String,
  603. time: Option<u64>,
  604. ) -> Result<(), crate::Error> {
  605. let time = time.unwrap_or_else(unix_time);
  606. let payment_ids = self.payment_ids();
  607. if payment_ids.contains(&&payment_id) {
  608. return Err(crate::Error::DuplicatePaymentId);
  609. }
  610. self.amount_paid = self
  611. .amount_paid
  612. .checked_add(&amount)
  613. .map_err(|_| crate::Error::AmountOverflow)?;
  614. let payment = IncomingPayment::new(amount, payment_id, time);
  615. self.payments.push(payment.clone());
  616. self.changes
  617. .get_or_insert_default()
  618. .payments
  619. .get_or_insert_default()
  620. .push(payment);
  621. Ok(())
  622. }
  623. /// Compute quote state
  624. #[instrument(skip(self))]
  625. fn compute_quote_state(&self) -> MintQuoteState {
  626. let zero_amount = Amount::new(0, self.unit.clone());
  627. if self.amount_paid == zero_amount && self.amount_issued == zero_amount {
  628. return MintQuoteState::Unpaid;
  629. }
  630. match self.amount_paid.value().cmp(&self.amount_issued.value()) {
  631. std::cmp::Ordering::Less => {
  632. tracing::error!("We should not have issued more then has been paid");
  633. MintQuoteState::Issued
  634. }
  635. std::cmp::Ordering::Equal => MintQuoteState::Issued,
  636. std::cmp::Ordering::Greater => MintQuoteState::Paid,
  637. }
  638. }
  639. }
  640. /// Mint Payments
  641. #[derive(Debug, Clone, Hash, PartialEq, Eq)]
  642. pub struct IncomingPayment {
  643. /// Amount
  644. pub amount: Amount<CurrencyUnit>,
  645. /// Pyament unix time
  646. pub time: u64,
  647. /// Payment id
  648. pub payment_id: String,
  649. }
  650. impl IncomingPayment {
  651. /// New [`IncomingPayment`]
  652. pub fn new(amount: Amount<CurrencyUnit>, payment_id: String, time: u64) -> Self {
  653. Self {
  654. payment_id,
  655. time,
  656. amount,
  657. }
  658. }
  659. }
  660. /// Information about issued quote
  661. #[derive(Debug, Clone, Hash, PartialEq, Eq)]
  662. pub struct Issuance {
  663. /// Amount
  664. pub amount: Amount<CurrencyUnit>,
  665. /// Time
  666. pub time: u64,
  667. }
  668. impl Issuance {
  669. /// Create new [`Issuance`]
  670. pub fn new(amount: Amount<CurrencyUnit>, time: u64) -> Self {
  671. Self { amount, time }
  672. }
  673. }
  674. /// Melt Quote Info
  675. #[derive(Debug, Clone, Hash, PartialEq, Eq)]
  676. pub struct MeltQuote {
  677. /// Quote id
  678. pub id: QuoteId,
  679. /// Quote unit
  680. pub unit: CurrencyUnit,
  681. /// Quote Payment request e.g. bolt11
  682. pub request: MeltPaymentRequest,
  683. /// Quote amount (typed for type safety)
  684. amount: Amount<CurrencyUnit>,
  685. /// Quote fee reserve (typed for type safety)
  686. fee_reserve: Amount<CurrencyUnit>,
  687. /// Quote state
  688. pub state: MeltQuoteState,
  689. /// Expiration time of quote
  690. pub expiry: u64,
  691. /// Payment preimage
  692. pub payment_preimage: Option<String>,
  693. /// Value used by ln backend to look up state of request
  694. pub request_lookup_id: Option<PaymentIdentifier>,
  695. /// Payment options
  696. ///
  697. /// Used for amountless invoices and MPP payments
  698. pub options: Option<MeltOptions>,
  699. /// Unix time quote was created
  700. pub created_time: u64,
  701. /// Unix time quote was paid
  702. pub paid_time: Option<u64>,
  703. /// Payment method
  704. pub payment_method: PaymentMethod,
  705. }
  706. impl MeltQuote {
  707. /// Create new [`MeltQuote`]
  708. #[allow(clippy::too_many_arguments)]
  709. pub fn new(
  710. request: MeltPaymentRequest,
  711. unit: CurrencyUnit,
  712. amount: Amount<CurrencyUnit>,
  713. fee_reserve: Amount<CurrencyUnit>,
  714. expiry: u64,
  715. request_lookup_id: Option<PaymentIdentifier>,
  716. options: Option<MeltOptions>,
  717. payment_method: PaymentMethod,
  718. ) -> Self {
  719. let id = Uuid::new_v4();
  720. Self {
  721. id: QuoteId::UUID(id),
  722. unit: unit.clone(),
  723. request,
  724. amount,
  725. fee_reserve,
  726. state: MeltQuoteState::Unpaid,
  727. expiry,
  728. payment_preimage: None,
  729. request_lookup_id,
  730. options,
  731. created_time: unix_time(),
  732. paid_time: None,
  733. payment_method,
  734. }
  735. }
  736. /// Quote amount
  737. #[inline]
  738. pub fn amount(&self) -> Amount<CurrencyUnit> {
  739. self.amount.clone()
  740. }
  741. /// Fee reserve
  742. #[inline]
  743. pub fn fee_reserve(&self) -> Amount<CurrencyUnit> {
  744. self.fee_reserve.clone()
  745. }
  746. /// Total amount needed (amount + fee_reserve)
  747. pub fn total_needed(&self) -> Result<Amount, crate::Error> {
  748. let total = self
  749. .amount
  750. .checked_add(&self.fee_reserve)
  751. .map_err(|_| crate::Error::AmountOverflow)?;
  752. Ok(Amount::from(total.value()))
  753. }
  754. /// Create MeltQuote from database fields (for deserialization)
  755. #[allow(clippy::too_many_arguments)]
  756. pub fn from_db(
  757. id: QuoteId,
  758. unit: CurrencyUnit,
  759. request: MeltPaymentRequest,
  760. amount: u64,
  761. fee_reserve: u64,
  762. state: MeltQuoteState,
  763. expiry: u64,
  764. payment_preimage: Option<String>,
  765. request_lookup_id: Option<PaymentIdentifier>,
  766. options: Option<MeltOptions>,
  767. created_time: u64,
  768. paid_time: Option<u64>,
  769. payment_method: PaymentMethod,
  770. ) -> Self {
  771. Self {
  772. id,
  773. unit: unit.clone(),
  774. request,
  775. amount: Amount::new(amount, unit.clone()),
  776. fee_reserve: Amount::new(fee_reserve, unit),
  777. state,
  778. expiry,
  779. payment_preimage,
  780. request_lookup_id,
  781. options,
  782. created_time,
  783. paid_time,
  784. payment_method,
  785. }
  786. }
  787. }
  788. /// Mint Keyset Info
  789. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  790. pub struct MintKeySetInfo {
  791. /// Keyset [`Id`]
  792. pub id: Id,
  793. /// Keyset [`CurrencyUnit`]
  794. pub unit: CurrencyUnit,
  795. /// Keyset active or inactive
  796. /// Mint will only issue new signatures on active keysets
  797. pub active: bool,
  798. /// Starting unix time Keyset is valid from
  799. pub valid_from: u64,
  800. /// [`DerivationPath`] keyset
  801. pub derivation_path: DerivationPath,
  802. /// DerivationPath index of Keyset
  803. pub derivation_path_index: Option<u32>,
  804. /// Supported amounts
  805. pub amounts: Vec<u64>,
  806. /// Input Fee ppk
  807. #[serde(default = "default_fee")]
  808. pub input_fee_ppk: u64,
  809. /// Final expiry
  810. pub final_expiry: Option<u64>,
  811. }
  812. /// Default fee
  813. pub fn default_fee() -> u64 {
  814. 0
  815. }
  816. impl From<MintKeySetInfo> for KeySetInfo {
  817. fn from(keyset_info: MintKeySetInfo) -> Self {
  818. Self {
  819. id: keyset_info.id,
  820. unit: keyset_info.unit,
  821. active: keyset_info.active,
  822. input_fee_ppk: keyset_info.input_fee_ppk,
  823. final_expiry: keyset_info.final_expiry,
  824. }
  825. }
  826. }
  827. impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
  828. fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<QuoteId> {
  829. MintQuoteBolt11Response {
  830. quote: mint_quote.id.clone(),
  831. state: mint_quote.state(),
  832. request: mint_quote.request,
  833. expiry: Some(mint_quote.expiry),
  834. pubkey: mint_quote.pubkey,
  835. amount: mint_quote.amount.map(Into::into),
  836. unit: Some(mint_quote.unit.clone()),
  837. }
  838. }
  839. }
  840. impl From<MintQuote> for MintQuoteBolt11Response<String> {
  841. fn from(quote: MintQuote) -> Self {
  842. let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
  843. quote.into()
  844. }
  845. }
  846. impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
  847. type Error = crate::Error;
  848. fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
  849. Ok(MintQuoteBolt12Response {
  850. quote: mint_quote.id.clone(),
  851. request: mint_quote.request,
  852. expiry: Some(mint_quote.expiry),
  853. amount_paid: Amount::from(mint_quote.amount_paid.value()),
  854. amount_issued: Amount::from(mint_quote.amount_issued.value()),
  855. pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
  856. amount: mint_quote.amount.map(Into::into),
  857. unit: mint_quote.unit,
  858. })
  859. }
  860. }
  861. impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
  862. type Error = crate::Error;
  863. fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
  864. let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
  865. Ok(quote.into())
  866. }
  867. }
  868. impl TryFrom<crate::mint::MintQuote> for crate::nuts::MintQuoteCustomResponse<QuoteId> {
  869. type Error = crate::Error;
  870. fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
  871. Ok(crate::nuts::MintQuoteCustomResponse {
  872. state: mint_quote.state(),
  873. quote: mint_quote.id.clone(),
  874. request: mint_quote.request,
  875. expiry: Some(mint_quote.expiry),
  876. pubkey: mint_quote.pubkey,
  877. amount: mint_quote.amount.map(Into::into),
  878. unit: Some(mint_quote.unit),
  879. extra: mint_quote.extra_json.unwrap_or_default(),
  880. })
  881. }
  882. }
  883. impl TryFrom<MintQuote> for crate::nuts::MintQuoteCustomResponse<String> {
  884. type Error = crate::Error;
  885. fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
  886. let quote: crate::nuts::MintQuoteCustomResponse<QuoteId> = quote.try_into()?;
  887. Ok(quote.into())
  888. }
  889. }
  890. impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
  891. fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
  892. MeltQuoteBolt11Response {
  893. quote: melt_quote.id.clone(),
  894. payment_preimage: None,
  895. change: None,
  896. state: melt_quote.state,
  897. expiry: melt_quote.expiry,
  898. amount: melt_quote.amount().clone().into(),
  899. fee_reserve: melt_quote.fee_reserve().clone().into(),
  900. request: None,
  901. unit: Some(melt_quote.unit.clone()),
  902. }
  903. }
  904. }
  905. impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
  906. fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
  907. MeltQuoteBolt11Response {
  908. quote: melt_quote.id.clone(),
  909. amount: melt_quote.amount().clone().into(),
  910. fee_reserve: melt_quote.fee_reserve().clone().into(),
  911. state: melt_quote.state,
  912. expiry: melt_quote.expiry,
  913. payment_preimage: melt_quote.payment_preimage,
  914. change: None,
  915. request: Some(melt_quote.request.to_string()),
  916. unit: Some(melt_quote.unit.clone()),
  917. }
  918. }
  919. }
  920. /// Payment request
  921. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  922. pub enum MeltPaymentRequest {
  923. /// Bolt11 Payment
  924. Bolt11 {
  925. /// Bolt11 invoice
  926. bolt11: Bolt11Invoice,
  927. },
  928. /// Bolt12 Payment
  929. Bolt12 {
  930. /// Offer
  931. #[serde(with = "offer_serde")]
  932. offer: Box<Offer>,
  933. },
  934. /// Custom payment method
  935. Custom {
  936. /// Payment method name
  937. method: String,
  938. /// Payment request string
  939. request: String,
  940. },
  941. }
  942. impl std::fmt::Display for MeltPaymentRequest {
  943. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  944. match self {
  945. MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
  946. MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
  947. MeltPaymentRequest::Custom { request, .. } => write!(f, "{request}"),
  948. }
  949. }
  950. }
  951. mod offer_serde {
  952. use std::str::FromStr;
  953. use serde::{self, Deserialize, Deserializer, Serializer};
  954. use super::Offer;
  955. pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
  956. where
  957. S: Serializer,
  958. {
  959. let s = offer.to_string();
  960. serializer.serialize_str(&s)
  961. }
  962. pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
  963. where
  964. D: Deserializer<'de>,
  965. {
  966. let s = String::deserialize(deserializer)?;
  967. Ok(Box::new(Offer::from_str(&s).map_err(|_| {
  968. serde::de::Error::custom("Invalid Bolt12 Offer")
  969. })?))
  970. }
  971. }