@@ -112,6 +112,19 @@ pub enum MintQuoteResponse<Q> {
112112 } ,
113113}
114114
115+ /// Errors from mint quote accounting validation.
116+ #[ derive( Debug , Clone , PartialEq , Eq , thiserror:: Error ) ]
117+ pub enum MintQuoteAccountingError {
118+ /// The response reports more issued ecash than paid amount.
119+ #[ error( "mint quote amount_issued ({amount_issued}) exceeds amount_paid ({amount_paid})" ) ]
120+ AmountIssuedExceedsAmountPaid {
121+ /// Amount paid to the mint.
122+ amount_paid : Amount ,
123+ /// Amount of ecash issued by the mint.
124+ amount_issued : Amount ,
125+ } ,
126+ }
127+
115128impl < Q > MintQuoteResponse < Q > {
116129 /// Returns the payment method for this response.
117130 pub fn method ( & self ) -> PaymentMethod {
@@ -145,20 +158,24 @@ impl<Q> MintQuoteResponse<Q> {
145158
146159 /// Returns the quote state derived from the response data.
147160 pub fn state ( & self ) -> Option < QuoteState > {
161+ self . try_state ( ) . ok ( )
162+ }
163+
164+ /// Returns the quote state derived from the response data, validating quote accounting.
165+ pub fn try_state ( & self ) -> Result < QuoteState , MintQuoteAccountingError > {
148166 match self {
149167 Self :: Bolt11 ( r) => {
150168 if r. amount_paid > Amount :: ZERO || r. amount_issued > Amount :: ZERO {
151- Some ( quote_state_from_amounts ( r. amount_paid , r. amount_issued ) )
169+ quote_state_from_amounts ( r. amount_paid , r. amount_issued )
152170 } else {
153- Some ( r. state )
171+ Ok ( r. state )
154172 }
155173 }
156- Self :: Bolt12 ( r) => Some ( quote_state_from_amounts ( r. amount_paid , r. amount_issued ) ) ,
157- Self :: Onchain ( r) => Some ( quote_state_from_amounts ( r. amount_paid , r. amount_issued ) ) ,
158- Self :: Custom { response, .. } => Some ( quote_state_from_amounts (
159- response. amount_paid ,
160- response. amount_issued ,
161- ) ) ,
174+ Self :: Bolt12 ( r) => quote_state_from_amounts ( r. amount_paid , r. amount_issued ) ,
175+ Self :: Onchain ( r) => quote_state_from_amounts ( r. amount_paid , r. amount_issued ) ,
176+ Self :: Custom { response, .. } => {
177+ quote_state_from_amounts ( response. amount_paid , response. amount_issued )
178+ }
162179 }
163180 }
164181
@@ -174,15 +191,26 @@ impl<Q> MintQuoteResponse<Q> {
174191}
175192
176193/// Derive the deprecated single-use mint quote state from canonical quote counters.
177- pub fn quote_state_from_amounts ( amount_paid : Amount , amount_issued : Amount ) -> QuoteState {
194+ pub fn quote_state_from_amounts (
195+ amount_paid : Amount ,
196+ amount_issued : Amount ,
197+ ) -> Result < QuoteState , MintQuoteAccountingError > {
198+ if amount_issued > amount_paid {
199+ return Err ( MintQuoteAccountingError :: AmountIssuedExceedsAmountPaid {
200+ amount_paid,
201+ amount_issued,
202+ } ) ;
203+ }
204+
178205 if amount_paid == Amount :: ZERO && amount_issued == Amount :: ZERO {
179- return QuoteState :: Unpaid ;
206+ return Ok ( QuoteState :: Unpaid ) ;
180207 }
181208
182- match amount_paid. cmp ( & amount_issued) {
183- std:: cmp:: Ordering :: Less | std:: cmp:: Ordering :: Equal => QuoteState :: Issued ,
184- std:: cmp:: Ordering :: Greater => QuoteState :: Paid ,
209+ if amount_paid == amount_issued {
210+ return Ok ( QuoteState :: Issued ) ;
185211 }
212+
213+ Ok ( QuoteState :: Paid )
186214}
187215
188216#[ cfg( test) ]
@@ -222,6 +250,14 @@ mod tests {
222250 custom_response( Amount :: from( 100 ) , Amount :: from( 100 ) ) . state( ) ,
223251 Some ( QuoteState :: Issued )
224252 ) ;
253+ assert_eq ! (
254+ custom_response( Amount :: from( 50 ) , Amount :: from( 100 ) ) . state( ) ,
255+ None
256+ ) ;
257+ assert ! ( matches!(
258+ custom_response( Amount :: from( 50 ) , Amount :: from( 100 ) ) . try_state( ) ,
259+ Err ( MintQuoteAccountingError :: AmountIssuedExceedsAmountPaid { .. } )
260+ ) ) ;
225261 }
226262
227263 #[ test]
0 commit comments