diff --git a/cashu/cashu.go b/cashu/cashu.go index cada1ed..db5e9b0 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -174,6 +174,7 @@ var ( InvalidProofErr = Error{Detail: "invalid proof", Code: ProofsErrCode} InputsBelowOutputs = Error{Detail: "amount of input proofs is below amount of outputs", Code: ProofsErrCode} QuoteNotExistErr = Error{Detail: "quote does not exist", Code: QuoteErrCode} + QuoteAlreadyPaid = Error{Detail: "quote already paid", Code: QuoteErrCode} InsufficientProofsAmount = Error{Detail: "insufficient amount in proofs", Code: ProofsErrCode} InvalidKeysetProof = Error{Detail: "proof from an invalid keyset", Code: ProofsErrCode} InvalidSignatureRequest = Error{Detail: "requested signature from non-active keyset", Code: KeysetErrCode} diff --git a/cashu/nuts/nut05/nut05.go b/cashu/nuts/nut05/nut05.go index 8644023..e50277e 100644 --- a/cashu/nuts/nut05/nut05.go +++ b/cashu/nuts/nut05/nut05.go @@ -3,7 +3,33 @@ // [NUT-05]: https://github.com/cashubtc/nuts/blob/main/05.md package nut05 -import "github.com/elnosh/gonuts/cashu" +import ( + "encoding/json" + + "github.com/elnosh/gonuts/cashu" +) + +type State int + +const ( + Unpaid State = iota + Pending + Paid + Unknown +) + +func (state State) String() string { + switch state { + case Unpaid: + return "UNPAID" + case Pending: + return "PENDING" + case Paid: + return "PAID" + default: + return "unknown" + } +} type PostMeltQuoteBolt11Request struct { Request string `json:"request"` @@ -14,8 +40,10 @@ type PostMeltQuoteBolt11Response struct { Quote string `json:"quote"` Amount uint64 `json:"amount"` FeeReserve uint64 `json:"fee_reserve"` - Paid bool `json:"paid"` + State State `json:"state"` + Paid bool `json:"paid"` // DEPRECATED: use state instead Expiry int64 `json:"expiry"` + Preimage string `json:"payment_preimage,omitempty"` } type PostMeltBolt11Request struct { @@ -23,7 +51,24 @@ type PostMeltBolt11Request struct { Inputs cashu.Proofs `json:"inputs"` } -type PostMeltBolt11Response struct { - Paid bool `json:"paid"` - Preimage string `json:"payment_preimage"` +// Custom marshaler to display state as string +func (quoteResponse *PostMeltQuoteBolt11Response) MarshalJSON() ([]byte, error) { + var response = struct { + Quote string `json:"quote"` + Amount uint64 `json:"amount"` + FeeReserve uint64 `json:"fee_reserve"` + State string `json:"state"` + Paid bool `json:"paid"` // DEPRECATED: use state instead + Expiry int64 `json:"expiry"` + Preimage string `json:"payment_preimage,omitempty"` + }{ + Quote: quoteResponse.Quote, + Amount: quoteResponse.Amount, + FeeReserve: quoteResponse.FeeReserve, + State: quoteResponse.State.String(), + Paid: quoteResponse.Paid, + Expiry: quoteResponse.Expiry, + Preimage: quoteResponse.Preimage, + } + return json.Marshal(response) } diff --git a/mint/lightning/lnd.go b/mint/lightning/lnd.go index 90329b9..0effe12 100644 --- a/mint/lightning/lnd.go +++ b/mint/lightning/lnd.go @@ -111,13 +111,19 @@ func (lnd *LndClient) InvoiceStatus(hash string) (Invoice, error) { return Invoice{}, err } + invoiceSettled := lookupInvoiceResponse.State == lnrpc.Invoice_SETTLED invoice := Invoice{ PaymentRequest: lookupInvoiceResponse.PaymentRequest, PaymentHash: hash, - Settled: lookupInvoiceResponse.State == lnrpc.Invoice_SETTLED, + Settled: invoiceSettled, Amount: uint64(lookupInvoiceResponse.Value), } + if invoiceSettled { + preimage := hex.EncodeToString(lookupInvoiceResponse.RPreimage) + invoice.Preimage = preimage + } + return invoice, nil } diff --git a/mint/mint.go b/mint/mint.go index ab1da40..078341a 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -14,6 +14,7 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut04" + "github.com/elnosh/gonuts/cashu/nuts/nut05" "github.com/elnosh/gonuts/cashu/nuts/nut06" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint/lightning" @@ -246,9 +247,11 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) type MeltQuote struct { Id string InvoiceRequest string + PaymentHash string Amount uint64 FeeReserve uint64 - Paid bool + State nut05.State + Paid bool // DEPRECATED: use state instead Expiry int64 Preimage string } @@ -286,8 +289,10 @@ func (m *Mint) MeltRequest(method, request, unit string) (MeltQuote, error) { meltQuote := MeltQuote{ Id: hex.EncodeToString(hash[:]), InvoiceRequest: request, + PaymentHash: bolt11.PaymentHash, Amount: satAmount, FeeReserve: fee, + State: nut05.Unpaid, Paid: false, Expiry: expiry, } @@ -308,6 +313,20 @@ func (m *Mint) GetMeltQuoteState(method, quoteId string) (MeltQuote, error) { return MeltQuote{}, cashu.QuoteNotExistErr } + // if quote not paid, check status of payment with backend + if meltQuote.State == nut05.Unpaid { + invoice, err := m.LightningClient.InvoiceStatus(meltQuote.PaymentHash) + if err != nil { + return MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.StandardErr.Code) + } + if invoice.Settled { + meltQuote.Paid = true + meltQuote.State = nut05.Paid + meltQuote.Preimage = invoice.Preimage + m.db.SaveMeltQuote(*meltQuote) + } + } + return *meltQuote, nil } @@ -322,6 +341,9 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (MeltQuot if meltQuote == nil { return MeltQuote{}, cashu.QuoteNotExistErr } + if meltQuote.State == nut05.Paid { + return MeltQuote{}, cashu.QuoteAlreadyPaid + } proofsAmount := proofs.Amount() @@ -344,8 +366,11 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (MeltQuot // if payment succeeded, mark melt quote as paid // and invalidate proofs + meltQuote.State = nut05.Paid + // Deprecate Paid field in favor of State meltQuote.Paid = true meltQuote.Preimage = preimage + m.db.SaveMeltQuote(*meltQuote) for _, proof := range proofs { m.db.SaveProof(proof) } diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index 84c8b46..129703a 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -287,8 +287,9 @@ func TestMelt(t *testing.T) { // test already used proofs _, err = testMint.MeltTokens(testutils.BOLT11_METHOD, meltQuote.Id, validProofs) - if !errors.Is(err, cashu.ProofAlreadyUsedErr) { - t.Fatalf("expected error '%v' but got '%v' instead", cashu.ProofAlreadyUsedErr, err) + //if !errors.Is(err, cashu.ProofAlreadyUsedErr) { + if !errors.Is(err, cashu.QuoteAlreadyPaid) { + t.Fatalf("expected error '%v' but got '%v' instead", cashu.QuoteAlreadyPaid, err) } } diff --git a/mint/server.go b/mint/server.go index 234b400..205afc5 100644 --- a/mint/server.go +++ b/mint/server.go @@ -117,6 +117,8 @@ func (ms *MintServer) writeResponse( rw.Write(response) } +// errResponse is the error that will be written in the response +// errLogMsg is the error to log func (ms *MintServer) writeErr(rw http.ResponseWriter, req *http.Request, errResponse error, errLogMsg ...string) { code := http.StatusBadRequest @@ -308,10 +310,11 @@ func (ms *MintServer) meltQuoteRequest(rw http.ResponseWriter, req *http.Request return } - quoteResponse := nut05.PostMeltQuoteBolt11Response{ + quoteResponse := &nut05.PostMeltQuoteBolt11Response{ Quote: meltQuote.Id, Amount: meltQuote.Amount, FeeReserve: meltQuote.FeeReserve, + State: meltQuote.State, Paid: meltQuote.Paid, Expiry: meltQuote.Expiry, } @@ -336,12 +339,14 @@ func (ms *MintServer) meltQuoteState(rw http.ResponseWriter, req *http.Request) return } - quoteState := nut05.PostMeltQuoteBolt11Response{ + quoteState := &nut05.PostMeltQuoteBolt11Response{ Quote: meltQuote.Id, Amount: meltQuote.Amount, FeeReserve: meltQuote.FeeReserve, + State: meltQuote.State, Paid: meltQuote.Paid, Expiry: meltQuote.Expiry, + Preimage: meltQuote.Preimage, } jsonRes, err := json.Marshal(quoteState) @@ -368,19 +373,25 @@ func (ms *MintServer) meltTokens(rw http.ResponseWriter, req *http.Request) { if err != nil { cashuErr, ok := err.(*cashu.Error) if ok && cashuErr.Code == cashu.InvoiceErrCode { - ms.writeErr(rw, req, cashu.BuildCashuError("unable to send payment", cashu.InvoiceErrCode), cashuErr.Error()) + responseError := cashu.BuildCashuError("unable to send payment", cashu.InvoiceErrCode) + ms.writeErr(rw, req, responseError, cashuErr.Error()) return } ms.writeErr(rw, req, err) return } - meltTokenResponse := nut05.PostMeltBolt11Response{ - Paid: meltQuote.Paid, - Preimage: meltQuote.Preimage, + meltQuoteResponse := &nut05.PostMeltQuoteBolt11Response{ + Quote: meltQuote.Id, + Amount: meltQuote.Amount, + FeeReserve: meltQuote.FeeReserve, + State: meltQuote.State, + Paid: meltQuote.Paid, + Expiry: meltQuote.Expiry, + Preimage: meltQuote.Preimage, } - jsonRes, err := json.Marshal(meltTokenResponse) + jsonRes, err := json.Marshal(meltQuoteResponse) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) return diff --git a/wallet/client.go b/wallet/client.go index 9ac8290..8fb7ae7 100644 --- a/wallet/client.go +++ b/wallet/client.go @@ -32,7 +32,7 @@ func GetMintInfo(mintURL string) (*nut06.MintInfo, error) { var mintInfo nut06.MintInfo if err := json.Unmarshal(body, &mintInfo); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &mintInfo, nil @@ -52,7 +52,7 @@ func GetActiveKeysets(mintURL string) (*nut01.GetKeysResponse, error) { var keysetRes nut01.GetKeysResponse if err := json.Unmarshal(body, &keysetRes); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &keysetRes, nil @@ -72,7 +72,7 @@ func GetAllKeysets(mintURL string) (*nut02.GetKeysetsResponse, error) { var keysetsRes nut02.GetKeysetsResponse if err := json.Unmarshal(body, &keysetsRes); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &keysetsRes, nil @@ -92,7 +92,7 @@ func GetKeysetById(mintURL, id string) (*nut01.GetKeysResponse, error) { var keysetRes nut01.GetKeysResponse if err := json.Unmarshal(body, &keysetRes); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &keysetRes, nil @@ -118,7 +118,7 @@ func PostMintQuoteBolt11(mintURL string, mintQuoteRequest nut04.PostMintQuoteBol var reqMintResponse nut04.PostMintQuoteBolt11Response if err := json.Unmarshal(body, &reqMintResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &reqMintResponse, nil @@ -138,7 +138,7 @@ func GetMintQuoteState(mintURL, quoteId string) (*nut04.PostMintQuoteBolt11Respo var mintQuoteResponse nut04.PostMintQuoteBolt11Response if err := json.Unmarshal(body, &mintQuoteResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &mintQuoteResponse, nil @@ -164,7 +164,7 @@ func PostMintBolt11(mintURL string, mintRequest nut04.PostMintBolt11Request) ( var reqMintResponse nut04.PostMintBolt11Response if err := json.Unmarshal(body, &reqMintResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &reqMintResponse, nil @@ -189,7 +189,7 @@ func PostSwap(mintURL string, swapRequest nut03.PostSwapRequest) (*nut03.PostSwa var swapResponse nut03.PostSwapResponse if err := json.Unmarshal(body, &swapResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &swapResponse, nil @@ -216,14 +216,14 @@ func PostMeltQuoteBolt11(mintURL string, meltQuoteRequest nut05.PostMeltQuoteBol var meltQuoteResponse nut05.PostMeltQuoteBolt11Response if err := json.Unmarshal(body, &meltQuoteResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &meltQuoteResponse, nil } func PostMeltBolt11(mintURL string, meltRequest nut05.PostMeltBolt11Request) ( - *nut05.PostMeltBolt11Response, error) { + *nut05.PostMeltQuoteBolt11Response, error) { requestBody, err := json.Marshal(meltRequest) if err != nil { @@ -241,9 +241,9 @@ func PostMeltBolt11(mintURL string, meltRequest nut05.PostMeltBolt11Request) ( return nil, err } - var meltResponse nut05.PostMeltBolt11Response + var meltResponse nut05.PostMeltQuoteBolt11Response if err := json.Unmarshal(body, &meltResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &meltResponse, nil @@ -270,7 +270,7 @@ func PostCheckProofState(mintURL string, stateRequest nut07.PostCheckStateReques var stateResponse nut07.PostCheckStateResponse if err := json.Unmarshal(body, &stateResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &stateResponse, nil @@ -297,7 +297,7 @@ func PostRestore(mintURL string, restoreRequest nut09.PostRestoreRequest) ( var restoreResponse nut09.PostRestoreResponse if err := json.Unmarshal(body, &restoreResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v\n", err) + return nil, fmt.Errorf("error reading response from mint: %v", err) } return &restoreResponse, nil diff --git a/wallet/wallet.go b/wallet/wallet.go index f5023a1..3194421 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -520,7 +520,7 @@ func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { } // Melt will request the mint to pay the given invoice -func (w *Wallet) Melt(invoice string, mint string) (*nut05.PostMeltBolt11Response, error) { +func (w *Wallet) Melt(invoice string, mint string) (*nut05.PostMeltQuoteBolt11Response, error) { selectedMint, ok := w.mints[mint] if !ok { return nil, ErrMintNotExist @@ -543,12 +543,13 @@ func (w *Wallet) Melt(invoice string, mint string) (*nut05.PostMeltBolt11Respons if err != nil || !meltBolt11Response.Paid { // save proofs if invoice was not paid w.saveProofs(proofs) - } else if meltBolt11Response.Paid { // save invoice to db + } else if meltBolt11Response.Paid { // TODO: USE STATE FIELD INSTEAD OF PAID bolt11, err := decodepay.Decodepay(invoice) if err != nil { return nil, fmt.Errorf("error decoding bolt11 invoice: %v", err) } + // save invoice to db invoice := storage.Invoice{ TransactionType: storage.Melt, QuoteAmount: amountNeeded,