From 3d125811f475d815a7bde63aa9648d1a681b5bf5 Mon Sep 17 00:00:00 2001 From: elnosh Date: Mon, 23 Sep 2024 14:26:45 -0500 Subject: [PATCH] settle mint and melt quotes internally if invoice is the same --- mint/lightning/lightning.go | 1 + mint/lightning/lnd.go | 1 + mint/mint.go | 129 +++++++++++++++++++++++------- mint/storage/sqlite/sqlite.go | 22 +++++ mint/storage/storage.go | 1 + wallet/wallet_integration_test.go | 2 +- 6 files changed, 124 insertions(+), 32 deletions(-) diff --git a/mint/lightning/lightning.go b/mint/lightning/lightning.go index 896d88b..e09523e 100644 --- a/mint/lightning/lightning.go +++ b/mint/lightning/lightning.go @@ -11,6 +11,7 @@ type Client interface { type Invoice struct { PaymentRequest string PaymentHash string + Preimage string Settled bool Amount uint64 Expiry uint64 diff --git a/mint/lightning/lnd.go b/mint/lightning/lnd.go index 1771601..3d55659 100644 --- a/mint/lightning/lnd.go +++ b/mint/lightning/lnd.go @@ -81,6 +81,7 @@ func (lnd *LndClient) InvoiceStatus(hash string) (Invoice, error) { invoice := Invoice{ PaymentRequest: lookupInvoiceResponse.PaymentRequest, PaymentHash: hash, + Preimage: hex.EncodeToString(lookupInvoiceResponse.RPreimage), Settled: invoiceSettled, Amount: uint64(lookupInvoiceResponse.Value), } diff --git a/mint/mint.go b/mint/mint.go index ff2d494..d9afa74 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -252,19 +252,22 @@ func (m *Mint) GetMintQuoteState(method, quoteId string) (storage.MintQuote, err return storage.MintQuote{}, cashu.QuoteNotExistErr } - // check if the invoice has been paid - status, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) - if err != nil { - msg := fmt.Sprintf("error getting status of payment request: %v", err) - return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) - } - - if status.Settled && mintQuote.State == nut04.Unpaid { - mintQuote.State = nut04.Paid - err := m.db.UpdateMintQuoteState(mintQuote.Id, mintQuote.State) + // if previously unpaid, check if invoice has been paid + if mintQuote.State == nut04.Unpaid { + // check if the invoice has been paid + status, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) if err != nil { - msg := fmt.Sprintf("error getting quote state: %v", err) - return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) + msg := fmt.Sprintf("error getting invoice status: %v", err) + return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) + } + + if status.Settled { + mintQuote.State = nut04.Paid + err := m.db.UpdateMintQuoteState(mintQuote.Id, mintQuote.State) + if err != nil { + msg := fmt.Sprintf("error getting quote state: %v", err) + return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) + } } } @@ -284,12 +287,21 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag } var blindedSignatures cashu.BlindedSignatures - status, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) - if err != nil { - msg := fmt.Sprintf("error getting status of payment request: %v", err) - return nil, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) + invoicePaid := false + if mintQuote.State == nut04.Unpaid { + invoiceStatus, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) + if err != nil { + msg := fmt.Sprintf("error getting invoice status: %v", err) + return nil, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) + } + if invoiceStatus.Settled { + invoicePaid = true + } + } else { + invoicePaid = true } - if status.Settled { + + if invoicePaid { if mintQuote.State == nut04.Issued { return nil, cashu.MintQuoteAlreadyIssued } @@ -418,7 +430,7 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) return blindedSignatures, nil } -// MeltRequest will process a request to melt tokens and return a MeltQuote. +// RequestMeltQuote will process a request to melt tokens and return a MeltQuote. // A melt is requested by a wallet to request the mint to pay an invoice. func (m *Mint) RequestMeltQuote(method, request, unit string) (storage.MeltQuote, error) { if method != BOLT11_METHOD { @@ -450,10 +462,8 @@ func (m *Mint) RequestMeltQuote(method, request, unit string) (storage.MeltQuote if err != nil { return storage.MeltQuote{}, cashu.StandardErr } - // Fee reserve that is required by the mint fee := m.lightningClient.FeeReserve(satAmount) - expiry := uint64(time.Now().Add(time.Minute * QuoteExpiryMins).Unix()) meltQuote := storage.MeltQuote{ Id: quoteId, @@ -462,8 +472,19 @@ func (m *Mint) RequestMeltQuote(method, request, unit string) (storage.MeltQuote Amount: satAmount, FeeReserve: fee, State: nut05.Unpaid, - Expiry: expiry, + Expiry: uint64(time.Now().Add(time.Minute * QuoteExpiryMins).Unix()), + } + + // check if a mint quote exists with the same invoice. + // if mint quote exists with same invoice, it can be + // settled internally so set the fee to 0 + mintQuote, err := m.db.GetMintQuoteByPaymentHash(bolt11.PaymentHash) + if err == nil { + meltQuote.InvoiceRequest = mintQuote.PaymentRequest + meltQuote.PaymentHash = mintQuote.PaymentHash + meltQuote.FeeReserve = 0 } + if err := m.db.SaveMeltQuote(meltQuote); err != nil { msg := fmt.Sprintf("error saving melt quote to db: %v", err) return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) @@ -530,26 +551,72 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage. return storage.MeltQuote{}, nut11.SigAllOnlySwap } - // if proofs are valid, ask the lightning backend - // to make the payment - preimage, err := m.lightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount) + // before asking backend to send payment, check if quotes can be settled + // internally (i.e mint and melt quotes exist with the same invoice) + mintQuote, err := m.db.GetMintQuoteByPaymentHash(meltQuote.PaymentHash) + if err == nil { + meltQuote, err = m.settleQuotesInternally(mintQuote, meltQuote) + if err != nil { + return storage.MeltQuote{}, err + } + } else { + // if quote can't be settled internally, ask backend to make payment + preimage, err := m.lightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount) + if err != nil { + return storage.MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.LightningBackendErrCode) + } + + // if payment succeeded, mark melt quote as paid + // and invalidate proofs + meltQuote.State = nut05.Paid + meltQuote.Preimage = preimage + err = m.db.UpdateMeltQuote(meltQuote.Id, meltQuote.Preimage, meltQuote.State) + if err != nil { + msg := fmt.Sprintf("error updating melt quote state: %v", err) + return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) + } + } + + err = m.db.SaveProofs(proofs) + if err != nil { + msg := fmt.Sprintf("error invalidating proofs. Could not save proofs to db: %v", err) + return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) + } + + return meltQuote, nil +} + +// if a pair of mint and melt quotes have the same invoice, +// settle them internally and update in db +func (m *Mint) settleQuotesInternally( + mintQuote storage.MintQuote, + meltQuote storage.MeltQuote, +) (storage.MeltQuote, error) { + invoice, err := m.lightningClient.InvoiceStatus(meltQuote.PaymentHash) if err != nil { - return storage.MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.LightningBackendErrCode) + msg := fmt.Sprintf("error getting invoice status from lightning backend: %v", err) + return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) } - // if payment succeeded, mark melt quote as paid - // and invalidate proofs + // need to get the invoice from the backend first to get the preimage + invoice, err = m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) + if err != nil { + msg := fmt.Sprintf("error getting invoice status from lightning backend: %v", err) + return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) + } meltQuote.State = nut05.Paid - meltQuote.Preimage = preimage + meltQuote.Preimage = invoice.Preimage err = m.db.UpdateMeltQuote(meltQuote.Id, meltQuote.Preimage, meltQuote.State) if err != nil { - msg := fmt.Sprintf("error getting quote state: %v", err) + msg := fmt.Sprintf("error updating melt quote state: %v", err) return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) } - err = m.db.SaveProofs(proofs) + // mark mint quote request as paid + mintQuote.State = nut04.Paid + err = m.db.UpdateMintQuoteState(mintQuote.Id, mintQuote.State) if err != nil { - msg := fmt.Sprintf("error invalidating proofs. Could not save proofs to db: %v", err) + msg := fmt.Sprintf("error updating mint quote state: %v", err) return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode) } diff --git a/mint/storage/sqlite/sqlite.go b/mint/storage/sqlite/sqlite.go index 182e458..24b5958 100644 --- a/mint/storage/sqlite/sqlite.go +++ b/mint/storage/sqlite/sqlite.go @@ -241,6 +241,28 @@ func (sqlite *SQLiteDB) GetMintQuote(quoteId string) (storage.MintQuote, error) return mintQuote, nil } +func (sqlite *SQLiteDB) GetMintQuoteByPaymentHash(paymentHash string) (storage.MintQuote, error) { + row := sqlite.db.QueryRow("SELECT * FROM mint_quotes WHERE payment_hash = ?", paymentHash) + + var mintQuote storage.MintQuote + var state string + + err := row.Scan( + &mintQuote.Id, + &mintQuote.PaymentRequest, + &mintQuote.PaymentHash, + &mintQuote.Amount, + &state, + &mintQuote.Expiry, + ) + if err != nil { + return storage.MintQuote{}, err + } + mintQuote.State = nut04.StringToState(state) + + return mintQuote, nil +} + func (sqlite *SQLiteDB) UpdateMintQuoteState(quoteId string, state nut04.State) error { updatedState := state.String() result, err := sqlite.db.Exec("UPDATE mint_quotes SET state = ? WHERE id = ?", updatedState, quoteId) diff --git a/mint/storage/storage.go b/mint/storage/storage.go index f76362e..46e70f4 100644 --- a/mint/storage/storage.go +++ b/mint/storage/storage.go @@ -21,6 +21,7 @@ type MintDB interface { SaveMintQuote(MintQuote) error GetMintQuote(string) (MintQuote, error) + GetMintQuoteByPaymentHash(string) (MintQuote, error) UpdateMintQuoteState(quoteId string, state nut04.State) error SaveMeltQuote(MeltQuote) error diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index abb009f..9d91f69 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -700,7 +700,7 @@ func TestSendToPubkey(t *testing.T) { p2pkMintURL := "http://127.0.0.1:8889" p2pkMintPath2 := filepath.Join(".", "p2pkmint2") - p2pkMint2, err := testutils.CreateTestMintServer(lnd2, "8890", p2pkMintPath, dbMigrationPath, 0) + p2pkMint2, err := testutils.CreateTestMintServer(lnd2, "8890", p2pkMintPath2, dbMigrationPath, 0) if err != nil { t.Fatal(err) }