Skip to content

Commit

Permalink
settle mint and melt quotes internally if invoice is the same
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Sep 24, 2024
1 parent b2294c3 commit 3d12581
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 32 deletions.
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
129 changes: 98 additions & 31 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}

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

0 comments on commit 3d12581

Please sign in to comment.