Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

settle mint and melt quotes internally if invoice is the same #62

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mint/lightning/lightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Client interface {
type Invoice struct {
PaymentRequest string
PaymentHash string
Preimage string
Settled bool
Amount uint64
Expiry uint64
Expand Down
1 change: 1 addition & 0 deletions mint/lightning/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
123 changes: 92 additions & 31 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,19 +252,21 @@ 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 {
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)
}
}
}

Expand All @@ -284,12 +286,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
}
Expand Down Expand Up @@ -418,7 +429,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 {
Expand Down Expand Up @@ -450,10 +461,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,
Expand All @@ -462,8 +471,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)
Expand Down Expand Up @@ -530,26 +550,67 @@ 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 {
return storage.MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.LightningBackendErrCode)
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) {
// 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)
}

// if payment succeeded, mark melt quote as paid
// and invalidate proofs
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)
}

Expand Down
37 changes: 35 additions & 2 deletions mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,41 @@ func TestMelt(t *testing.T) {
if melt.State != nut05.Paid {
t.Fatal("got unexpected unpaid melt quote")
}
if melt.State != nut05.Paid {
t.Fatal("got unexpected unpaid melt quote")

// test internal quotes (mint and melt quotes with same invoice)
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)
}
keyset := testMint.GetActiveKeyset()
blindedMessages, _, _, err := testutils.CreateBlindedMessages(mintAmount, keyset)

proofs, err := testutils.GetValidProofsForAmount(mintAmount, testMint, lnd2)
if err != nil {
t.Fatalf("error generating valid proofs: %v", err)
}

meltQuote, err = testMint.RequestMeltQuote(testutils.BOLT11_METHOD, mintQuoteResponse.PaymentRequest, testutils.SAT_UNIT)
if err != nil {
t.Fatalf("got unexpected error in melt request: %v", err)
}
if meltQuote.FeeReserve != 0 {
t.Fatal("RequestMeltQuote did not return fee reserve of 0 for internal quote")
}

melt, err = testMint.MeltTokens(testutils.BOLT11_METHOD, meltQuote.Id, proofs)
if err != nil {
t.Fatalf("got unexpected error in melt: %v", err)
}
if len(melt.Preimage) == 0 {
t.Fatal("melt returned empty preimage")
}

// now mint should work because quote was settled internally
_, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Id, blindedMessages)
if err != nil {
t.Fatalf("got unexpected error in mint: %v", err)
}
}

Expand Down
22 changes: 22 additions & 0 deletions mint/storage/sqlite/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions mint/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion wallet/wallet_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading