mint.rs 33 KB

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