Skip to content

Commit

Permalink
mint - add pending proofs to response from nut-07
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Oct 4, 2024
1 parent 95761be commit 47d55a2
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 10 deletions.
11 changes: 8 additions & 3 deletions mint/lightning/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"math"
"strings"
"time"

"github.com/lightningnetwork/lnd/lnrpc"
Expand Down Expand Up @@ -103,7 +104,8 @@ func (lnd *LndClient) SendPayment(ctx context.Context, request string, amount ui
if err != nil {
// if context deadline is exceeded (1 min), mark payment as pending
// if any other error, mark as failed
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
if errors.Is(ctx.Err(), context.DeadlineExceeded) ||
strings.Contains(err.Error(), "context deadline exceeded") {
return PaymentStatus{PaymentStatus: Pending}, nil
} else {
return PaymentStatus{PaymentStatus: Failed}, err
Expand Down Expand Up @@ -132,7 +134,8 @@ func (lnd *LndClient) OutgoingPaymentStatus(ctx context.Context, hash string) (P

trackPaymentStream, err := lnd.routerClient.TrackPaymentV2(ctx, &trackPaymentRequest)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
if errors.Is(ctx.Err(), context.DeadlineExceeded) ||
strings.Contains(err.Error(), "context deadline exceeded") {
return PaymentStatus{PaymentStatus: Pending}, nil
}
return PaymentStatus{PaymentStatus: Failed}, err
Expand All @@ -141,7 +144,9 @@ func (lnd *LndClient) OutgoingPaymentStatus(ctx context.Context, hash string) (P
// this should block until final payment update
payment, err := trackPaymentStream.Recv()
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
if errors.Is(ctx.Err(), context.DeadlineExceeded) ||
strings.Contains(err.Error(), "context deadline exceeded") {

return PaymentStatus{PaymentStatus: Pending}, nil
}
return PaymentStatus{PaymentStatus: Failed}, fmt.Errorf("payment failed: %w", err)
Expand Down
49 changes: 45 additions & 4 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,23 +908,64 @@ func (m *Mint) settleProofs(Ys []string, proofs cashu.Proofs) error {
}

func (m *Mint) ProofsStateCheck(Ys []string) ([]nut07.ProofState, error) {
usedProofs, err := m.db.GetProofsUsed(Ys)
// status of proofs that are pending due to an in-flight lightning payment
// could have changed so need to check with the lightning backend the status
// of the payment
pendingProofs, err := m.db.GetPendingProofs(Ys)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
errmsg := fmt.Sprintf("could not get used proofs from db: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
errmsg := fmt.Sprintf("could not get pending proofs from db: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

pendingQuotes := make(map[string]bool)
for _, pendingProof := range pendingProofs {
if !pendingQuotes[pendingProof.MeltQuoteId] {
pendingQuotes[pendingProof.MeltQuoteId] = true
}
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()

m.logDebugf("checking if status of pending proofs has changed")
for quoteId, _ := range pendingQuotes {
// GetMeltQuoteState will check the status of the quote
// and update the db tables (pending proofs, used proofs) appropriately
// if the status has changed
_, err := m.GetMeltQuoteState(ctx, BOLT11_METHOD, quoteId)
if err != nil {
return nil, err
}
}

// get pending proofs from db since they could have changed
// from checking the quote state
pendingProofs, err = m.db.GetPendingProofs(Ys)
if err != nil {
errmsg := fmt.Sprintf("could not get pending proofs from db: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

usedProofs, err := m.db.GetProofsUsed(Ys)
if err != nil {
errmsg := fmt.Sprintf("could not get used proofs from db: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

proofStates := make([]nut07.ProofState, len(Ys))
for i, y := range Ys {
state := nut07.Unspent

YSpent := slices.ContainsFunc(usedProofs, func(proof storage.DBProof) bool {
return proof.Y == y
})
YPending := slices.ContainsFunc(pendingProofs, func(proof storage.DBProof) bool {
return proof.Y == y
})
if YSpent {
state = nut07.Spent
} else if YPending {
state = nut07.Pending
}

proofStates[i] = nut07.ProofState{Y: y, State: state}
Expand Down
29 changes: 28 additions & 1 deletion mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,11 +680,27 @@ func TestPendingProofs(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error getting melt quote state: %v", err)
}

if meltQuote.State != nut05.Pending {
t.Fatalf("expected melt quote with state of '%s' but got '%s' instead", nut05.Pending, melt.State)
}

Ys := make([]string, len(validProofs))
for i, proof := range validProofs {
Y, _ := crypto.HashToCurve([]byte(proof.Secret))
Yhex := hex.EncodeToString(Y.SerializeCompressed())
Ys[i] = Yhex
}

states, err := testMint.ProofsStateCheck(Ys)
if err != nil {
t.Fatalf("unexpected error checking states of proofs: %v", err)
}
for _, proofState := range states {
if proofState.State != nut07.Pending {
t.Fatalf("expected pending proof but got '%s' instead", proofState.State)
}
}

_, err = testMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, validProofs)
if !errors.Is(err, cashu.MeltQuotePending) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.MeltQuotePending, err)
Expand Down Expand Up @@ -716,6 +732,17 @@ func TestPendingProofs(t *testing.T) {
if meltQuote.Preimage != expectedPreimage {
t.Fatalf("expected melt quote with preimage of '%v' but got '%v' instead", preimage, meltQuote.Preimage)
}

states, err = testMint.ProofsStateCheck(Ys)
if err != nil {
t.Fatalf("unexpected error checking states of proofs: %v", err)
}

for _, proofState := range states {
if proofState.State != nut07.Spent {
t.Fatalf("expected spent proof but got '%s' instead", proofState.State)
}
}
}

func TestProofsStateCheck(t *testing.T) {
Expand Down
11 changes: 10 additions & 1 deletion mint/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,16 @@ func (ms *MintServer) tokenStateCheck(rw http.ResponseWriter, req *http.Request)

proofStates, err := ms.mint.ProofsStateCheck(stateRequest.Ys)
if err != nil {
ms.writeErr(rw, req, cashu.StandardErr, err.Error())
cashuErr, ok := err.(*cashu.Error)
// note: if there was internal error from lightning backend
// or error from db, log that error but return generic response
if ok {
if cashuErr.Code == cashu.LightningBackendErrCode || cashuErr.Code == cashu.DBErrCode {
ms.writeErr(rw, req, cashu.StandardErr, cashuErr.Error())
return
}
}
ms.writeErr(rw, req, err)
return
}

Expand Down
3 changes: 2 additions & 1 deletion mint/storage/sqlite/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func (sqlite *SQLiteDB) AddPendingProofs(proofs cashu.Proofs, quoteId string) er

func (sqlite *SQLiteDB) GetPendingProofs(Ys []string) ([]storage.DBProof, error) {
proofs := []storage.DBProof{}
query := `SELECT y, amount, keyset_id, secret, c FROM pending_proofs WHERE y in (?` + strings.Repeat(",?", len(Ys)-1) + `)`
query := `SELECT * FROM pending_proofs WHERE y in (?` + strings.Repeat(",?", len(Ys)-1) + `)`

args := make([]any, len(Ys))
for i, y := range Ys {
Expand All @@ -259,6 +259,7 @@ func (sqlite *SQLiteDB) GetPendingProofs(Ys []string) ([]storage.DBProof, error)
&proof.Id,
&proof.Secret,
&proof.C,
&proof.MeltQuoteId,
)
if err != nil {
return nil, err
Expand Down
2 changes: 2 additions & 0 deletions mint/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type DBProof struct {
Secret string
Y string
C string
// for proofs in pending table
MeltQuoteId string
}

type MintQuote struct {
Expand Down

0 comments on commit 47d55a2

Please sign in to comment.