From cfe4e91af62dd0d11cb341e6c894594fee3a5420 Mon Sep 17 00:00:00 2001 From: elnosh Date: Tue, 2 Jul 2024 11:51:14 -0500 Subject: [PATCH 1/2] nut04 state changes --- cashu/nuts/nut04/nut04.go | 84 ++++++++++++++++++++++++++++++++++++++- mint/mint.go | 14 +++++-- mint/server.go | 4 +- wallet/wallet.go | 13 +----- 4 files changed, 97 insertions(+), 18 deletions(-) diff --git a/cashu/nuts/nut04/nut04.go b/cashu/nuts/nut04/nut04.go index 55557a9..a1632f7 100644 --- a/cashu/nuts/nut04/nut04.go +++ b/cashu/nuts/nut04/nut04.go @@ -3,7 +3,50 @@ // [NUT-04]: https://github.com/cashubtc/nuts/blob/main/04.md package nut04 -import "github.com/elnosh/gonuts/cashu" +import ( + "encoding/json" + + "github.com/elnosh/gonuts/cashu" +) + +type State int + +const ( + Unpaid State = iota + Paid + Pending + Issued + Unknown +) + +func (state State) String() string { + switch state { + case Unpaid: + return "UNPAID" + case Paid: + return "PAID" + case Pending: + return "PENDING" + case Issued: + return "ISSUED" + default: + return "unknown" + } +} + +func StringToState(state string) State { + switch state { + case "UNPAID": + return Unpaid + case "PAID": + return Paid + case "PENDING": + return Pending + case "ISSUED": + return Issued + } + return Unknown +} type PostMintQuoteBolt11Request struct { Amount uint64 `json:"amount"` @@ -13,7 +56,8 @@ type PostMintQuoteBolt11Request struct { type PostMintQuoteBolt11Response struct { Quote string `json:"quote"` Request string `json:"request"` - Paid bool `json:"paid"` + State State `json:"state"` + Paid bool `json:"paid"` // DEPRECATED: use State instead Expiry int64 `json:"expiry"` } @@ -25,3 +69,39 @@ type PostMintBolt11Request struct { type PostMintBolt11Response struct { Signatures cashu.BlindedSignatures `json:"signatures"` } + +type TempQuote struct { + Quote string `json:"quote"` + Request string `json:"request"` + State string `json:"state"` + Paid bool `json:"paid"` // DEPRECATED: use State instead + Expiry int64 `json:"expiry"` +} + +func (quoteResponse *PostMintQuoteBolt11Response) MarshalJSON() ([]byte, error) { + var tempQuote = TempQuote{ + Quote: quoteResponse.Quote, + Request: quoteResponse.Request, + State: quoteResponse.State.String(), + Paid: quoteResponse.Paid, + Expiry: quoteResponse.Expiry, + } + return json.Marshal(tempQuote) +} + +func (quoteResponse *PostMintQuoteBolt11Response) UnmarshalJSON(data []byte) error { + tempQuote := &TempQuote{} + + if err := json.Unmarshal(data, tempQuote); err != nil { + return err + } + + quoteResponse.Quote = tempQuote.Quote + quoteResponse.Request = tempQuote.Request + state := StringToState(tempQuote.State) + quoteResponse.State = state + quoteResponse.Paid = tempQuote.Paid + quoteResponse.Expiry = tempQuote.Expiry + + return nil +} diff --git a/mint/mint.go b/mint/mint.go index 15056ce..7d14916 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -119,7 +119,8 @@ func (m *Mint) RequestMintQuote(method string, amount uint64, unit string) (nut0 reqMintQuoteResponse := nut04.PostMintQuoteBolt11Response{ Quote: invoice.Id, Request: invoice.PaymentRequest, - Paid: invoice.Settled, + State: nut04.Unpaid, + Paid: invoice.Settled, // DEPRECATED: remove after wallets have upgraded Expiry: invoice.Expiry, } @@ -144,15 +145,22 @@ func (m *Mint) GetMintQuoteState(method, quoteId string) (nut04.PostMintQuoteBol msg := fmt.Sprintf("error getting invoice status: %v", err) return nut04.PostMintQuoteBolt11Response{}, cashu.BuildCashuError(msg, cashu.InvoiceErrCode) } - if status.Settled && status.Settled != invoice.Settled { + + state := nut04.Unpaid + if status.Settled { invoice.Settled = status.Settled + state = nut04.Paid + if invoice.Redeemed { + state = nut04.Issued + } m.db.SaveInvoice(*invoice) } quoteState := nut04.PostMintQuoteBolt11Response{ Quote: invoice.Id, Request: invoice.PaymentRequest, - Paid: invoice.Settled, + State: state, + Paid: invoice.Settled, // DEPRECATED: remove after wallets have upgraded Expiry: invoice.Expiry, } return quoteState, nil diff --git a/mint/server.go b/mint/server.go index eb4b079..372d1c7 100644 --- a/mint/server.go +++ b/mint/server.go @@ -219,7 +219,7 @@ func (ms *MintServer) mintRequest(rw http.ResponseWriter, req *http.Request) { return } - jsonRes, err := json.Marshal(reqMintResponse) + jsonRes, err := json.Marshal(&reqMintResponse) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) return @@ -246,7 +246,7 @@ func (ms *MintServer) mintQuoteState(rw http.ResponseWriter, req *http.Request) ms.writeErr(rw, req, err) return } - jsonRes, err := json.Marshal(mintQuoteStateResponse) + jsonRes, err := json.Marshal(&mintQuoteStateResponse) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) return diff --git a/wallet/wallet.go b/wallet/wallet.go index b2d9369..d1b680c 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -310,16 +310,6 @@ func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, return mintResponse, nil } -// CheckQuotePaid reports whether the mint quote has been paid -func (w *Wallet) CheckQuotePaid(quoteId string) bool { - mintQuote, err := GetMintQuoteState(w.currentMint.mintURL, quoteId) - if err != nil { - return false - } - - return mintQuote.Paid -} - // MintTokens will check whether if the mint quote has been paid. // If yes, it will create blinded messages that will send to the mint // to get the blinded signatures. @@ -330,7 +320,8 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { if err != nil { return nil, err } - if !mintQuote.Paid { + // TODO: remove usage of 'Paid' field after mints have upgraded + if !mintQuote.Paid || mintQuote.State == nut04.Unpaid { return nil, errors.New("invoice not paid") } From 05e6a825c31cc0144e10ce69285e10a188131d81 Mon Sep 17 00:00:00 2001 From: elnosh Date: Tue, 2 Jul 2024 12:58:34 -0500 Subject: [PATCH 2/2] tests for nut04 state changes --- cashu/nuts/nut04/nut04.go | 5 --- mint/mint_integration_test.go | 81 +++++++++++++++++++++++++++++++++++ wallet/wallet.go | 3 ++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/cashu/nuts/nut04/nut04.go b/cashu/nuts/nut04/nut04.go index a1632f7..e18ce1c 100644 --- a/cashu/nuts/nut04/nut04.go +++ b/cashu/nuts/nut04/nut04.go @@ -14,7 +14,6 @@ type State int const ( Unpaid State = iota Paid - Pending Issued Unknown ) @@ -25,8 +24,6 @@ func (state State) String() string { return "UNPAID" case Paid: return "PAID" - case Pending: - return "PENDING" case Issued: return "ISSUED" default: @@ -40,8 +37,6 @@ func StringToState(state string) State { return Unpaid case "PAID": return Paid - case "PENDING": - return Pending case "ISSUED": return Issued } diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index 036f6f3..05ea5be 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -14,6 +14,7 @@ import ( btcdocker "github.com/elnosh/btc-docker-test" "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint" @@ -112,6 +113,86 @@ func TestRequestMintQuote(t *testing.T) { } } +func TestMintQuoteState(t *testing.T) { + var mintAmount uint64 = 42000 + mintQuoteResponse, err := testMint.RequestMintQuote(testutils.BOLT11_METHOD, mintAmount, testutils.SAT_UNIT) + if err != nil { + t.Fatalf("error requesting mint quote: %v", err) + } + + var keyset crypto.Keyset + for _, k := range testMint.ActiveKeysets { + keyset = k + break + } + + // test invalid method + _, err = testMint.GetMintQuoteState("strike", mintQuoteResponse.Quote) + if !errors.Is(err, cashu.PaymentMethodNotSupportedErr) { + t.Fatalf("expected error '%v' but got '%v' instead", cashu.PaymentMethodNotSupportedErr, err) + } + + // test invalid quote + _, err = testMint.GetMintQuoteState(testutils.BOLT11_METHOD, "mintquote1234") + if !errors.Is(err, cashu.QuoteNotExistErr) { + t.Fatalf("expected error '%v' but got '%v' instead", cashu.QuoteNotExistErr, err) + } + + // test quote state before paying invoice + quoteStateResponse, err := testMint.GetMintQuoteState(testutils.BOLT11_METHOD, mintQuoteResponse.Quote) + if err != nil { + t.Fatalf("unexpected error getting quote state: %v", err) + } + if quoteStateResponse.Paid { + t.Fatalf("expected quote.Paid '%v' but got '%v' instead", false, quoteStateResponse.Paid) + } + if quoteStateResponse.State != nut04.Unpaid { + t.Fatalf("expected quote state '%v' but got '%v' instead", nut04.Unpaid.String(), quoteStateResponse.State.String()) + } + + //pay invoice + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: mintQuoteResponse.Request, + } + response, _ := lnd2.Client.SendPaymentSync(ctx, &sendPaymentRequest) + if len(response.PaymentError) > 0 { + t.Fatalf("error paying invoice: %v", response.PaymentError) + } + + // test quote state after paying invoice + quoteStateResponse, err = testMint.GetMintQuoteState(testutils.BOLT11_METHOD, mintQuoteResponse.Quote) + if err != nil { + t.Fatalf("unexpected error getting quote state: %v", err) + } + if !quoteStateResponse.Paid { + t.Fatalf("expected quote.Paid '%v' but got '%v' instead", true, quoteStateResponse.Paid) + } + if quoteStateResponse.State != nut04.Paid { + t.Fatalf("expected quote state '%v' but got '%v' instead", nut04.Paid.String(), quoteStateResponse.State.String()) + } + + blindedMessages, _, _, err := testutils.CreateBlindedMessages(mintAmount, keyset) + + // mint tokens + _, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Quote, blindedMessages) + if err != nil { + t.Fatalf("got unexpected error minting tokens: %v", err) + } + + // test quote state after minting tokens + quoteStateResponse, err = testMint.GetMintQuoteState(testutils.BOLT11_METHOD, mintQuoteResponse.Quote) + if err != nil { + t.Fatalf("unexpected error getting quote state: %v", err) + } + if !quoteStateResponse.Paid { + t.Fatalf("expected quote.Paid '%v' but got '%v' instead", true, quoteStateResponse.Paid) + } + if quoteStateResponse.State != nut04.Issued { + t.Fatalf("expected quote state '%v' but got '%v' instead", nut04.Issued.String(), quoteStateResponse.State.String()) + } + +} + func TestMintTokens(t *testing.T) { var mintAmount uint64 = 42000 mintQuoteResponse, err := testMint.RequestMintQuote(testutils.BOLT11_METHOD, mintAmount, testutils.SAT_UNIT) diff --git a/wallet/wallet.go b/wallet/wallet.go index d1b680c..b5db262 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -324,6 +324,9 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { if !mintQuote.Paid || mintQuote.State == nut04.Unpaid { return nil, errors.New("invoice not paid") } + if mintQuote.State == nut04.Issued { + return nil, errors.New("quote has already been issued") + } invoice, err := w.GetInvoiceByPaymentRequest(mintQuote.Request) if err != nil {