From bc6032348fe4e8c23f6e10d509b384a60ab46300 Mon Sep 17 00:00:00 2001 From: leonardo Date: Mon, 7 Oct 2024 12:24:17 +0200 Subject: [PATCH 01/13] separate into functions --- internal/database/main.go | 4 +- internal/routes/bolt11.go | 80 +++++++-------------------------------- internal/routes/mint.go | 57 +++++++--------------------- internal/utils/proofs.go | 69 +++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 112 deletions(-) create mode 100644 internal/utils/proofs.go diff --git a/internal/database/main.go b/internal/database/main.go index d0d01bb..6547592 100644 --- a/internal/database/main.go +++ b/internal/database/main.go @@ -323,12 +323,12 @@ func GetMeltQuoteById(pool *pgxpool.Pool, id string) (cashu.MeltRequestDB, error return quote, nil } -func CheckListOfProofs(pool *pgxpool.Pool, CList []string, SecretList []string) ([]cashu.Proof, error) { +func CheckListOfProofs(pool *pgxpool.Pool, SecretList []string) ([]cashu.Proof, error) { var proofList []cashu.Proof ctx := context.Background() - rows, err := pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE C = ANY($1) OR secret = ANY($2)", CList, SecretList) + rows, err := pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE secret = ANY($1)", SecretList) defer rows.Close() diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index c4fa556..e987aa3 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -485,27 +485,11 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - var CList, SecretList []string - var AmountProofs uint64 - - // check proof have the same amount as blindedSignatures - for i, proof := range meltRequest.Inputs { - AmountProofs += proof.Amount - CList = append(CList, proof.C) - SecretList = append(SecretList, proof.Secret) - - p, err := proof.HashSecretToCurve() - - if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) - logger.Info(fmt.Sprintf("proof.HashSecretToCurve(): %+v", err)) - c.JSON(400, "Problem processing proofs") - return - } - - meltRequest.Inputs[i] = p - mint.PendingProofs = append(mint.PendingProofs, p) - + AmountProofs, SecretsList, err := utils.GetProofsValues(&meltRequest.Inputs) + if err != nil { + logger.Warn("utils.GetProofsValues(&meltRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) + c.JSON(400, "Problem processing proofs") + return } if AmountProofs < (quote.Amount + quote.FeeReserve + uint64(fee)) { @@ -516,7 +500,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * } // check if we know any of the proofs - knownProofs, err := database.CheckListOfProofs(pool, CList, SecretList) + knownProofs, err := database.CheckListOfProofs(pool, SecretsList) if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) @@ -537,27 +521,8 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Debug("Could not verify Proofs", slog.String(utils.LogExtraInfo, err.Error())) - - switch { - case errors.Is(err, cashu.ErrEmptyWitness): - c.JSON(403, "Empty Witness") - return - case errors.Is(err, cashu.ErrNoValidSignatures): - c.JSON(403, cashu.ErrorCodeToResponse(cashu.TOKEN_NOT_VERIFIED, nil)) - return - case errors.Is(err, cashu.ErrNotEnoughSignatures): - c.JSON(403, cashu.ErrorCodeToResponse(cashu.TOKEN_NOT_VERIFIED, nil)) - return - case errors.Is(err, cashu.ErrLocktimePassed): - c.JSON(403, cashu.ErrLocktimePassed.Error()) - return - case errors.Is(err, cashu.ErrInvalidPreimage): - c.JSON(403, cashu.ErrInvalidPreimage.Error()) - return - - } - - c.JSON(403, "Invalid Proof") + errorCode, details := utils.ParseVerifyProofError(err) + c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) return } @@ -570,7 +535,6 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * } var paidLightningFeeSat uint64 - var changeResponse []cashu.BlindSignature payment, err := mint.LightningBackend.PayInvoice(quote.Request, invoice, quote.FeeReserve, quote.Mpp, quote.Amount) @@ -621,6 +585,9 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * quote.PaymentPreimage = payment.Preimage } + quote.Melted = true + response := quote.GetPostMeltQuoteResponse() + // if fees where lower than expected return sats to the user paidLightningFeeSat = uint64(payment.PaidFeeSat) @@ -628,24 +595,9 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * // change is returned totalExpent := quote.Amount + paidLightningFeeSat + uint64(fee) if AmountProofs > totalExpent && len(meltRequest.Outputs) > 0 { - overpaidFees := AmountProofs - totalExpent - - amounts := cashu.AmountSplit(overpaidFees) - change := meltRequest.Outputs - switch { - case len(amounts) > len(meltRequest.Outputs): - for i := range change { - change[i].Amount = amounts[i] - } - default: - change = change[:len(amounts)] - - for i := range change { - change[i].Amount = amounts[i] - } - - } + overpaidFees := AmountProofs - totalExpent + change := utils.GetChangeOutput(overpaidFees, meltRequest.Outputs) blindSignatures, recoverySigsDb, err := mint.SignBlindedMessages(change, quote.Unit) if err != nil { @@ -663,13 +615,9 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * logger.Error("recoverySigsDb", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", recoverySigsDb))) } - changeResponse = blindSignatures + response.Change = blindSignatures } - quote.Melted = true - response := quote.GetPostMeltQuoteResponse() - response.Change = changeResponse - err = database.ModifyQuoteMeltPayStatusAndMelted(pool, quote.RequestPaid, quote.Melted, quote.State, quote.Quote) if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) diff --git a/internal/routes/mint.go b/internal/routes/mint.go index 3f5b0ae..bf854da 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -1,18 +1,15 @@ package routes import ( - "errors" "fmt" - "log/slog" - "slices" - "time" - "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" "github.com/lescuer97/nutmix/internal/database" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" + "log/slog" + "slices" ) func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) { @@ -199,8 +196,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. return } - var AmountProofs, AmountSignature uint64 - var CList, SecretsList []string + var AmountSignature uint64 if len(swapRequest.Inputs) == 0 || len(swapRequest.Outputs) == 0 { logger.Info("Inputs or Outputs are empty") @@ -208,27 +204,17 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. return } - now := time.Now().Unix() - // check proof have the same amount as blindedSignatures - for i, proof := range swapRequest.Inputs { - AmountProofs += proof.Amount - CList = append(CList, proof.C) - SecretsList = append(SecretsList, proof.Secret) - - p, err := proof.HashSecretToCurve() - - if err != nil { - logger.Warn("proof.HashSecretToCurve()", slog.String(utils.LogExtraInfo, err.Error())) - c.JSON(400, "Problem processing proofs") - return - } - swapRequest.Inputs[i] = p - swapRequest.Inputs[i].SeenAt = now + AmountProofs, SecretsList, err := utils.GetProofsValues(&swapRequest.Inputs) + if err != nil { + logger.Warn("utils.GetProofsValues(&swapRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) + c.JSON(400, "Problem processing proofs") + return } for _, output := range swapRequest.Outputs { AmountSignature += output.Amount } + unit, err := mint.CheckProofsAreSameUnit(swapRequest.Inputs) if err != nil { @@ -263,10 +249,10 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. } // check if we know any of the proofs - knownProofs, err := database.CheckListOfProofs(pool, CList, SecretsList) + knownProofs, err := database.CheckListOfProofs(pool, SecretsList) if err != nil { - logger.Error("database.CheckListOfProofs(pool, CList, SecretsList)", slog.String(utils.LogExtraInfo, err.Error())) + logger.Error("database.CheckListOfProofs(pool, SecretsList)", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(400, cashu.ErrorCodeToResponse(cashu.KEYSET_NOT_KNOW, nil)) return } @@ -282,25 +268,8 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Warn("mint.VerifyListOfProofs", slog.String(utils.LogExtraInfo, err.Error())) - switch { - case errors.Is(err, cashu.ErrEmptyWitness): - c.JSON(403, "Empty Witness") - return - case errors.Is(err, cashu.ErrNoValidSignatures): - c.JSON(403, cashu.ErrorCodeToResponse(cashu.TOKEN_NOT_VERIFIED, nil)) - return - case errors.Is(err, cashu.ErrNotEnoughSignatures): - c.JSON(403, cashu.ErrorCodeToResponse(cashu.TOKEN_NOT_VERIFIED, nil)) - return - case errors.Is(err, cashu.ErrLocktimePassed): - c.JSON(403, cashu.ErrLocktimePassed.Error()) - return - case errors.Is(err, cashu.ErrInvalidPreimage): - c.JSON(403, cashu.ErrInvalidPreimage.Error()) - return - } - - c.JSON(403, cashu.ErrorCodeToResponse(cashu.TOKEN_NOT_VERIFIED, nil)) + errorCode, details := utils.ParseVerifyProofError(err) + c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) return } diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go new file mode 100644 index 0000000..51ab2b8 --- /dev/null +++ b/internal/utils/proofs.go @@ -0,0 +1,69 @@ +package utils + +import ( + "errors" + "fmt" + "time" + + "github.com/lescuer97/nutmix/api/cashu" +) + +func ParseVerifyProofError(proofError error) (cashu.ErrorCode, *string) { + switch { + case errors.Is(proofError, cashu.ErrEmptyWitness): + + message := "Empty Witness" + return cashu.UNKNOWN, &message + case errors.Is(proofError, cashu.ErrNoValidSignatures): + return cashu.TOKEN_NOT_VERIFIED, nil + case errors.Is(proofError, cashu.ErrNotEnoughSignatures): + return cashu.TOKEN_NOT_VERIFIED, nil + case errors.Is(proofError, cashu.ErrLocktimePassed): + message := cashu.ErrLocktimePassed.Error() + return cashu.UNKNOWN, &message + case errors.Is(proofError, cashu.ErrInvalidPreimage): + message := cashu.ErrInvalidPreimage.Error() + return cashu.UNKNOWN, &message + } + + return cashu.TOKEN_NOT_VERIFIED, nil + +} +func GetChangeOutput(overpaidFees uint64, outputs []cashu.BlindedMessage) []cashu.BlindedMessage { + amounts := cashu.AmountSplit(overpaidFees) + switch { + case len(amounts) > len(outputs): + for i := range outputs { + outputs[i].Amount = amounts[i] + } + + default: + outputs = outputs[:len(amounts)] + + for i := range outputs { + outputs[i].Amount = amounts[i] + } + + } + return outputs +} + +func GetProofsValues(proofs *[]cashu.Proof) (uint64, []string, error) { + now := time.Now().Unix() + var totalAmount uint64 + var SecretsList []string + for i, proof := range *proofs { + totalAmount += proof.Amount + SecretsList = append(SecretsList, proof.Secret) + + p, err := proof.HashSecretToCurve() + + if err != nil { + return 0, SecretsList, fmt.Errorf("proof.HashSecretToCurve(). %w", err) + } + (*proofs)[i] = p + (*proofs)[i].SeenAt = now + } + + return totalAmount, SecretsList, nil +} From 0ab7639f3b57ab0935ea5c8f2515ea4cfe6311ed Mon Sep 17 00:00:00 2001 From: leonardo Date: Mon, 7 Oct 2024 17:22:20 +0200 Subject: [PATCH 02/13] fix testing add testing to proofs --- api/cashu/keys.go | 5 +- cmd/nutmix/htlc_route_test.go | 37 ++++++++++-- cmd/nutmix/main_test.go | 15 ++++- cmd/nutmix/p2pk_route_test.go | 10 +++- internal/routes/bolt11.go | 3 +- internal/routes/mint.go | 2 +- internal/utils/proofs.go | 5 +- internal/utils/proofs_test.go | 109 ++++++++++++++++++++++++++++++++++ 8 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 internal/utils/proofs_test.go diff --git a/api/cashu/keys.go b/api/cashu/keys.go index a6410c0..c427495 100644 --- a/api/cashu/keys.go +++ b/api/cashu/keys.go @@ -4,11 +4,10 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "math" - "time" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/tyler-smith/go-bip32" + "math" + "time" ) func DeriveKeysetId(keysets []Keyset) (string, error) { diff --git a/cmd/nutmix/htlc_route_test.go b/cmd/nutmix/htlc_route_test.go index a0d1bd1..bfdb272 100644 --- a/cmd/nutmix/htlc_route_test.go +++ b/cmd/nutmix/htlc_route_test.go @@ -248,7 +248,17 @@ func TestRoutesHTLCSwapMelt(t *testing.T) { t.Fatalf("Expected status code 403, got %d", w.Code) } - if w.Body.String() != `"Invalid preimage"` { + var errorRes cashu.ErrorResponse + err = json.Unmarshal(w.Body.Bytes(), &errorRes) + if err != nil { + t.Fatalf("json.Unmarshal(w.Body.Bytes(), &errorRes): %v", err) + } + + if errorRes.Code != cashu.UNKNOWN { + t.Errorf("Expected Invalid Proof, got %s", w.Body.String()) + } + + if *errorRes.Detail != `Invalid preimage` { t.Fatalf("Expected response Invalid preimage, got %s", w.Body.String()) } @@ -557,7 +567,17 @@ func TestHTLCMultisigSigning(t *testing.T) { t.Fatalf("Expected status code 403, got %d", w.Code) } - if w.Body.String() != `"Locktime has passed and no refund key was found"` { + var errorRes cashu.ErrorResponse + err = json.Unmarshal(w.Body.Bytes(), &errorRes) + if err != nil { + t.Fatalf("json.Unmarshal(w.Body.Bytes(), &errorRes): %v", err) + } + + if errorRes.Code != cashu.UNKNOWN { + t.Errorf("Expected Invalid Proof, got %s", w.Body.String()) + } + + if *errorRes.Detail != `Locktime has passed and no refund key was found` { t.Fatalf("Expected response No valid signatures, got %s", w.Body.String()) } @@ -710,8 +730,17 @@ func TestHTLCMultisigSigning(t *testing.T) { t.Fatalf("Expected status code 403, got %d", w.Code) } - if w.Body.String() != `"Invalid preimage"` { - t.Fatalf("Expected response No valid signatures, got %s", w.Body.String()) + err = json.Unmarshal(w.Body.Bytes(), &errorRes) + if err != nil { + t.Fatalf("json.Unmarshal(w.Body.Bytes(), &errorRes): %v", err) + } + + if errorRes.Code != cashu.UNKNOWN { + t.Errorf("Expected Invalid Proof, got %s", w.Body.String()) + } + + if *errorRes.Detail != `Invalid preimage` { + t.Fatalf("Expected response Invalid preimage, got %s", w.Body.String()) } // Try swapping with correct signatures and correct preimage diff --git a/cmd/nutmix/main_test.go b/cmd/nutmix/main_test.go index 5473d8e..c00de26 100644 --- a/cmd/nutmix/main_test.go +++ b/cmd/nutmix/main_test.go @@ -577,8 +577,13 @@ func TestMintBolt11FakeWallet(t *testing.T) { if w.Code != 403 { t.Errorf("Expected status code 403, got %d", w.Code) } + var errorRes cashu.ErrorResponse + err = json.Unmarshal(w.Body.Bytes(), &errorRes) + if err != nil { + t.Fatalf("json.Unmarshal(w.Body.Bytes(), &errorRes): %v", err) + } - if w.Body.String() != `"Invalid Proof"` { + if errorRes.Code != cashu.TOKEN_NOT_VERIFIED { t.Errorf("Expected Invalid Proof, got %s", w.Body.String()) } @@ -1485,7 +1490,13 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer t.Errorf("Expected status code 403, got %d", w.Code) } - if w.Body.String() != `"Invalid Proof"` { + var errorRes cashu.ErrorResponse + err = json.Unmarshal(w.Body.Bytes(), &errorRes) + if err != nil { + t.Fatalf("json.Unmarshal(w.Body.Bytes(), &errorRes): %v", err) + } + + if errorRes.Code != cashu.TOKEN_NOT_VERIFIED { t.Errorf("Expected Invalid Proof, got %s", w.Body.String()) } diff --git a/cmd/nutmix/p2pk_route_test.go b/cmd/nutmix/p2pk_route_test.go index 23ac054..1e5dff4 100644 --- a/cmd/nutmix/p2pk_route_test.go +++ b/cmd/nutmix/p2pk_route_test.go @@ -501,8 +501,14 @@ func TestP2PKMultisigSigning(t *testing.T) { t.Fatalf("Expected status code 403, got %d", w.Code) } - if w.Body.String() != `"Locktime has passed and no refund key was found"` { - t.Fatalf("Expected response No valid signatures, got %s", w.Body.String()) + var errorRes cashu.ErrorResponse + err = json.Unmarshal(w.Body.Bytes(), &errorRes) + if err != nil { + t.Fatalf("Error generating proofs: %v", err) + } + + if *errorRes.Detail != `Locktime has passed and no refund key was found` { + t.Fatalf("Expected response No valid signatures, got %s", *errorRes.Detail) } // TRY SWAPPING with refund key diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index e987aa3..1135150 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -485,7 +485,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - AmountProofs, SecretsList, err := utils.GetProofsValues(&meltRequest.Inputs) + AmountProofs, SecretsList, err := utils.GetAndCalculateProofsValues(&meltRequest.Inputs) if err != nil { logger.Warn("utils.GetProofsValues(&meltRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(400, "Problem processing proofs") @@ -598,6 +598,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * overpaidFees := AmountProofs - totalExpent change := utils.GetChangeOutput(overpaidFees, meltRequest.Outputs) + blindSignatures, recoverySigsDb, err := mint.SignBlindedMessages(change, quote.Unit) if err != nil { diff --git a/internal/routes/mint.go b/internal/routes/mint.go index bf854da..b805bf1 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -204,7 +204,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. return } - AmountProofs, SecretsList, err := utils.GetProofsValues(&swapRequest.Inputs) + AmountProofs, SecretsList, err := utils.GetAndCalculateProofsValues(&swapRequest.Inputs) if err != nil { logger.Warn("utils.GetProofsValues(&swapRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(400, "Problem processing proofs") diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go index 51ab2b8..9aadcfb 100644 --- a/internal/utils/proofs.go +++ b/internal/utils/proofs.go @@ -29,8 +29,11 @@ func ParseVerifyProofError(proofError error) (cashu.ErrorCode, *string) { return cashu.TOKEN_NOT_VERIFIED, nil } + func GetChangeOutput(overpaidFees uint64, outputs []cashu.BlindedMessage) []cashu.BlindedMessage { amounts := cashu.AmountSplit(overpaidFees) + // if there are more outputs then amount to change. + // we size down the total amount of blind messages switch { case len(amounts) > len(outputs): for i := range outputs { @@ -48,7 +51,7 @@ func GetChangeOutput(overpaidFees uint64, outputs []cashu.BlindedMessage) []cash return outputs } -func GetProofsValues(proofs *[]cashu.Proof) (uint64, []string, error) { +func GetAndCalculateProofsValues(proofs *[]cashu.Proof) (uint64, []string, error) { now := time.Now().Unix() var totalAmount uint64 var SecretsList []string diff --git a/internal/utils/proofs_test.go b/internal/utils/proofs_test.go new file mode 100644 index 0000000..4f99bb5 --- /dev/null +++ b/internal/utils/proofs_test.go @@ -0,0 +1,109 @@ +package utils + +import ( + "github.com/lescuer97/nutmix/api/cashu" + "testing" +) + +func setListofEmptyBlindMessages(amounts int) []cashu.BlindedMessage { + var messages []cashu.BlindedMessage + for i := 0; i < amounts; i++ { + message := cashu.BlindedMessage{ + Id: "mockid", + Amount: 0, + } + messages = append(messages, message) + + } + + return messages +} +func TestGetChangeWithEnoughBlindMessages(t *testing.T) { + + emptyBlindMessages := setListofEmptyBlindMessages(10) + + // create change for value of 2 + change := GetChangeOutput(2, emptyBlindMessages) + + if len(change) != 1 { + t.Errorf("Incorrect size for change slice %v, should be 1", len(change)) + + } + + if change[0].Amount != 2 { + t.Errorf("Incorrect amount for change slice %v, should be 2", change[0].Amount) + } + + // create change for a 0 amount + change = GetChangeOutput(0, emptyBlindMessages) + + if len(change) != 0 { + t.Errorf("Incorrect size for change slice %v, should be 0", len(change)) + } + +} + +func TestGetChangeWithOutEnoughBlindMessages(t *testing.T) { + + emptyBlindMessages := setListofEmptyBlindMessages(1) + + // create change for value of 2 + change := GetChangeOutput(10, emptyBlindMessages) + + if len(change) != 1 { + t.Errorf("Incorrect size for change slice %v, should be 1", len(change)) + } + + if change[0].Amount != 2 { + t.Errorf("Incorrect amount for change slice %v, should be 2", change[0].Amount) + } + +} + +func MakeListofMockProofs(amounts int) []cashu.Proof { + + var proofs []cashu.Proof + for i := 0; i < amounts; i++ { + proof := cashu.Proof{ + Id: "mockid", + Amount: 0, + } + proofs = append(proofs, proof) + + } + + return proofs +} + +func TestGetValuesFromProofs(t *testing.T) { + + listOfProofs := []cashu.Proof{ + { + Id: "mockid", + Amount: 2, + Secret: "mockSecret", + }, + { + Id: "mockid", + Amount: 6, + Secret: "mockSecret2", + }, + } + + TotalAmount, secretsList, err := GetAndCalculateProofsValues(&listOfProofs) + if err != nil { + t.Fatal("GetAndCalculateProofsValues(&listOfProofs)") + } + + if TotalAmount != 8 { + t.Errorf("Incorrect total amount %v. Should be 8", TotalAmount) + } + + if secretsList[0] != "mockSecret" { + t.Errorf("Incorrect total amount %v. Should be 8", TotalAmount) + } + if listOfProofs[0].Y != "02aa4a2c024e41bd87e8c2758d5a7c2d81e09afe52f67fc8a69768bd73d515e28f" { + t.Errorf("Incorrect Y: %v. ", listOfProofs[0].Y) + } + +} From e603461a5d5ec640468b50751290682a762a1b98 Mon Sep 17 00:00:00 2001 From: leonardo Date: Thu, 10 Oct 2024 11:58:55 +0200 Subject: [PATCH 03/13] fix payment status and quote check --- internal/lightning/cln.go | 4 ++-- internal/routes/bolt11.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/lightning/cln.go b/internal/lightning/cln.go index 0b55f04..8ee3cb2 100644 --- a/internal/lightning/cln.go +++ b/internal/lightning/cln.go @@ -228,7 +228,7 @@ func (l CLNGRPCWallet) CheckPayed(quote string) (PaymentStatus, string, error) { case pay.Status == cln_grpc.ListpaysPays_PENDING: return PENDING, hex.EncodeToString(pay.PaymentHash), nil case pay.Status == cln_grpc.ListpaysPays_FAILED: - return SETTLED, hex.EncodeToString(pay.PaymentHash), nil + return FAILED, hex.EncodeToString(pay.PaymentHash), nil } @@ -288,7 +288,7 @@ func (l CLNGRPCWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mp return fee, nil } -func (l CLNGRPCWallet) RequestInvoice(amount int64) (InvoiceResponse, error) { +func (l CLNGRPCWallet) ReqSETTLEDuestInvoice(amount int64) (InvoiceResponse, error) { var response InvoiceResponse ctx := metadata.AppendToOutgoingContext(context.Background(), "macaroon", l.macaroon) diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index 1135150..cd6835c 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -448,7 +448,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * if quote.State == cashu.PENDING { mint.RemoveQuotesAndProofs(meltRequest.Quote, meltRequest.Inputs) logger.Warn("Quote is pending") - c.JSON(400, cashu.ErrorCodeToResponse(cashu.INVOICE_ALREADY_PAID, nil)) + c.JSON(400, cashu.ErrorCodeToResponse(cashu.QUOTE_PENDING, nil)) return } From cbe999b1ef86049f9c6908617880bbb085c1f051 Mon Sep 17 00:00:00 2001 From: leonardo Date: Wed, 16 Oct 2024 20:59:03 +0200 Subject: [PATCH 04/13] using interface for database and postgres --- api/cashu/types.go | 6 - cmd/nutmix/main.go | 18 +- cmd/nutmix/main_test.go | 13 +- internal/database/auth.go | 47 ----- internal/database/backend.go | 64 ++++++ internal/database/mock_db/admin.go | 50 +++++ internal/database/mock_db/main.go | 192 +++++++++++++++++ internal/database/{ => postgresql}/admin.go | 49 ++++- internal/database/{ => postgresql}/main.go | 216 +++++++++----------- internal/lightning/cln.go | 2 +- internal/mint/mint.go | 14 +- internal/mint/mint_test.go | 10 +- internal/routes/admin/auth.go | 8 +- internal/routes/admin/keysets.go | 27 ++- internal/routes/admin/lightning.go | 3 +- internal/routes/admin/main.go | 31 ++- internal/routes/admin/mint-activity.go | 19 +- internal/routes/admin/pages.go | 9 +- internal/routes/admin/tabs.go | 9 +- internal/routes/bolt11.go | 50 +++-- internal/routes/mint.go | 16 +- internal/routes/routes.go | 7 +- 22 files changed, 559 insertions(+), 301 deletions(-) delete mode 100644 internal/database/auth.go create mode 100644 internal/database/backend.go create mode 100644 internal/database/mock_db/admin.go create mode 100644 internal/database/mock_db/main.go rename internal/database/{ => postgresql}/admin.go (52%) rename internal/database/{ => postgresql}/main.go (54%) diff --git a/api/cashu/types.go b/api/cashu/types.go index 044f0f9..0e0a5ad 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -807,9 +807,3 @@ func (b *BlindSignature) VerifyDLEQ( return hashed_keys_priv.Key.Negate().String() == e.Key.String(), nil } - -type NostrLoginAuth struct { - Nonce string - Activated bool - Expiry int -} diff --git a/cmd/nutmix/main.go b/cmd/nutmix/main.go index 3f10d43..2b98f02 100644 --- a/cmd/nutmix/main.go +++ b/cmd/nutmix/main.go @@ -14,7 +14,7 @@ import ( "github.com/gin-gonic/gin" "github.com/joho/godotenv" "github.com/lescuer97/nutmix/api/cashu" - "github.com/lescuer97/nutmix/internal/database" + "github.com/lescuer97/nutmix/internal/database/postgresql" "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/routes" "github.com/lescuer97/nutmix/internal/routes/admin" @@ -85,15 +85,15 @@ func main() { logger.Info("Running in Release mode") } - pool, err := database.DatabaseSetup(ctx, "migrations") - defer pool.Close() + db, err := postgresql.DatabaseSetup(ctx, "migrations") + defer db.Close() if err != nil { logger.Error(fmt.Sprintf("Error conecting to db %+v", err)) log.Panic() } - seeds, err := database.GetAllSeeds(pool) + seeds, err := db.GetAllSeeds() if err != nil { logger.Error(fmt.Sprintf("Could not GetAllSeeds: %v", err)) @@ -123,7 +123,7 @@ func main() { log.Panic() } - err = database.SaveNewSeeds(pool, generatedSeeds) + err = db.SaveNewSeeds(generatedSeeds) seeds = append(seeds, generatedSeeds...) @@ -146,7 +146,7 @@ func main() { seed.Encrypted = true - err = database.UpdateSeed(pool, seed) + err = db.UpdateSeedEncryption(seed) if err != nil { logger.Error(fmt.Sprintf("Could not update seeds %+v", err)) log.Panic() @@ -161,7 +161,7 @@ func main() { } // remove mint private key from variable - mint, err := mint.SetUpMint(ctx, parsedPrivateKey, seeds, config) + mint, err := mint.SetUpMint(ctx, parsedPrivateKey, seeds, config, db) // clear mint seeds and privatekey seeds = []cashu.Seed{} @@ -181,9 +181,9 @@ func main() { r.Use(cors.Default()) - routes.V1Routes(r, pool, mint, logger) + routes.V1Routes(r, mint, logger) - admin.AdminRoutes(ctx, r, pool, mint, logger) + admin.AdminRoutes(ctx, r, mint, logger) logger.Info("Nutmix started in port 8080") diff --git a/cmd/nutmix/main_test.go b/cmd/nutmix/main_test.go index c00de26..3891819 100644 --- a/cmd/nutmix/main_test.go +++ b/cmd/nutmix/main_test.go @@ -19,6 +19,7 @@ import ( "github.com/gin-gonic/gin" "github.com/lescuer97/nutmix/api/cashu" "github.com/lescuer97/nutmix/internal/database" + pq "github.com/lescuer97/nutmix/internal/database/postgresql" "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/routes" "github.com/lescuer97/nutmix/internal/routes/admin" @@ -658,13 +659,13 @@ func TestMintBolt11FakeWallet(t *testing.T) { func SetupRoutingForTesting(ctx context.Context, adminRoute bool) (*gin.Engine, *mint.Mint) { - pool, err := database.DatabaseSetup(ctx, "../../migrations/") + db, err := pq.DatabaseSetup(ctx, "../../migrations/") if err != nil { log.Fatal("Error conecting to db", err) } - seeds, err := database.GetAllSeeds(pool) + seeds, err := db.GetAllSeeds() if err != nil { log.Fatalf("Could not keysets: %v", err) @@ -686,7 +687,7 @@ func SetupRoutingForTesting(ctx context.Context, adminRoute bool) (*gin.Engine, generatedSeeds, err := cashu.DeriveSeedsFromKey(parsedPrivateKey, 1, cashu.AvailableSeeds) - err = database.SaveNewSeeds(pool, generatedSeeds) + err = db.SaveNewSeeds(generatedSeeds) seeds = append(seeds, generatedSeeds...) @@ -712,7 +713,7 @@ func SetupRoutingForTesting(ctx context.Context, adminRoute bool) (*gin.Engine, log.Fatalf("could not setup config file: %+v ", err) } - mint, err := mint.SetUpMint(ctx, parsedPrivateKey, seeds, config) + mint, err := mint.SetUpMint(ctx, parsedPrivateKey, seeds, config, db) if err != nil { log.Fatalf("SetUpMint: %+v ", err) @@ -722,10 +723,10 @@ func SetupRoutingForTesting(ctx context.Context, adminRoute bool) (*gin.Engine, logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - routes.V1Routes(r, pool, mint, logger) + routes.V1Routes(r, mint, logger) if adminRoute { - admin.AdminRoutes(ctx, r, pool, mint, logger) + admin.AdminRoutes(ctx, r, mint, logger) } return r, mint diff --git a/internal/database/auth.go b/internal/database/auth.go deleted file mode 100644 index faa3858..0000000 --- a/internal/database/auth.go +++ /dev/null @@ -1,47 +0,0 @@ -package database - -import ( - "context" - "fmt" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/lescuer97/nutmix/api/cashu" -) - -func SaveNostrLoginAuth(pool *pgxpool.Pool, auth cashu.NostrLoginAuth) error { - _, err := pool.Exec(context.Background(), "INSERT INTO nostr_login (nonce, expiry , activated) VALUES ($1, $2, $3)", auth.Nonce, auth.Expiry, auth.Activated) - - if err != nil { - return databaseError(fmt.Errorf("Inserting to nostr_login: %w", err)) - - } - return nil -} - -func UpdateNostrLoginActivation(pool *pgxpool.Pool, auth cashu.NostrLoginAuth) error { - // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE nostr_login SET activated = $1 WHERE nonce = $2", auth.Activated, auth.Nonce) - if err != nil { - return databaseError(fmt.Errorf("Update to seeds: %w", err)) - - } - return nil -} - -func GetNostrLogin(pool *pgxpool.Pool, nonce string) (cashu.NostrLoginAuth, error) { - rows, err := pool.Query(context.Background(), "SELECT nonce, activated, expiry FROM nostr_login WHERE nonce = $1", nonce) - defer rows.Close() - if err != nil { - return cashu.NostrLoginAuth{}, fmt.Errorf("Error checking for Active seeds: %w", err) - } - - nostrLogin, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.NostrLoginAuth]) - - if err != nil { - return nostrLogin, fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.NostrLoginAuth]): %w", err) - } - - return nostrLogin, nil - -} diff --git a/internal/database/backend.go b/internal/database/backend.go new file mode 100644 index 0000000..a5bc95b --- /dev/null +++ b/internal/database/backend.go @@ -0,0 +1,64 @@ +package database + +import ( + "errors" + + "github.com/lescuer97/nutmix/api/cashu" +) + +type MintMeltBalance struct { + Mint []cashu.MintRequestDB + Melt []cashu.MeltRequestDB +} +type NostrLoginAuth struct { + Nonce string + Activated bool + Expiry int +} + +var DBError = errors.New("ERROR DATABASE") + +var DATABASE_URL_ENV = "DATABASE_URL" + +const ( + DOCKERDATABASE = "DOCKERDATABASE" + CUSTOMDATABASE = "CUSTOMDATABASE" +) + +type MintDB interface { + + /// Calls for the Functioning of the mint + + GetAllSeeds() ([]cashu.Seed, error) + GetSeedsByUnit(unit cashu.Unit) ([]cashu.Seed, error) + SaveNewSeed(seed cashu.Seed) error + SaveNewSeeds(seeds []cashu.Seed) error + // This should be used to only update the Active Status of seed on the db + UpdateSeedsActiveStatus(seeds []cashu.Seed) error + // This is for updating if a mint is encrypted or not + UpdateSeedEncryption(seeds cashu.Seed) error + + SaveMintRequest(request cashu.MintRequestDB) error + ChangeMintRequestState(quote string, paid bool, state cashu.ACTION_STATE, minted bool) error + GetMintRequestById(quote string) (cashu.MintRequestDB, error) + + GetMeltRequestById(quote string) (cashu.MeltRequestDB, error) + SaveMeltRequest(request cashu.MeltRequestDB) error + ChangeMeltRequestState(quote string, paid bool, state cashu.ACTION_STATE, melted bool) error + AddPreimageMeltRequest(quote string, preimage string) error + + SaveProof(proofs []cashu.Proof) error + GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) + GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, error) + + GetRestoreSigsFromBlindedMessages(B_ []string) ([]cashu.RecoverSigDB, error) + SaveRestoreSigs(recover_sigs []cashu.RecoverSigDB) error + + /// Calls for the admin dashboard + + GetMintMeltBalanceByTime(time int64) (MintMeltBalance, error) + + SaveNostrAuth(auth NostrLoginAuth) error + UpdateNostrAuthActivation(nonce string, activated bool) error + GetNostrAuth(nonce string) (NostrLoginAuth, error) +} diff --git a/internal/database/mock_db/admin.go b/internal/database/mock_db/admin.go new file mode 100644 index 0000000..ca0bee1 --- /dev/null +++ b/internal/database/mock_db/admin.go @@ -0,0 +1,50 @@ +package mockdb + +import ( + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/database" +) + +func (m MockDB) SaveNostrAuth(auth database.NostrLoginAuth) error { + return nil + +} + +func (m MockDB) UpdateNostrAuthActivation(nonce string, activated bool) error { + return nil +} + +func (m MockDB) GetNostrAuth(nonce string) (database.NostrLoginAuth, error) { + var seeds []database.NostrLoginAuth + for i := 0; i < len(m.NostrAuth); i++ { + + if m.Seeds[i].Unit == nonce { + seeds = append(seeds, m.NostrAuth[i]) + + } + + } + return seeds[0], nil + +} + +func (m MockDB) GetMintMeltBalanceByTime(time int64) (database.MintMeltBalance, error) { + var mintmeltbalance database.MintMeltBalance + + for i := 0; i < len(m.MeltRequest); i++ { + if m.MeltRequest[i].State == cashu.ISSUED || m.MeltRequest[i].State == cashu.PAID { + mintmeltbalance.Melt = append(mintmeltbalance.Melt, m.MeltRequest[i]) + + } + + } + + for j := 0; j < len(m.MeltRequest); j++ { + if m.MintRequest[j].State == cashu.ISSUED || m.MintRequest[j].State == cashu.PAID { + mintmeltbalance.Mint = append(mintmeltbalance.Mint, m.MintRequest[j]) + + } + + } + return mintmeltbalance, nil +} diff --git a/internal/database/mock_db/main.go b/internal/database/mock_db/main.go new file mode 100644 index 0000000..2cc2566 --- /dev/null +++ b/internal/database/mock_db/main.go @@ -0,0 +1,192 @@ +package mockdb + +import ( + "errors" + "fmt" + + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/database" +) + +var DBError = errors.New("ERROR DATABASE") + +var DATABASE_URL_ENV = "DATABASE_URL" + +type MockDB struct { + // pool *pgxpool.Pool + Proofs []cashu.Proof + MeltRequest []cashu.MeltRequestDB + MintRequest []cashu.MintRequestDB + RecoverSigDB []cashu.RecoverSigDB + NostrAuth []database.NostrLoginAuth + Seeds []cashu.Seed + ErrorToReturn error +} + +func databaseError(err error) error { + return fmt.Errorf("%w %w", DBError, err) +} + +func (m MockDB) GetAllSeeds() ([]cashu.Seed, error) { + return m.Seeds, nil +} + +func (m MockDB) GetSeedsByUnit(unit cashu.Unit) ([]cashu.Seed, error) { + var seeds []cashu.Seed + for i := 0; i < len(m.Seeds); i++ { + + if m.Seeds[i].Unit == unit.String() { + seeds = append(seeds, m.Seeds[i]) + + } + + } + return seeds, nil +} + +func (m MockDB) SaveNewSeed(seed cashu.Seed) error { + return nil +} + +func (m MockDB) SaveNewSeeds(seeds []cashu.Seed) error { + return nil +} + +func (m MockDB) UpdateSeedsActiveStatus(seeds []cashu.Seed) error { + return nil +} + +func (m MockDB) UpdateSeedEncryption(seed cashu.Seed) error { + return nil +} + +func (m MockDB) SaveMintRequest(request cashu.MintRequestDB) error { + return nil +} + +func (m MockDB) ChangeMintRequestState(quote string, paid bool, state cashu.ACTION_STATE, minted bool) error { + return nil +} + +func (m MockDB) GetMintRequestById(id string) (cashu.MintRequestDB, error) { + var mintRequests []cashu.MintRequestDB + for i := 0; i < len(m.MintRequest); i++ { + + if m.MintRequest[i].Quote == id { + mintRequests = append(mintRequests, m.MintRequest[i]) + + } + + } + + return mintRequests[0], nil +} + +func (m MockDB) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) { + var meltRequests []cashu.MeltRequestDB + for i := 0; i < len(m.MeltRequest); i++ { + + if m.MintRequest[i].Quote == id { + meltRequests = append(meltRequests, m.MeltRequest[i]) + + } + + } + + return meltRequests[0], nil +} + +func (m MockDB) ModifyQuoteMintMintedStatus(minted bool, state cashu.ACTION_STATE, quote string) error { + return nil +} +func (m MockDB) SaveMeltRequest(request cashu.MeltRequestDB) error { + + return nil + +} + +func (m MockDB) AddPreimageMeltRequest(preimage string, quote string) error { + return nil + +} +func (m MockDB) ChangeMeltRequestState(quote string, paid bool, state cashu.ACTION_STATE, melted bool) error { + return nil + +} +func (m MockDB) ModifyQuoteMeltMeltedStatus(melted bool, quote string) error { + return nil + +} + +func (m MockDB) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) { + var proofs []cashu.Proof + for i := 0; i < len(SecretList); i++ { + + secret := SecretList[i] + + for j := 0; j < len(m.Proofs); j++ { + + if secret == m.Proofs[j].Secret { + proofs = append(proofs, m.Proofs[j]) + + } + + } + + } + + return proofs, nil +} + +func (m MockDB) SaveProof(proofs []cashu.Proof) error { + return nil + +} + +func (m MockDB) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, error) { + var proofs []cashu.Proof + for i := 0; i < len(Ys); i++ { + + secretCurve := Ys[i] + + for j := 0; j < len(m.Proofs); j++ { + + if secretCurve == m.Proofs[j].Y { + proofs = append(proofs, m.Proofs[j]) + + } + + } + + } + + return proofs, nil +} + +func (m MockDB) GetRestoreSigsFromBlindedMessages(B_ []string) ([]cashu.RecoverSigDB, error) { + var restore []cashu.RecoverSigDB + for i := 0; i < len(B_); i++ { + + blindMessage := B_[i] + + for j := 0; j < len(m.RecoverSigDB); j++ { + + if blindMessage == m.RecoverSigDB[j].B_ { + restore = append(restore, m.RecoverSigDB[j]) + + } + + } + + } + + return restore, nil +} + +func (m MockDB) SaveRestoreSigs(recover_sigs []cashu.RecoverSigDB) error { + return nil + +} + +func (m MockDB) Close() { +} diff --git a/internal/database/admin.go b/internal/database/postgresql/admin.go similarity index 52% rename from internal/database/admin.go rename to internal/database/postgresql/admin.go index d128d26..703368c 100644 --- a/internal/database/admin.go +++ b/internal/database/postgresql/admin.go @@ -1,28 +1,59 @@ -package database +package postgresql import ( "context" "fmt" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" - // "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/database" ) -type MintMeltBalance struct { - Mint []cashu.MintRequestDB - Melt []cashu.MeltRequestDB +func (pql Postgresql) SaveNostrAuth(auth database.NostrLoginAuth) error { + _, err := pql.pool.Exec(context.Background(), "INSERT INTO nostr_login (nonce, expiry , activated) VALUES ($1, $2, $3)", auth.Nonce, auth.Expiry, auth.Activated) + + if err != nil { + return databaseError(fmt.Errorf("Inserting to nostr_login: %w", err)) + + } + return nil +} + +func (pql Postgresql) UpdateNostrAuthActivation(nonce string, activated bool) error { + // change the paid status of the quote + _, err := pql.pool.Exec(context.Background(), "UPDATE nostr_login SET activated = $1 WHERE nonce = $2", activated, nonce) + if err != nil { + return databaseError(fmt.Errorf("Update to seeds: %w", err)) + + } + return nil +} + +func (pql Postgresql) GetNostrAuth(nonce string) (database.NostrLoginAuth, error) { + rows, err := pql.pool.Query(context.Background(), "SELECT nonce, activated, expiry FROM nostr_login WHERE nonce = $1", nonce) + defer rows.Close() + if err != nil { + return database.NostrLoginAuth{}, fmt.Errorf("Error checking for Active seeds: %w", err) + } + + nostrLogin, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[database.NostrLoginAuth]) + + if err != nil { + return nostrLogin, fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.NostrLoginAuth]): %w", err) + } + + return nostrLogin, nil + } -func GetMintMeltBalanceByTime(pool *pgxpool.Pool, time int64) (MintMeltBalance, error) { - var mintMeltBalance MintMeltBalance +func (pql Postgresql) GetMintMeltBalanceByTime(time int64) (database.MintMeltBalance, error) { + var mintMeltBalance database.MintMeltBalance // change the paid status of the quote batch := pgx.Batch{} batch.Queue("SELECT quote, request, request_paid, expiry, unit, minted, state, seen_at FROM mint_request WHERE seen_at >= $1 AND (state = 'ISSUED' OR state = 'PAID') ", time) batch.Queue("SELECT quote, request, amount, request_paid, expiry, unit, melted, fee_reserve, state, payment_preimage, seen_at, mpp FROM melt_request WHERE seen_at >= $1 AND (state = 'ISSUED' OR state = 'PAID')", time) - results := pool.SendBatch(context.Background(), &batch) + results := pql.pool.SendBatch(context.Background(), &batch) defer results.Close() diff --git a/internal/database/main.go b/internal/database/postgresql/main.go similarity index 54% rename from internal/database/main.go rename to internal/database/postgresql/main.go index 6547592..5f1f8a4 100644 --- a/internal/database/main.go +++ b/internal/database/postgresql/main.go @@ -1,36 +1,38 @@ -package database +package postgresql import ( "context" "errors" "fmt" + "log" + "os" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/lescuer97/nutmix/api/cashu" "github.com/pressly/goose/v3" - "log" - "os" ) var DBError = errors.New("ERROR DATABASE") var DATABASE_URL_ENV = "DATABASE_URL" -const ( - DOCKERDATABASE = "DOCKERDATABASE" - CUSTOMDATABASE = "CUSTOMDATABASE" -) +type Postgresql struct { + pool *pgxpool.Pool +} func databaseError(err error) error { return fmt.Errorf("%w %w", DBError, err) } -func DatabaseSetup(ctx context.Context, migrationDir string) (*pgxpool.Pool, error) { +func DatabaseSetup(ctx context.Context, migrationDir string) (Postgresql, error) { + + var postgresql Postgresql dbUrl := os.Getenv(DATABASE_URL_ENV) if dbUrl == "" { - return &pgxpool.Pool{}, fmt.Errorf("%v enviroment variable empty", DATABASE_URL_ENV) + return postgresql, fmt.Errorf("%v enviroment variable empty", DATABASE_URL_ENV) } @@ -51,16 +53,17 @@ func DatabaseSetup(ctx context.Context, migrationDir string) (*pgxpool.Pool, err } if err != nil { - return nil, databaseError(fmt.Errorf("Error connecting to database: %w", err)) + return postgresql, databaseError(fmt.Errorf("Error connecting to database: %w", err)) } + postgresql.pool = pool - return pool, nil + return postgresql, nil } -func GetAllSeeds(pool *pgxpool.Pool) ([]cashu.Seed, error) { +func (pql Postgresql) GetAllSeeds() ([]cashu.Seed, error) { var seeds []cashu.Seed - rows, err := pool.Query(context.Background(), `SELECT seed, created_at, active, version, unit, id, encrypted, "input_fee_ppk" FROM seeds ORDER BY version DESC`) + rows, err := pql.pool.Query(context.Background(), `SELECT seed, created_at, active, version, unit, id, encrypted, "input_fee_ppk" FROM seeds ORDER BY version DESC`) defer rows.Close() if err != nil { @@ -80,23 +83,8 @@ func GetAllSeeds(pool *pgxpool.Pool) ([]cashu.Seed, error) { return seeds_collect, nil } -func GetActiveSeed(pool *pgxpool.Pool) (cashu.Seed, error) { - rows, err := pool.Query(context.Background(), "SELECT seed, created_at, active, version, unit, id, encrypted, input_fee_ppk FROM seeds WHERE active") - defer rows.Close() - if err != nil { - return cashu.Seed{}, fmt.Errorf("Error checking for Active seeds: %w", err) - } - - seed, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.Seed]) - - if err != nil { - return seed, databaseError(fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.Seed]): %w", err)) - } - - return seed, nil -} -func GetSeedsByUnit(pool *pgxpool.Pool, unit cashu.Unit) ([]cashu.Seed, error) { - rows, err := pool.Query(context.Background(), "SELECT seed, created_at, active, version, unit, id, encrypted, input_fee_ppk FROM seeds WHERE unit = $1", unit.String()) +func (pql Postgresql) GetSeedsByUnit(unit cashu.Unit) ([]cashu.Seed, error) { + rows, err := pql.pool.Query(context.Background(), "SELECT seed, created_at, active, version, unit, id, encrypted, input_fee_ppk FROM seeds WHERE unit = $1", unit.String()) defer rows.Close() if err != nil { return []cashu.Seed{}, fmt.Errorf("Error checking for Active seeds: %w", err) @@ -111,13 +99,13 @@ func GetSeedsByUnit(pool *pgxpool.Pool, unit cashu.Unit) ([]cashu.Seed, error) { return seeds, nil } -func SaveNewSeed(pool *pgxpool.Pool, seed *cashu.Seed) error { +func (pql Postgresql) SaveNewSeed(seed cashu.Seed) error { tries := 0 for { tries += 1 - _, err := pool.Exec(context.Background(), "INSERT INTO seeds (seed, active, created_at, unit, id, version, encrypted, input_fee_ppk) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", seed.Seed, seed.Active, seed.CreatedAt, seed.Unit, seed.Id, seed.Version, seed.Encrypted, seed.InputFeePpk) + _, err := pql.pool.Exec(context.Background(), "INSERT INTO seeds (seed, active, created_at, unit, id, version, encrypted, input_fee_ppk) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", seed.Seed, seed.Active, seed.CreatedAt, seed.Unit, seed.Id, seed.Version, seed.Encrypted, seed.InputFeePpk) switch { case err != nil && tries < 3: @@ -131,7 +119,7 @@ func SaveNewSeed(pool *pgxpool.Pool, seed *cashu.Seed) error { } } -func SaveNewSeeds(pool *pgxpool.Pool, seeds []cashu.Seed) error { +func (pql Postgresql) SaveNewSeeds(seeds []cashu.Seed) error { tries := 0 entries := [][]any{} @@ -144,7 +132,7 @@ func SaveNewSeeds(pool *pgxpool.Pool, seeds []cashu.Seed) error { for { tries += 1 - _, err := pool.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) + _, err := pql.pool.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) switch { case err != nil && tries < 3: @@ -159,7 +147,7 @@ func SaveNewSeeds(pool *pgxpool.Pool, seeds []cashu.Seed) error { } -func UpdateActiveStatusSeeds(pool *pgxpool.Pool, seeds []cashu.Seed) error { +func (pql Postgresql) UpdateSeedsActiveStatus(seeds []cashu.Seed) error { // change the paid status of the quote batch := pgx.Batch{} for _, seed := range seeds { @@ -167,7 +155,7 @@ func UpdateActiveStatusSeeds(pool *pgxpool.Pool, seeds []cashu.Seed) error { batch.Queue("UPDATE seeds SET active = $1 WHERE id = $2", seed.Active, seed.Id) } - results := pool.SendBatch(context.Background(), &batch) + results := pql.pool.SendBatch(context.Background(), &batch) defer results.Close() rows, err := results.Query() @@ -183,9 +171,9 @@ func UpdateActiveStatusSeeds(pool *pgxpool.Pool, seeds []cashu.Seed) error { } -func UpdateSeed(pool *pgxpool.Pool, seed cashu.Seed) error { +func (pql Postgresql) UpdateSeedEncryption(seed cashu.Seed) error { // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE seeds SET encrypted = $1, seed = $2 WHERE id = $3", seed.Encrypted, seed.Seed, seed.Id) + _, err := pql.pool.Exec(context.Background(), "UPDATE seeds SET encrypted = $1, seed = $2 WHERE id = $3", seed.Encrypted, seed.Seed, seed.Id) if err != nil { return databaseError(fmt.Errorf("Update to seeds: %w", err)) @@ -193,19 +181,20 @@ func UpdateSeed(pool *pgxpool.Pool, seed cashu.Seed) error { return nil } -func SaveMintRequestDB(pool *pgxpool.Pool, request cashu.MintRequestDB) error { +func (pql Postgresql) SaveMintRequest(request cashu.MintRequestDB) error { ctx := context.Background() - _, err := pool.Exec(ctx, "INSERT INTO mint_request (quote, request, request_paid, expiry, unit, minted, state, seen_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", request.Quote, request.Request, request.RequestPaid, request.Expiry, request.Unit, request.Minted, request.State, request.SeenAt) + _, err := pql.pool.Exec(ctx, "INSERT INTO mint_request (quote, request, request_paid, expiry, unit, minted, state, seen_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", request.Quote, request.Request, request.RequestPaid, request.Expiry, request.Unit, request.Minted, request.State, request.SeenAt) if err != nil { return databaseError(fmt.Errorf("Inserting to mint_request: %w", err)) } return nil } -func ModifyQuoteMintPayStatus(pool *pgxpool.Pool, requestPaid bool, state cashu.ACTION_STATE, quote string) error { + +func (pql Postgresql) ChangeMintRequestState(quote string, paid bool, state cashu.ACTION_STATE, minted bool) error { // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE mint_request SET request_paid = $1, state = $3 WHERE quote = $2", requestPaid, quote, state) + _, err := pql.pool.Exec(context.Background(), "UPDATE mint_request SET request_paid = $1, state = $3, minted = $4 WHERE quote = $2", paid, quote, state, minted) if err != nil { return databaseError(fmt.Errorf("Inserting to mint_request: %w", err)) @@ -213,7 +202,52 @@ func ModifyQuoteMintPayStatus(pool *pgxpool.Pool, requestPaid bool, state cashu. return nil } -func ModifyQuoteMintMintedStatus(pool *pgxpool.Pool, minted bool, state cashu.ACTION_STATE, quote string) error { +func (pql Postgresql) GetMintRequestById(id string) (cashu.MintRequestDB, error) { + + rows, err := pql.pool.Query(context.Background(), "SELECT quote, request, request_paid, expiry, unit, minted, state, seen_at FROM mint_request WHERE quote = $1", id) + defer rows.Close() + if err != nil { + if err == pgx.ErrNoRows { + return cashu.MintRequestDB{}, err + } + } + + quote, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.MintRequestDB]) + rows.Close() + if err != nil { + if err == pgx.ErrNoRows { + return cashu.MintRequestDB{}, err + } + return quote, databaseError(fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.PostMintQuoteBolt11Response]): %w", err)) + } + + return quote, nil +} + +func (pql Postgresql) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) { + + rows, err := pql.pool.Query(context.Background(), "SELECT quote, request, amount, request_paid, expiry, unit, melted, fee_reserve, state, payment_preimage, seen_at, mpp FROM melt_request WHERE quote = $1", id) + defer rows.Close() + if err != nil { + if err == pgx.ErrNoRows { + return cashu.MeltRequestDB{}, err + } + } + + quote, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.MeltRequestDB]) + + if err != nil { + if err == pgx.ErrNoRows { + return cashu.MeltRequestDB{}, err + } + + return quote, databaseError(fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.MeltRequestDB]): %w", err)) + } + + return quote, nil +} + +func (pql Postgresql) ModifyQuoteMintMintedStatus(minted bool, state cashu.ACTION_STATE, quote string) error { args := pgx.NamedArgs{ "state": state, @@ -224,7 +258,7 @@ func ModifyQuoteMintMintedStatus(pool *pgxpool.Pool, minted bool, state cashu.AC query := `UPDATE mint_request SET minted = @minted, state = @state WHERE quote = @quote` // change the paid status of the quote - _, err := pool.Exec(context.Background(), query, args) + _, err := pql.pool.Exec(context.Background(), query, args) if err != nil { return databaseError(fmt.Errorf("Inserting to mint_request: %w", err)) @@ -232,46 +266,36 @@ func ModifyQuoteMintMintedStatus(pool *pgxpool.Pool, minted bool, state cashu.AC } return nil } -func SaveQuoteMeltRequest(pool *pgxpool.Pool, request cashu.MeltRequestDB) error { +func (pql Postgresql) SaveMeltRequest(request cashu.MeltRequestDB) error { - _, err := pool.Exec(context.Background(), "INSERT INTO melt_request (quote, request, fee_reserve, expiry, unit, amount, request_paid, melted, state, payment_preimage, seen_at, mpp) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", request.Quote, request.Request, request.FeeReserve, request.Expiry, request.Unit, request.Amount, request.RequestPaid, request.Melted, request.State, request.PaymentPreimage, request.SeenAt, request.Mpp) + _, err := pql.pool.Exec(context.Background(), "INSERT INTO melt_request (quote, request, fee_reserve, expiry, unit, amount, request_paid, melted, state, payment_preimage, seen_at, mpp) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", request.Quote, request.Request, request.FeeReserve, request.Expiry, request.Unit, request.Amount, request.RequestPaid, request.Melted, request.State, request.PaymentPreimage, request.SeenAt, request.Mpp) if err != nil { return databaseError(fmt.Errorf("Inserting to mint_request: %w", err)) } return nil } -func AddPaymentPreimageToMeltRequest(pool *pgxpool.Pool, preimage string, quote string) error { +func (pql Postgresql) AddPreimageMeltRequest(preimage string, quote string) error { // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE melt_request SET payment_preimage = $1 WHERE quote = $2", preimage, quote) + _, err := pql.pool.Exec(context.Background(), "UPDATE melt_request SET payment_preimage = $1 WHERE quote = $2", preimage, quote) if err != nil { return databaseError(fmt.Errorf("updating melt_request with preimage: %w", err)) } return nil } -func ModifyQuoteMeltPayStatus(pool *pgxpool.Pool, paid bool, state cashu.ACTION_STATE, request string) error { - // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE melt_request SET request_paid = $1, state = $3 WHERE quote = $2", paid, request, state) - if err != nil { - return databaseError(fmt.Errorf("updating mint_request: %w", err)) - - } - return nil -} -func ModifyQuoteMeltPayStatusAndMelted(pool *pgxpool.Pool, paid bool, melted bool, state cashu.ACTION_STATE, request string) error { +func (pql Postgresql) ChangeMeltRequestState(quote string, paid bool, state cashu.ACTION_STATE, melted bool) error { // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE melt_request SET request_paid = $1, melted = $3, state = $4 WHERE quote = $2", paid, request, melted, state) + _, err := pql.pool.Exec(context.Background(), "UPDATE melt_request SET request_paid = $1, state = $3, melted = $4 WHERE quote = $2", paid, quote, state, melted) if err != nil { return databaseError(fmt.Errorf("updating mint_request: %w", err)) } return nil } - -func ModifyQuoteMeltMeltedStatus(pool *pgxpool.Pool, melted bool, quote string) error { +func (pql Postgresql) ModifyQuoteMeltMeltedStatus(melted bool, quote string) error { // change the paid status of the quote - _, err := pool.Exec(context.Background(), "UPDATE melt_request SET melted = $1 WHERE quote = $2", melted, quote) + _, err := pql.pool.Exec(context.Background(), "UPDATE melt_request SET melted = $1 WHERE quote = $2", melted, quote) if err != nil { return databaseError(fmt.Errorf("updating mint_request: %w", err)) @@ -279,56 +303,12 @@ func ModifyQuoteMeltMeltedStatus(pool *pgxpool.Pool, melted bool, quote string) return nil } -func GetMintQuoteById(pool *pgxpool.Pool, id string) (cashu.MintRequestDB, error) { - - rows, err := pool.Query(context.Background(), "SELECT quote, request, request_paid, expiry, unit, minted, state, seen_at FROM mint_request WHERE quote = $1", id) - defer rows.Close() - if err != nil { - if err == pgx.ErrNoRows { - return cashu.MintRequestDB{}, err - } - } - - quote, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.MintRequestDB]) - rows.Close() - if err != nil { - if err == pgx.ErrNoRows { - return cashu.MintRequestDB{}, err - } - return quote, databaseError(fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.PostMintQuoteBolt11Response]): %w", err)) - } - - return quote, nil -} -func GetMeltQuoteById(pool *pgxpool.Pool, id string) (cashu.MeltRequestDB, error) { - - rows, err := pool.Query(context.Background(), "SELECT quote, request, amount, request_paid, expiry, unit, melted, fee_reserve, state, payment_preimage, seen_at, mpp FROM melt_request WHERE quote = $1", id) - defer rows.Close() - if err != nil { - if err == pgx.ErrNoRows { - return cashu.MeltRequestDB{}, err - } - } - - quote, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.MeltRequestDB]) - - if err != nil { - if err == pgx.ErrNoRows { - return cashu.MeltRequestDB{}, err - } - - return quote, databaseError(fmt.Errorf("pgx.CollectOneRow(rows, pgx.RowToStructByName[cashu.MeltRequestDB]): %w", err)) - } - - return quote, nil -} - -func CheckListOfProofs(pool *pgxpool.Pool, SecretList []string) ([]cashu.Proof, error) { +func (pql Postgresql) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) { var proofList []cashu.Proof ctx := context.Background() - rows, err := pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE secret = ANY($1)", SecretList) + rows, err := pql.pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE secret = ANY($1)", SecretList) defer rows.Close() @@ -353,7 +333,7 @@ func CheckListOfProofs(pool *pgxpool.Pool, SecretList []string) ([]cashu.Proof, return proofList, nil } -func SaveProofs(pool *pgxpool.Pool, proofs []cashu.Proof) error { +func (pql Postgresql) SaveProof(proofs []cashu.Proof) error { entries := [][]any{} columns := []string{"c", "secret", "amount", "id", "y", "witness", "seen_at"} tableName := "proofs" @@ -366,7 +346,7 @@ func SaveProofs(pool *pgxpool.Pool, proofs []cashu.Proof) error { for { tries += 1 - _, err := pool.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) + _, err := pql.pool.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) switch { case err != nil && tries < 3: @@ -381,11 +361,11 @@ func SaveProofs(pool *pgxpool.Pool, proofs []cashu.Proof) error { } -func CheckListOfProofsBySecretCurve(pool *pgxpool.Pool, Ys []string) ([]cashu.Proof, error) { +func (pql Postgresql) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, error) { var proofList []cashu.Proof - rows, err := pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE y = ANY($1)`, Ys) + rows, err := pql.pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE y = ANY($1)`, Ys) defer rows.Close() if err != nil { @@ -409,11 +389,11 @@ func CheckListOfProofsBySecretCurve(pool *pgxpool.Pool, Ys []string) ([]cashu.Pr return proofList, nil } -func GetRestoreSigsFromBlindedMessages(pool *pgxpool.Pool, B_ []string) ([]cashu.RecoverSigDB, error) { +func (pql Postgresql) GetRestoreSigsFromBlindedMessages(B_ []string) ([]cashu.RecoverSigDB, error) { var signaturesList []cashu.RecoverSigDB - rows, err := pool.Query(context.Background(), `SELECT id, amount, "C_", "B_", created_at, witness FROM recovery_signature WHERE "B_" = ANY($1)`, B_) + rows, err := pql.pool.Query(context.Background(), `SELECT id, amount, "C_", "B_", created_at, witness FROM recovery_signature WHERE "B_" = ANY($1)`, B_) defer rows.Close() if err != nil { @@ -437,7 +417,7 @@ func GetRestoreSigsFromBlindedMessages(pool *pgxpool.Pool, B_ []string) ([]cashu return signaturesList, nil } -func SetRestoreSigs(pool *pgxpool.Pool, recover_sigs []cashu.RecoverSigDB) error { +func (pql Postgresql) SaveRestoreSigs(recover_sigs []cashu.RecoverSigDB) error { entries := [][]any{} columns := []string{"id", "amount", "B_", "C_", "created_at", "witness"} tableName := "recovery_signature" @@ -449,7 +429,7 @@ func SetRestoreSigs(pool *pgxpool.Pool, recover_sigs []cashu.RecoverSigDB) error for { tries += 1 - _, err := pool.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) + _, err := pql.pool.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) switch { case err != nil && tries < 3: @@ -462,3 +442,7 @@ func SetRestoreSigs(pool *pgxpool.Pool, recover_sigs []cashu.RecoverSigDB) error } } + +func (pql Postgresql) Close() { + pql.pool.Close() +} diff --git a/internal/lightning/cln.go b/internal/lightning/cln.go index 8ee3cb2..086d749 100644 --- a/internal/lightning/cln.go +++ b/internal/lightning/cln.go @@ -288,7 +288,7 @@ func (l CLNGRPCWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mp return fee, nil } -func (l CLNGRPCWallet) ReqSETTLEDuestInvoice(amount int64) (InvoiceResponse, error) { +func (l CLNGRPCWallet) RequestInvoice(amount int64) (InvoiceResponse, error) { var response InvoiceResponse ctx := metadata.AppendToOutgoingContext(context.Background(), "macaroon", l.macaroon) diff --git a/internal/mint/mint.go b/internal/mint/mint.go index a68b4bc..fbda604 100644 --- a/internal/mint/mint.go +++ b/internal/mint/mint.go @@ -12,8 +12,8 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/database" "github.com/lescuer97/nutmix/internal/lightning" "github.com/lescuer97/nutmix/pkg/crypto" ) @@ -27,6 +27,7 @@ type Mint struct { ActiveQuotes *ActiveQuote Config Config MintPubkey string + MintDB database.MintDB } var ( @@ -346,7 +347,7 @@ func CheckChainParams(network string) (chaincfg.Params, error) { } -func SetUpMint(ctx context.Context, mint_privkey *secp256k1.PrivateKey, seeds []cashu.Seed, config Config) (*Mint, error) { +func SetUpMint(ctx context.Context, mint_privkey *secp256k1.PrivateKey, seeds []cashu.Seed, config Config, db database.MintDB) (*Mint, error) { activeProofs := ActiveProofs{ Proofs: make(map[cashu.Proof]bool), } @@ -359,6 +360,7 @@ func SetUpMint(ctx context.Context, mint_privkey *secp256k1.PrivateKey, seeds [] Config: config, ActiveProofs: &activeProofs, ActiveQuotes: &activeQuotes, + MintDB: db, } chainparam, err := CheckChainParams(config.NETWORK) @@ -425,9 +427,9 @@ func SetUpMint(ctx context.Context, mint_privkey *secp256k1.PrivateKey, seeds [] return &mint, nil } -type AddToDBFunc func(*pgxpool.Pool, bool, cashu.ACTION_STATE, string) error +type AddToDBFunc func(string, bool, cashu.ACTION_STATE, bool) error -func (m *Mint) VerifyLightingPaymentHappened(pool *pgxpool.Pool, paid bool, quote string, dbCall AddToDBFunc) (cashu.ACTION_STATE, string, error) { +func (m *Mint) VerifyLightingPaymentHappened(paid bool, quote string, dbCall AddToDBFunc) (cashu.ACTION_STATE, string, error) { state, preimage, err := m.LightningBackend.CheckPayed(quote) if err != nil { return cashu.UNPAID, "", fmt.Errorf("mint.LightningComs.CheckIfInvoicePayed: %w", err) @@ -435,14 +437,14 @@ func (m *Mint) VerifyLightingPaymentHappened(pool *pgxpool.Pool, paid bool, quot switch { case state == lightning.SETTLED: - err := dbCall(pool, true, cashu.PAID, quote) + err := dbCall(quote, true, cashu.PAID, false) if err != nil { return cashu.PAID, preimage, fmt.Errorf("dbCall: %w", err) } return cashu.PAID, preimage, nil case state == lightning.PENDING: - err := dbCall(pool, true, cashu.UNPAID, quote) + err := dbCall(quote, false, cashu.UNPAID, false) if err != nil { return cashu.UNPAID, preimage, fmt.Errorf("dbCall: %w", err) } diff --git a/internal/mint/mint_test.go b/internal/mint/mint_test.go index 83734a7..418e4fc 100644 --- a/internal/mint/mint_test.go +++ b/internal/mint/mint_test.go @@ -3,11 +3,13 @@ package mint import ( "context" "encoding/hex" + "os" + "testing" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lescuer97/nutmix/api/cashu" + mockdb "github.com/lescuer97/nutmix/internal/database/mock_db" "github.com/tyler-smith/go-bip32" - "os" - "testing" ) const MintPrivateKey string = "0000000000000000000000000000000000000000000000000000000000000001" @@ -51,7 +53,9 @@ func TestSetUpMint(t *testing.T) { t.Errorf("could not setup config file: %+v", err) } - mint, err := SetUpMint(ctx, parsedPrivateKey, seeds, config) + db := mockdb.MockDB{} + + mint, err := SetUpMint(ctx, parsedPrivateKey, seeds, config, db) if err != nil { t.Errorf("could not setup mint: %+v", err) diff --git a/internal/routes/admin/auth.go b/internal/routes/admin/auth.go index 51e9716..11457ff 100644 --- a/internal/routes/admin/auth.go +++ b/internal/routes/admin/auth.go @@ -11,9 +11,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" - "github.com/lescuer97/nutmix/internal/database" "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" "github.com/nbd-wtf/go-nostr" @@ -81,7 +79,7 @@ func AuthMiddleware(logger *slog.Logger, secret []byte) gin.HandlerFunc { } } -func Login(pool *pgxpool.Pool, mint *mint.Mint, logger *slog.Logger) gin.HandlerFunc { +func Login(mint *mint.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { // parse data for login logger.Debug("Attempting log in") @@ -97,7 +95,7 @@ func Login(pool *pgxpool.Pool, mint *mint.Mint, logger *slog.Logger) gin.Handler return } - nostrLogin, err := database.GetNostrLogin(pool, nostrEvent.Content) + nostrLogin, err := mint.MintDB.GetNostrAuth(nostrEvent.Content) if err != nil { logger.Error( @@ -191,7 +189,7 @@ func Login(pool *pgxpool.Pool, mint *mint.Mint, logger *slog.Logger) gin.Handler nostrLogin.Activated = verified - err = database.UpdateNostrLoginActivation(pool, nostrLogin) + err = mint.MintDB.UpdateNostrAuthActivation(nostrLogin.Nonce, nostrLogin.Activated) if err != nil { logger.Error("database.UpdateNostrLoginActivation(pool, nostrLogin)", slog.String(utils.LogExtraInfo, err.Error())) diff --git a/internal/routes/admin/keysets.go b/internal/routes/admin/keysets.go index 041b865..6eb1bac 100644 --- a/internal/routes/admin/keysets.go +++ b/internal/routes/admin/keysets.go @@ -3,27 +3,24 @@ package admin import ( "encoding/hex" "fmt" - "log/slog" - "os" - "strconv" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" - "github.com/lescuer97/nutmix/internal/database" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" + "log/slog" + "os" + "strconv" ) -func KeysetsPage(pool *pgxpool.Pool, mint *m.Mint) gin.HandlerFunc { +func KeysetsPage(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { c.HTML(200, "keysets.html", nil) } } -func KeysetsLayoutPage(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func KeysetsLayoutPage(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { type KeysetData struct { @@ -39,7 +36,7 @@ func KeysetsLayoutPage(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gi Keysets []KeysetData }{} - seeds, err := database.GetAllSeeds(pool) + seeds, err := mint.MintDB.GetAllSeeds() if err != nil { logger.Error("database.GetAllSeeds(pool) %+v", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(500, "Server side error") @@ -65,8 +62,8 @@ type RotateRequest struct { Fee int } -func rotateSatsSeed(pool *pgxpool.Pool, mint *m.Mint, rotateRequest RotateRequest) error { - seeds, err := database.GetSeedsByUnit(pool, cashu.Sat) +func rotateSatsSeed(mint *m.Mint, rotateRequest RotateRequest) error { + seeds, err := mint.MintDB.GetSeedsByUnit(cashu.Sat) if err != nil { return fmt.Errorf("database.GetSeedsByUnit(pool, cashu.Sat). %w", err) @@ -103,11 +100,11 @@ func rotateSatsSeed(pool *pgxpool.Pool, mint *m.Mint, rotateRequest RotateReques generatedSeed.InputFeePpk = rotateRequest.Fee // add new key to db - err = database.SaveNewSeed(pool, &generatedSeed) + err = mint.MintDB.SaveNewSeed(generatedSeed) if err != nil { return fmt.Errorf(`database.SaveNewSeed(pool, &generatedSeed). %w`, err) } - err = database.UpdateActiveStatusSeeds(pool, seeds) + err = mint.MintDB.UpdateSeedsActiveStatus(seeds) if err != nil { return fmt.Errorf(`database.UpdateActiveStatusSeeds(pool, seeds). %w`, err) } @@ -127,7 +124,7 @@ func rotateSatsSeed(pool *pgxpool.Pool, mint *m.Mint, rotateRequest RotateReques return nil } -func RotateSatsSeed(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func RotateSatsSeed(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { var rotateRequest RotateRequest if c.ContentType() == gin.MIMEJSON { @@ -156,7 +153,7 @@ func RotateSatsSeed(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.H rotateRequest.Fee = newSeedFee } - err := rotateSatsSeed(pool, mint, rotateRequest) + err := rotateSatsSeed(mint, rotateRequest) if err != nil { logger.Error( diff --git a/internal/routes/admin/lightning.go b/internal/routes/admin/lightning.go index 4f9de08..7b75bfa 100644 --- a/internal/routes/admin/lightning.go +++ b/internal/routes/admin/lightning.go @@ -2,11 +2,10 @@ package admin import ( "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" m "github.com/lescuer97/nutmix/internal/mint" ) -func LightningDataFormFields(pool *pgxpool.Pool, mint *m.Mint) gin.HandlerFunc { +func LightningDataFormFields(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { backend := c.Query(m.MINT_LIGHTNING_BACKEND_ENV) diff --git a/internal/routes/admin/main.go b/internal/routes/admin/main.go index 8872297..7b43063 100644 --- a/internal/routes/admin/main.go +++ b/internal/routes/admin/main.go @@ -11,7 +11,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" @@ -21,7 +20,7 @@ type ErrorNotif struct { Error string } -func AdminRoutes(ctx context.Context, r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger *slog.Logger) { +func AdminRoutes(ctx context.Context, r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { testPath := os.Getenv("TEST_PATH") if testPath != "" { r.Static("static", testPath+"static") @@ -39,24 +38,24 @@ func AdminRoutes(ctx context.Context, r *gin.Engine, pool *pgxpool.Pool, mint *m // PAGES SETUP // This is /admin - adminRoute.GET("", InitPage(pool, mint)) - adminRoute.GET("/keysets", KeysetsPage(pool, mint)) - adminRoute.GET("/settings", MintSettingsPage(pool, mint)) - adminRoute.GET("/login", LoginPage(pool, logger, mint)) - adminRoute.GET("/bolt11", LightningNodePage(pool, mint)) + adminRoute.GET("", InitPage(mint)) + adminRoute.GET("/keysets", KeysetsPage(mint)) + adminRoute.GET("/settings", MintSettingsPage(mint)) + adminRoute.GET("/login", LoginPage(logger, mint)) + adminRoute.GET("/bolt11", LightningNodePage(mint)) // change routes - adminRoute.POST("/login", Login(pool, mint, logger)) - adminRoute.POST("/mintsettings", MintSettingsForm(pool, mint, logger)) - adminRoute.POST("/bolt11", Bolt11Post(pool, mint, logger)) - adminRoute.POST("/rotate/sats", RotateSatsSeed(pool, mint, logger)) + adminRoute.POST("/login", Login(mint, logger)) + adminRoute.POST("/mintsettings", MintSettingsForm(mint, logger)) + adminRoute.POST("/bolt11", Bolt11Post(mint, logger)) + adminRoute.POST("/rotate/sats", RotateSatsSeed(mint, logger)) // fractional html components - adminRoute.GET("/keysets-layout", KeysetsLayoutPage(pool, mint, logger)) - adminRoute.GET("/lightningdata", LightningDataFormFields(pool, mint)) - adminRoute.GET("/mint-balance", MintBalance(pool, mint, logger)) - adminRoute.GET("/mint-melt-summary", MintMeltSummary(pool, mint, logger)) - adminRoute.GET("/mint-melt-list", MintMeltList(pool, mint, logger)) + adminRoute.GET("/keysets-layout", KeysetsLayoutPage(mint, logger)) + adminRoute.GET("/lightningdata", LightningDataFormFields(mint)) + adminRoute.GET("/mint-balance", MintBalance(mint, logger)) + adminRoute.GET("/mint-melt-summary", MintMeltSummary(mint, logger)) + adminRoute.GET("/mint-melt-list", MintMeltList(mint, logger)) adminRoute.GET("/logs", LogsTab(logger)) } diff --git a/internal/routes/admin/mint-activity.go b/internal/routes/admin/mint-activity.go index 01bb9f0..e72d4c8 100644 --- a/internal/routes/admin/mint-activity.go +++ b/internal/routes/admin/mint-activity.go @@ -1,20 +1,17 @@ package admin import ( - "log/slog" - "sort" - "time" - "github.com/btcsuite/btcd/btcutil" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/lescuer97/nutmix/internal/database" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" "github.com/lightningnetwork/lnd/zpay32" + "log/slog" + "sort" + "time" ) -func MintBalance(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func MintBalance(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { @@ -43,14 +40,14 @@ func MintBalance(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.Hand } } -func MintMeltSummary(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func MintMeltSummary(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { timeHeader := c.GetHeader("time") timeRequestDuration := ParseToTimeRequest(timeHeader) - mintMeltBalance, err := database.GetMintMeltBalanceByTime(pool, timeRequestDuration.RollBackFromNow().Unix()) + mintMeltBalance, err := mint.MintDB.GetMintMeltBalanceByTime(timeRequestDuration.RollBackFromNow().Unix()) if err != nil { logger.Error( @@ -101,12 +98,12 @@ func MintMeltSummary(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin. c.HTML(200, "mint-melt-activity", mintMeltTotal) } } -func MintMeltList(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func MintMeltList(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { timeHeader := c.GetHeader("time") timeRequestDuration := ParseToTimeRequest(timeHeader) - mintMeltBalance, err := database.GetMintMeltBalanceByTime(pool, timeRequestDuration.RollBackFromNow().Unix()) + mintMeltBalance, err := mint.MintDB.GetMintMeltBalanceByTime(timeRequestDuration.RollBackFromNow().Unix()) if err != nil { logger.Error( diff --git a/internal/routes/admin/pages.go b/internal/routes/admin/pages.go index 2e9256a..913e937 100644 --- a/internal/routes/admin/pages.go +++ b/internal/routes/admin/pages.go @@ -5,7 +5,6 @@ import ( "os" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" "github.com/lescuer97/nutmix/internal/database" "github.com/lescuer97/nutmix/internal/mint" @@ -17,7 +16,7 @@ type LoginParams struct { ADMINNPUB string } -func LoginPage(pool *pgxpool.Pool, logger *slog.Logger, mint *mint.Mint) gin.HandlerFunc { +func LoginPage(logger *slog.Logger, mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { // generate nonce for login nostr @@ -30,13 +29,13 @@ func LoginPage(pool *pgxpool.Pool, logger *slog.Logger, mint *mint.Mint) gin.Han } } - nostrLogin := cashu.NostrLoginAuth{ + nostrLogin := database.NostrLoginAuth{ Nonce: nonce, Expiry: int(cashu.ExpiryTimeMinUnit(15)), Activated: false, } - err = database.SaveNostrLoginAuth(pool, nostrLogin) + err = mint.MintDB.SaveNostrAuth(nostrLogin) if err != nil { logger.Error( "database.SaveNostrLoginAuth(pool, nostrLogin)", @@ -66,7 +65,7 @@ func LoginPage(pool *pgxpool.Pool, logger *slog.Logger, mint *mint.Mint) gin.Han } } -func InitPage(pool *pgxpool.Pool, mint *mint.Mint) gin.HandlerFunc { +func InitPage(mint *mint.Mint) gin.HandlerFunc { return func(c *gin.Context) { c.HTML(200, "mint_activity.html", nil) } diff --git a/internal/routes/admin/tabs.go b/internal/routes/admin/tabs.go index 3425921..dbf2c50 100644 --- a/internal/routes/admin/tabs.go +++ b/internal/routes/admin/tabs.go @@ -6,7 +6,6 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/internal/lightning" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" @@ -14,7 +13,7 @@ import ( "github.com/nbd-wtf/go-nostr/nip19" ) -func MintSettingsPage(pool *pgxpool.Pool, mint *m.Mint) gin.HandlerFunc { +func MintSettingsPage(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { c.HTML(200, "settings.html", mint.Config) } @@ -50,7 +49,7 @@ func isNostrKeyValid(nostrKey string) (bool, error) { } -func MintSettingsForm(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func MintSettingsForm(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { // check the different variables that could change @@ -154,13 +153,13 @@ func MintSettingsForm(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin } } -func LightningNodePage(pool *pgxpool.Pool, mint *m.Mint) gin.HandlerFunc { +func LightningNodePage(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { c.HTML(200, "bolt11.html", mint.Config) } } -func Bolt11Post(pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { +func Bolt11Post(mint *m.Mint, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index cd6835c..8064df7 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -4,15 +4,8 @@ import ( "encoding/hex" "errors" "fmt" - "log/slog" - "slices" - "strings" - "time" - "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" - "github.com/lescuer97/nutmix/internal/database" "github.com/lescuer97/nutmix/internal/lightning" "github.com/lescuer97/nutmix/internal/mint" m "github.com/lescuer97/nutmix/internal/mint" @@ -20,9 +13,13 @@ import ( "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/zpay32" + "log/slog" + "slices" + "strings" + "time" ) -func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger *slog.Logger) { +func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { v1 := r.Group("/v1") v1.POST("/mint/quote/bolt11", func(c *gin.Context) { @@ -92,7 +89,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * mintRequestDB.State = cashu.PAID } - err = database.SaveMintRequestDB(pool, mintRequestDB) + err = mint.MintDB.SaveMintRequest(mintRequestDB) if err != nil { logger.Error(fmt.Errorf("SaveQuoteRequest: %w", err).Error()) @@ -106,7 +103,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * v1.GET("/mint/quote/bolt11/:quote", func(c *gin.Context) { quoteId := c.Param("quote") - quote, err := database.GetMintQuoteById(pool, quoteId) + quote, err := mint.MintDB.GetMintRequestById(quoteId) if quote.State == cashu.PAID || quote.State == cashu.ISSUED { c.JSON(200, quote) @@ -118,7 +115,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - state, _, err := mint.VerifyLightingPaymentHappened(pool, quote.RequestPaid, quote.Quote, database.ModifyQuoteMintPayStatus) + state, _, err := mint.VerifyLightingPaymentHappened(quote.RequestPaid, quote.Quote, mint.MintDB.ChangeMintRequestState) if err != nil { logger.Warn(fmt.Errorf("VerifyLightingPaymentHappened: %w", err).Error()) @@ -153,7 +150,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - quote, err := database.GetMintQuoteById(pool, mintRequest.Quote) + quote, err := mint.MintDB.GetMintRequestById(mintRequest.Quote) if err != nil { mint.ActiveQuotes.RemoveQuote(quote.Quote) @@ -178,7 +175,8 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * blindedSignatures := []cashu.BlindSignature{} recoverySigsDb := []cashu.RecoverSigDB{} - state, _, err := mint.VerifyLightingPaymentHappened(pool, quote.RequestPaid, quote.Quote, database.ModifyQuoteMintPayStatus) + state, _, err := mint.VerifyLightingPaymentHappened(quote.RequestPaid, quote.Quote, mint.MintDB.ChangeMintRequestState) + if err != nil { mint.ActiveQuotes.RemoveQuote(quote.Quote) if errors.Is(err, invoices.ErrInvoiceNotFound) || strings.Contains(err.Error(), "NotFound") { @@ -243,13 +241,13 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * quote.Minted = true quote.State = cashu.ISSUED - err = database.ModifyQuoteMintMintedStatus(pool, quote.Minted, quote.State, quote.Quote) + err = mint.MintDB.ChangeMintRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Minted) if err != nil { logger.Error(fmt.Errorf("ModifyQuoteMintMintedStatus: %w", err).Error()) mint.ActiveQuotes.RemoveQuote(quote.Quote) } - err = database.SetRestoreSigs(pool, recoverySigsDb) + err = mint.MintDB.SaveRestoreSigs(recoverySigsDb) if err != nil { mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("SetRecoverySigs: %w", err).Error()) @@ -361,7 +359,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * Mpp: isMpp, } - err = database.SaveQuoteMeltRequest(pool, dbRequest) + err = mint.MintDB.SaveMeltRequest(dbRequest) if err != nil { logger.Warn(fmt.Errorf("SaveQuoteMeltRequest: %w", err).Error()) @@ -376,7 +374,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * v1.GET("/melt/quote/bolt11/:quote", func(c *gin.Context) { quoteId := c.Param("quote") - quote, err := database.GetMeltQuoteById(pool, quoteId) + quote, err := mint.MintDB.GetMeltRequestById(quoteId) if err != nil { logger.Warn(fmt.Errorf("database.GetMeltQuoteById: %w", err).Error()) c.JSON(500, "Opps!, something went wrong") @@ -388,7 +386,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - state, preimage, err := mint.VerifyLightingPaymentHappened(pool, quote.RequestPaid, quote.Quote, database.ModifyQuoteMeltPayStatus) + state, preimage, err := mint.VerifyLightingPaymentHappened(quote.RequestPaid, quote.Quote, mint.MintDB.ChangeMeltRequestState) if err != nil { if errors.Is(err, invoices.ErrInvoiceNotFound) || strings.Contains(err.Error(), "NotFound") { c.JSON(200, quote.GetPostMeltQuoteResponse()) @@ -404,7 +402,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * quote.RequestPaid = true } - err = database.AddPaymentPreimageToMeltRequest(pool, preimage, quote.Quote) + err = mint.MintDB.AddPreimageMeltRequest(quote.Quote, preimage) if err != nil { logger.Error(fmt.Errorf("database.AddPaymentPreimageToMeltRequest(pool, : %w", err).Error()) c.JSON(200, quote.GetPostMeltQuoteResponse()) @@ -436,7 +434,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - quote, err := database.GetMeltQuoteById(pool, meltRequest.Quote) + quote, err := mint.MintDB.GetMeltRequestById(meltRequest.Quote) if err != nil { mint.RemoveQuotesAndProofs(meltRequest.Quote, meltRequest.Inputs) @@ -500,7 +498,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * } // check if we know any of the proofs - knownProofs, err := database.CheckListOfProofs(pool, SecretsList) + knownProofs, err := mint.MintDB.GetProofsFromSecret(SecretsList) if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) @@ -547,7 +545,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * quote.State = cashu.PENDING response := quote.GetPostMeltQuoteResponse() - err = database.ModifyQuoteMeltPayStatusAndMelted(pool, quote.RequestPaid, quote.Melted, quote.State, quote.Quote) + err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) if err != nil { mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("ModifyQuoteMeltPayStatusAndMelted: %w", err).Error()) @@ -562,7 +560,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * quote.State = cashu.PENDING response := quote.GetPostMeltQuoteResponse() - err = database.ModifyQuoteMeltPayStatusAndMelted(pool, quote.RequestPaid, quote.Melted, quote.State, quote.Quote) + err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) if err != nil { mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("ModifyQuoteMeltPayStatusAndMelted: %w", err).Error()) @@ -608,7 +606,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } - err = database.SetRestoreSigs(pool, recoverySigsDb) + err = mint.MintDB.SaveRestoreSigs(recoverySigsDb) if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) @@ -619,7 +617,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * response.Change = blindSignatures } - err = database.ModifyQuoteMeltPayStatusAndMelted(pool, quote.RequestPaid, quote.Melted, quote.State, quote.Quote) + err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Error(fmt.Errorf("ModifyQuoteMeltPayStatusAndMelted: %w", err).Error()) @@ -627,7 +625,7 @@ func v1bolt11Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger * return } // send proofs to database - err = database.SaveProofs(pool, meltRequest.Inputs) + err = mint.MintDB.SaveProof(meltRequest.Inputs) if err != nil { mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) diff --git a/internal/routes/mint.go b/internal/routes/mint.go index b805bf1..207ceb6 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -3,16 +3,14 @@ package routes import ( "fmt" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/api/cashu" - "github.com/lescuer97/nutmix/internal/database" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" "log/slog" "slices" ) -func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog.Logger) { +func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { v1 := r.Group("/v1") v1.GET("/keys", func(c *gin.Context) { @@ -42,7 +40,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. }) v1.GET("/keysets", func(c *gin.Context) { - seeds, err := database.GetAllSeeds(pool) + seeds, err := mint.MintDB.GetAllSeeds() if err != nil { logger.Error(fmt.Errorf("could not get keysets, database.GetAllSeeds(pool) %w", err).Error()) c.JSON(500, "Server side error") @@ -249,7 +247,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. } // check if we know any of the proofs - knownProofs, err := database.CheckListOfProofs(pool, SecretsList) + knownProofs, err := mint.MintDB.GetProofsFromSecret(SecretsList) if err != nil { logger.Error("database.CheckListOfProofs(pool, SecretsList)", slog.String(utils.LogExtraInfo, err.Error())) @@ -288,7 +286,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. } // send proofs to database - err = database.SaveProofs(pool, swapRequest.Inputs) + err = mint.MintDB.SaveProof(swapRequest.Inputs) if err != nil { mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) @@ -298,7 +296,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. return } - err = database.SetRestoreSigs(pool, recoverySigsDb) + err = mint.MintDB.SaveRestoreSigs(recoverySigsDb) if err != nil { mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Error("database.SetRestoreSigs", slog.String(utils.LogExtraInfo, err.Error())) @@ -324,7 +322,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. States: make([]cashu.CheckState, 0), } // set as unspent - proofs, err := database.CheckListOfProofsBySecretCurve(pool, checkStateRequest.Ys) + proofs, err := mint.MintDB.GetProofsFromSecretCurve(checkStateRequest.Ys) proofsForRemoval := make([]cashu.Proof, 0) @@ -391,7 +389,7 @@ func v1MintRoutes(r *gin.Engine, pool *pgxpool.Pool, mint *m.Mint, logger *slog. blindingFactors = append(blindingFactors, output.B_) } - blindRecoverySigs, err := database.GetRestoreSigsFromBlindedMessages(pool, blindingFactors) + blindRecoverySigs, err := mint.MintDB.GetRestoreSigsFromBlindedMessages(blindingFactors) if err != nil { logger.Error("database.GetRestoreSigsFromBlindedMessages", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(500, "Opps!, something went wrong") diff --git a/internal/routes/routes.go b/internal/routes/routes.go index c159b2f..fee2861 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -4,12 +4,11 @@ import ( "log/slog" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" "github.com/lescuer97/nutmix/internal/mint" ) -func V1Routes(r *gin.Engine, pool *pgxpool.Pool, mint *mint.Mint, logger *slog.Logger) { - v1MintRoutes(r, pool, mint, logger) - v1bolt11Routes(r, pool, mint, logger) +func V1Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { + v1MintRoutes(r, mint, logger) + v1bolt11Routes(r, mint, logger) } From 73b2598e25ae0b0b0497316155be469c510eb4d2 Mon Sep 17 00:00:00 2001 From: leonardo Date: Wed, 23 Oct 2024 22:10:04 +0100 Subject: [PATCH 05/13] finish up mock db --- internal/database/mock_db/main.go | 83 ++++++++++++++++++---------- internal/database/postgresql/main.go | 28 ---------- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/internal/database/mock_db/main.go b/internal/database/mock_db/main.go index 2cc2566..ba92507 100644 --- a/internal/database/mock_db/main.go +++ b/internal/database/mock_db/main.go @@ -13,7 +13,6 @@ var DBError = errors.New("ERROR DATABASE") var DATABASE_URL_ENV = "DATABASE_URL" type MockDB struct { - // pool *pgxpool.Pool Proofs []cashu.Proof MeltRequest []cashu.MeltRequestDB MintRequest []cashu.MintRequestDB @@ -27,11 +26,11 @@ func databaseError(err error) error { return fmt.Errorf("%w %w", DBError, err) } -func (m MockDB) GetAllSeeds() ([]cashu.Seed, error) { +func (m *MockDB) GetAllSeeds() ([]cashu.Seed, error) { return m.Seeds, nil } -func (m MockDB) GetSeedsByUnit(unit cashu.Unit) ([]cashu.Seed, error) { +func (m *MockDB) GetSeedsByUnit(unit cashu.Unit) ([]cashu.Seed, error) { var seeds []cashu.Seed for i := 0; i < len(m.Seeds); i++ { @@ -44,31 +43,47 @@ func (m MockDB) GetSeedsByUnit(unit cashu.Unit) ([]cashu.Seed, error) { return seeds, nil } -func (m MockDB) SaveNewSeed(seed cashu.Seed) error { +func (m *MockDB) SaveNewSeed(seed cashu.Seed) error { + m.Seeds = append(m.Seeds, seed) return nil } -func (m MockDB) SaveNewSeeds(seeds []cashu.Seed) error { +func (m *MockDB) SaveNewSeeds(seeds []cashu.Seed) error { + m.Seeds = append(m.Seeds, seeds...) return nil } -func (m MockDB) UpdateSeedsActiveStatus(seeds []cashu.Seed) error { - return nil -} +func (m *MockDB) UpdateSeedsActiveStatus(seeds []cashu.Seed) error { + for i := 0; i < len(m.Seeds); i++ { + for j := 0; j < len(seeds); j++ { + if m.Seeds[i].Id == seeds[j].Id { + m.Seeds[i].Active = seeds[j].Active + break + } + } + + } -func (m MockDB) UpdateSeedEncryption(seed cashu.Seed) error { return nil } -func (m MockDB) SaveMintRequest(request cashu.MintRequestDB) error { +func (m *MockDB) SaveMintRequest(request cashu.MintRequestDB) error { + m.MintRequest = append(m.MintRequest, request) return nil } -func (m MockDB) ChangeMintRequestState(quote string, paid bool, state cashu.ACTION_STATE, minted bool) error { +func (m *MockDB) ChangeMintRequestState(quote string, paid bool, state cashu.ACTION_STATE, minted bool) error { + for i := 0; i < len(m.MintRequest); i++ { + if m.MintRequest[i].Quote == quote { + m.MintRequest[i].State = state + m.MintRequest[i].Minted = minted + } + + } return nil } -func (m MockDB) GetMintRequestById(id string) (cashu.MintRequestDB, error) { +func (m *MockDB) GetMintRequestById(id string) (cashu.MintRequestDB, error) { var mintRequests []cashu.MintRequestDB for i := 0; i < len(m.MintRequest); i++ { @@ -82,7 +97,7 @@ func (m MockDB) GetMintRequestById(id string) (cashu.MintRequestDB, error) { return mintRequests[0], nil } -func (m MockDB) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) { +func (m *MockDB) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) { var meltRequests []cashu.MeltRequestDB for i := 0; i < len(m.MeltRequest); i++ { @@ -96,29 +111,38 @@ func (m MockDB) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) { return meltRequests[0], nil } -func (m MockDB) ModifyQuoteMintMintedStatus(minted bool, state cashu.ACTION_STATE, quote string) error { - return nil -} -func (m MockDB) SaveMeltRequest(request cashu.MeltRequestDB) error { +func (m *MockDB) SaveMeltRequest(request cashu.MeltRequestDB) error { + + m.MeltRequest = append(m.MeltRequest, request) return nil } -func (m MockDB) AddPreimageMeltRequest(preimage string, quote string) error { - return nil +func (m *MockDB) AddPreimageMeltRequest(preimage string, quote string) error { + for i := 0; i < len(m.MeltRequest); i++ { + if m.MeltRequest[i].Quote == quote { + m.MeltRequest[i].PaymentPreimage = preimage + } -} -func (m MockDB) ChangeMeltRequestState(quote string, paid bool, state cashu.ACTION_STATE, melted bool) error { + } return nil } -func (m MockDB) ModifyQuoteMeltMeltedStatus(melted bool, quote string) error { +func (m *MockDB) ChangeMeltRequestState(quote string, paid bool, state cashu.ACTION_STATE, melted bool) error { + for i := 0; i < len(m.MeltRequest); i++ { + if m.MeltRequest[i].Quote == quote { + m.MeltRequest[i].RequestPaid = paid + m.MeltRequest[i].State = state + m.MeltRequest[i].Melted = melted + } + + } return nil } -func (m MockDB) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) { +func (m *MockDB) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) { var proofs []cashu.Proof for i := 0; i < len(SecretList); i++ { @@ -138,12 +162,13 @@ func (m MockDB) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) return proofs, nil } -func (m MockDB) SaveProof(proofs []cashu.Proof) error { +func (m *MockDB) SaveProof(proofs []cashu.Proof) error { + m.Proofs = append(m.Proofs, proofs...) return nil } -func (m MockDB) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, error) { +func (m *MockDB) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, error) { var proofs []cashu.Proof for i := 0; i < len(Ys); i++ { @@ -163,7 +188,7 @@ func (m MockDB) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, error) { return proofs, nil } -func (m MockDB) GetRestoreSigsFromBlindedMessages(B_ []string) ([]cashu.RecoverSigDB, error) { +func (m *MockDB) GetRestoreSigsFromBlindedMessages(B_ []string) ([]cashu.RecoverSigDB, error) { var restore []cashu.RecoverSigDB for i := 0; i < len(B_); i++ { @@ -183,10 +208,8 @@ func (m MockDB) GetRestoreSigsFromBlindedMessages(B_ []string) ([]cashu.RecoverS return restore, nil } -func (m MockDB) SaveRestoreSigs(recover_sigs []cashu.RecoverSigDB) error { +func (m *MockDB) SaveRestoreSigs(recover_sigs []cashu.RecoverSigDB) error { + m.RecoverSigDB = append(m.RecoverSigDB, recover_sigs...) return nil } - -func (m MockDB) Close() { -} diff --git a/internal/database/postgresql/main.go b/internal/database/postgresql/main.go index 85279da..f357d06 100644 --- a/internal/database/postgresql/main.go +++ b/internal/database/postgresql/main.go @@ -237,25 +237,6 @@ func (pql Postgresql) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) return quote, nil } -func (pql Postgresql) ModifyQuoteMintMintedStatus(minted bool, state cashu.ACTION_STATE, quote string) error { - - args := pgx.NamedArgs{ - "state": state, - "minted": minted, - "quote": quote, - } - - query := `UPDATE mint_request SET minted = @minted, state = @state WHERE quote = @quote` - - // change the paid status of the quote - _, err := pql.pool.Exec(context.Background(), query, args) - - if err != nil { - return databaseError(fmt.Errorf("Inserting to mint_request: %w", err)) - - } - return nil -} func (pql Postgresql) SaveMeltRequest(request cashu.MeltRequestDB) error { _, err := pql.pool.Exec(context.Background(), "INSERT INTO melt_request (quote, request, fee_reserve, expiry, unit, amount, request_paid, melted, state, payment_preimage, seen_at, mpp) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", request.Quote, request.Request, request.FeeReserve, request.Expiry, request.Unit, request.Amount, request.RequestPaid, request.Melted, request.State, request.PaymentPreimage, request.SeenAt, request.Mpp) @@ -283,15 +264,6 @@ func (pql Postgresql) ChangeMeltRequestState(quote string, paid bool, state cash } return nil } -func (pql Postgresql) ModifyQuoteMeltMeltedStatus(melted bool, quote string) error { - // change the paid status of the quote - _, err := pql.pool.Exec(context.Background(), "UPDATE melt_request SET melted = $1 WHERE quote = $2", melted, quote) - if err != nil { - return databaseError(fmt.Errorf("updating mint_request: %w", err)) - - } - return nil -} func (pql Postgresql) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, error) { From d6246771ba36c3abbdd4b0f9cab5f0874169222d Mon Sep 17 00:00:00 2001 From: leonardo Date: Wed, 30 Oct 2024 12:04:16 +0000 Subject: [PATCH 06/13] defer lock and and state to proofs db --- api/cashu/types.go | 15 +++---- internal/database/postgresql/main.go | 8 ++-- internal/lightning/fake_wallet.go | 56 +++++++++++++++++++++++++- internal/routes/bolt11.go | 59 ++++++++++++++-------------- internal/routes/mint.go | 8 +--- internal/utils/proofs.go | 2 + migrations/17_add_proofs_state.sql | 11 ++++++ 7 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 migrations/17_add_proofs_state.sql diff --git a/api/cashu/types.go b/api/cashu/types.go index d33f185..f15a55d 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -145,13 +145,14 @@ const PROOF_SPENT ProofState = "SPENT" const PROOF_PENDING ProofState = "PENDING" type Proof struct { - Amount uint64 `json:"amount"` - Id string `json:"id"` - Secret string `json:"secret"` - C string `json:"C" db:"c"` - Y string `json:"Y" db:"y"` - Witness string `json:"witness" db:"witness"` - SeenAt int64 `json:"seen_at"` + Amount uint64 `json:"amount"` + Id string `json:"id"` + Secret string `json:"secret"` + C string `json:"C" db:"c"` + Y string `json:"Y" db:"y"` + Witness string `json:"witness" db:"witness"` + SeenAt int64 `json:"seen_at"` + State ProofState `json:"state"` } func (p Proof) VerifyWitness(spendCondition *SpendCondition, witness *Witness, pubkeysFromProofs *map[*btcec.PublicKey]bool) (bool, error) { diff --git a/internal/database/postgresql/main.go b/internal/database/postgresql/main.go index f357d06..139a75f 100644 --- a/internal/database/postgresql/main.go +++ b/internal/database/postgresql/main.go @@ -270,7 +270,7 @@ func (pql Postgresql) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, e var proofList []cashu.Proof ctx := context.Background() - rows, err := pql.pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE secret = ANY($1)", SecretList) + rows, err := pql.pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at, state FROM proofs WHERE secret = ANY($1)", SecretList) defer rows.Close() @@ -297,13 +297,13 @@ func (pql Postgresql) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, e func (pql Postgresql) SaveProof(proofs []cashu.Proof) error { entries := [][]any{} - columns := []string{"c", "secret", "amount", "id", "y", "witness", "seen_at"} + columns := []string{"c", "secret", "amount", "id", "y", "witness", "seen_at", "state"} tableName := "proofs" tries := 0 for _, proof := range proofs { - entries = append(entries, []any{proof.C, proof.Secret, proof.Amount, proof.Id, proof.Y, proof.Witness, proof.SeenAt}) + entries = append(entries, []any{proof.C, proof.Secret, proof.Amount, proof.Id, proof.Y, proof.Witness, proof.SeenAt, proof.State}) } for { @@ -327,7 +327,7 @@ func (pql Postgresql) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, erro var proofList []cashu.Proof - rows, err := pql.pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at FROM proofs WHERE y = ANY($1)`, Ys) + rows, err := pql.pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at, state FROM proofs WHERE y = ANY($1)`, Ys) defer rows.Close() if err != nil { diff --git a/internal/lightning/fake_wallet.go b/internal/lightning/fake_wallet.go index 1f26b36..e107b61 100644 --- a/internal/lightning/fake_wallet.go +++ b/internal/lightning/fake_wallet.go @@ -2,19 +2,63 @@ package lightning import ( "fmt" + "slices" + "github.com/btcsuite/btcd/chaincfg" "github.com/google/uuid" "github.com/lescuer97/nutmix/api/cashu" "github.com/lightningnetwork/lnd/zpay32" ) +type FakeWalletError int + +const ( + NONE = 0 + + FailPaymentPending = iota + 1 + FailPaymentFailed = iota + 2 + FailPaymentUnknown = iota + 3 + + FailQueryPending = iota + 4 + FailQueryFailed = iota + 5 + FailQueryUnknown = iota + 6 +) + type FakeWallet struct { - Network chaincfg.Params + Network chaincfg.Params + UnpurposeErrors []FakeWalletError } const mock_preimage = "fakewalletpreimage" func (f FakeWallet) PayInvoice(invoice string, zpayInvoice *zpay32.Invoice, feeReserve uint64, mpp bool, amount_sat uint64) (PaymentResponse, error) { + switch { + case slices.Contains(f.UnpurposeErrors, FailPaymentUnknown): + return PaymentResponse{ + Preimage: "", + PaymentRequest: "", + PaymentState: UNKNOWN, + Rhash: "", + PaidFeeSat: 0, + }, nil + + case slices.Contains(f.UnpurposeErrors, FailPaymentFailed): + return PaymentResponse{ + Preimage: "", + PaymentRequest: "", + PaymentState: FAILED, + Rhash: "", + PaidFeeSat: 0, + }, nil + case slices.Contains(f.UnpurposeErrors, FailPaymentPending): + return PaymentResponse{ + Preimage: "", + PaymentRequest: "", + PaymentState: PENDING, + Rhash: "", + PaidFeeSat: 0, + }, nil + } return PaymentResponse{ Preimage: mock_preimage, @@ -26,6 +70,16 @@ func (f FakeWallet) PayInvoice(invoice string, zpayInvoice *zpay32.Invoice, feeR } func (f FakeWallet) CheckPayed(quote string) (PaymentStatus, string, error) { + switch { + case slices.Contains(f.UnpurposeErrors, FailPaymentUnknown): + return UNKNOWN, "", nil + case slices.Contains(f.UnpurposeErrors, FailPaymentFailed): + return FAILED, "", nil + case slices.Contains(f.UnpurposeErrors, FailPaymentPending): + return PENDING, "", nil + + } + return SETTLED, mock_preimage, nil } diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index 8064df7..414ab8f 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -150,17 +150,16 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { return } + defer mint.ActiveQuotes.RemoveQuote(mintRequest.Quote) quote, err := mint.MintDB.GetMintRequestById(mintRequest.Quote) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("Incorrect body: %w", err).Error()) c.JSON(500, "Opps!, something went wrong") return } if quote.Minted { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Warn("Quote already minted", slog.String(utils.LogExtraInfo, quote.Quote)) c.JSON(400, cashu.ErrorCodeToResponse(cashu.TOKEN_ALREADY_ISSUED, nil)) return @@ -178,7 +177,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { state, _, err := mint.VerifyLightingPaymentHappened(quote.RequestPaid, quote.Quote, mint.MintDB.ChangeMintRequestState) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) if errors.Is(err, invoices.ErrInvoiceNotFound) || strings.Contains(err.Error(), "NotFound") { c.JSON(200, quote) return @@ -192,7 +190,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { if quote.State == cashu.PAID { quote.RequestPaid = true } else { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Debug("Quote not paid") c.JSON(400, cashu.ErrorCodeToResponse(cashu.REQUEST_NOT_PAID, nil)) return @@ -201,7 +198,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { invoice, err := zpay32.Decode(quote.Request, mint.LightningBackend.GetNetwork()) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Warn(fmt.Errorf("Mint decoding zpay32.Decode: %w", err).Error()) c.JSON(500, "Opps!, something went wrong") return @@ -210,7 +206,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { amountMilsats, err := lnrpc.UnmarshallAmt(int64(amountBlindMessages), 0) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Info(fmt.Errorf("UnmarshallAmt: %w", err).Error()) c.JSON(500, "Opps!, something went wrong") return @@ -218,7 +213,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { // check the amount in outputs are the same as the quote if int32(*invoice.MilliSat) != int32(amountMilsats) { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Info(fmt.Errorf("wrong amount of milisats: %v, needed %v", int32(*invoice.MilliSat), int32(amountMilsats)).Error()) c.JSON(403, "Amounts in outputs are not the same") return @@ -227,7 +221,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { blindedSignatures, recoverySigsDb, err = mint.SignBlindedMessages(mintRequest.Outputs, quote.Unit) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("mint.SignBlindedMessages: %w", err).Error()) if errors.Is(err, m.ErrInvalidBlindMessage) { c.JSON(403, m.ErrInvalidBlindMessage.Error()) @@ -245,16 +238,13 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { if err != nil { logger.Error(fmt.Errorf("ModifyQuoteMintMintedStatus: %w", err).Error()) - mint.ActiveQuotes.RemoveQuote(quote.Quote) } err = mint.MintDB.SaveRestoreSigs(recoverySigsDb) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("SetRecoverySigs: %w", err).Error()) logger.Error(fmt.Errorf("recoverySigsDb: %+v", recoverySigsDb).Error()) return } - mint.ActiveQuotes.RemoveQuote(quote.Quote) // Store BlidedSignature c.JSON(200, cashu.PostMintBolt11Response{ @@ -434,24 +424,22 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { return } + defer mint.RemoveQuotesAndProofs(meltRequest.Quote, meltRequest.Inputs) quote, err := mint.MintDB.GetMeltRequestById(meltRequest.Quote) if err != nil { - mint.RemoveQuotesAndProofs(meltRequest.Quote, meltRequest.Inputs) logger.Info(fmt.Errorf("GetMeltQuoteById: %w", err).Error()) c.JSON(500, "Opps!, something went wrong") return } if quote.State == cashu.PENDING { - mint.RemoveQuotesAndProofs(meltRequest.Quote, meltRequest.Inputs) logger.Warn("Quote is pending") c.JSON(400, cashu.ErrorCodeToResponse(cashu.QUOTE_PENDING, nil)) return } if quote.Melted { - mint.RemoveQuotesAndProofs(meltRequest.Quote, meltRequest.Inputs) logger.Info("Quote already melted", slog.String(utils.LogExtraInfo, quote.Quote)) c.JSON(400, cashu.ErrorCodeToResponse(cashu.INVOICE_ALREADY_PAID, nil)) return @@ -460,7 +448,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { unit, err := mint.CheckProofsAreSameUnit(meltRequest.Inputs) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info(fmt.Sprintf("CheckProofsAreSameUnit: %+v", err)) c.JSON(400, "Proofs are not the same unit") return @@ -469,7 +456,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { // TODO - REMOVE this when doing multi denomination tokens with Milisats if unit != cashu.Sat { logger.Info("Incorrect Unit for minting", slog.String(utils.LogExtraInfo, quote.Unit)) - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) c.JSON(400, cashu.ErrorCodeToResponse(cashu.UNIT_NOT_SUPPORTED, nil)) return } @@ -477,7 +463,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { // check for needed amount of fees fee, err := cashu.Fees(meltRequest.Inputs, mint.Keysets[unit.String()]) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info(fmt.Sprintf("cashu.Fees(meltRequest.Inputs, mint.Keysets[unit.String()]): %+v", err)) c.JSON(400, "Could not find keyset for proof id") return @@ -491,7 +476,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { } if AmountProofs < (quote.Amount + quote.FeeReserve + uint64(fee)) { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info(fmt.Sprintf("Not enought proofs to expend. Needs: %v", quote.Amount)) c.JSON(403, "Not enought proofs to expend. Needs: %v") return @@ -501,14 +485,12 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { knownProofs, err := mint.MintDB.GetProofsFromSecret(SecretsList) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Warn(fmt.Sprintf("CheckListOfProofs: %+v", err)) c.JSON(500, "Opps! there was an issue") return } if len(knownProofs) != 0 { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info("Proofs already used", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", knownProofs))) c.JSON(400, cashu.ErrorCodeToResponse(cashu.TOKEN_ALREADY_SPENT, nil)) return @@ -517,7 +499,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { err = mint.VerifyListOfProofs(meltRequest.Inputs, []cashu.BlindedMessage{}, unit) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Debug("Could not verify Proofs", slog.String(utils.LogExtraInfo, err.Error())) errorCode, details := utils.ParseVerifyProofError(err) c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) @@ -526,7 +507,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { invoice, err := zpay32.Decode(quote.Request, mint.LightningBackend.GetNetwork()) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info(fmt.Errorf("zpay32.Decode: %w", err).Error()) c.JSON(500, "Opps!, something went wrong") return @@ -536,6 +516,7 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { payment, err := mint.LightningBackend.PayInvoice(quote.Request, invoice, quote.FeeReserve, quote.Mpp, quote.Amount) + // Hardened error handling if err != nil || payment.PaymentState == lightning.FAILED || payment.PaymentState == lightning.UNKNOWN { // if exception of lightning payment says fail do a payment status recheck. status, _, err := mint.LightningBackend.CheckPayed(quote.Quote) @@ -547,9 +528,18 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { response := quote.GetPostMeltQuoteResponse() err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) if err != nil { - mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("ModifyQuoteMeltPayStatusAndMelted: %w", err).Error()) } + + // Save proofs with pending state + err = mint.MintDB.SaveProof(meltRequest.Inputs) + if err != nil { + logger.Error(fmt.Errorf("SaveProofs: %w", err).Error()) + logger.Error(fmt.Errorf("Proofs: %+v", meltRequest.Inputs).Error()) + c.JSON(200, response) + return + } + c.JSON(200, response) return } @@ -560,17 +550,27 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { quote.State = cashu.PENDING response := quote.GetPostMeltQuoteResponse() + // change melt request state err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) if err != nil { mint.ActiveQuotes.RemoveQuote(quote.Quote) logger.Error(fmt.Errorf("ModifyQuoteMeltPayStatusAndMelted: %w", err).Error()) } + + // Save proofs with pending state + err = mint.MintDB.SaveProof(meltRequest.Inputs) + if err != nil { + logger.Error(fmt.Errorf("SaveProofs: %w", err).Error()) + logger.Error(fmt.Errorf("Proofs: %+v", meltRequest.Inputs).Error()) + c.JSON(200, response) + return + } + c.JSON(200, response) return // finish failure and release the proofs case lightning.FAILED, lightning.UNKNOWN: - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info(fmt.Sprintf("mint.LightningComs.PayInvoice %+v", err)) c.JSON(400, "could not make payment") return @@ -600,7 +600,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { blindSignatures, recoverySigsDb, err := mint.SignBlindedMessages(change, quote.Unit) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Info("mint.SignBlindedMessages", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(500, "Opps!, something went wrong") return @@ -609,7 +608,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { err = mint.MintDB.SaveRestoreSigs(recoverySigsDb) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Error("database.SetRestoreSigs", slog.String(utils.LogExtraInfo, err.Error())) logger.Error("recoverySigsDb", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", recoverySigsDb))) } @@ -619,22 +617,25 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Error(fmt.Errorf("ModifyQuoteMeltPayStatusAndMelted: %w", err).Error()) c.JSON(200, response) return } + + // change proofs to spent + for i := 0; i < len(meltRequest.Inputs); i++ { + meltRequest.Inputs[i].State = cashu.PROOF_SPENT + } + // send proofs to database err = mint.MintDB.SaveProof(meltRequest.Inputs) if err != nil { - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) logger.Error(fmt.Errorf("SaveProofs: %w", err).Error()) logger.Error(fmt.Errorf("Proofs: %+v", meltRequest.Inputs).Error()) c.JSON(200, response) return } - mint.RemoveQuotesAndProofs(quote.Quote, meltRequest.Inputs) newPendingProofs := []cashu.Proof{} // remove proofs from pending proofs diff --git a/internal/routes/mint.go b/internal/routes/mint.go index e6cba44..af9b1fe 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -216,7 +216,6 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { unit, err := mint.CheckProofsAreSameUnit(swapRequest.Inputs) if err != nil { - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Warn("CheckProofsAreSameUnit", slog.String(utils.LogExtraInfo, err.Error())) detail := "Proofs are not the same unit" c.JSON(400, cashu.ErrorCodeToResponse(cashu.UNIT_NOT_SUPPORTED, &detail)) @@ -226,7 +225,6 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { // check for needed amount of fees fee, err := cashu.Fees(swapRequest.Inputs, mint.Keysets[unit.String()]) if err != nil { - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Warn("cashu.Fees(swapRequest.Inputs, mint.Keysets[unit.String()])", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(400, cashu.ErrorCodeToResponse(cashu.KEYSET_NOT_KNOW, nil)) return @@ -245,6 +243,7 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { c.JSON(400, "There was a problem during swapping") return } + defer mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) // check if we know any of the proofs knownProofs, err := mint.MintDB.GetProofsFromSecret(SecretsList) @@ -263,7 +262,6 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { err = mint.VerifyListOfProofs(swapRequest.Inputs, swapRequest.Outputs, unit) if err != nil { - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Warn("mint.VerifyListOfProofs", slog.String(utils.LogExtraInfo, err.Error())) errorCode, details := utils.ParseVerifyProofError(err) @@ -275,7 +273,6 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { blindedSignatures, recoverySigsDb, err := mint.SignBlindedMessages(swapRequest.Outputs, cashu.Sat.String()) if err != nil { - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Error("mint.SignBlindedMessages", slog.String(utils.LogExtraInfo, err.Error())) c.JSON(500, "Opps!, something went wrong") return @@ -289,7 +286,6 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { err = mint.MintDB.SaveProof(swapRequest.Inputs) if err != nil { - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Error("database.SaveProofs", slog.String(utils.LogExtraInfo, err.Error())) logger.Error("Proofs", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", swapRequest.Inputs))) c.JSON(200, response) @@ -298,13 +294,11 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { err = mint.MintDB.SaveRestoreSigs(recoverySigsDb) if err != nil { - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) logger.Error("database.SetRestoreSigs", slog.String(utils.LogExtraInfo, err.Error())) logger.Error("recoverySigsDb", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", recoverySigsDb))) c.JSON(200, response) return } - mint.ActiveProofs.RemoveProofs(swapRequest.Inputs) c.JSON(200, response) }) diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go index 9aadcfb..1277b97 100644 --- a/internal/utils/proofs.go +++ b/internal/utils/proofs.go @@ -51,6 +51,7 @@ func GetChangeOutput(overpaidFees uint64, outputs []cashu.BlindedMessage) []cash return outputs } +// Sets some values being used by the mint like seen, secretY, seen, and pending state func GetAndCalculateProofsValues(proofs *[]cashu.Proof) (uint64, []string, error) { now := time.Now().Unix() var totalAmount uint64 @@ -66,6 +67,7 @@ func GetAndCalculateProofsValues(proofs *[]cashu.Proof) (uint64, []string, error } (*proofs)[i] = p (*proofs)[i].SeenAt = now + (*proofs)[i].State = cashu.PROOF_PENDING } return totalAmount, SecretsList, nil diff --git a/migrations/17_add_proofs_state.sql b/migrations/17_add_proofs_state.sql new file mode 100644 index 0000000..f17e833 --- /dev/null +++ b/migrations/17_add_proofs_state.sql @@ -0,0 +1,11 @@ +-- +goose Up +ALTER TABLE proofs ADD state TEXT; + +-- if the proofs have null value on state asume they are spent +UPDATE proofs +SET state = 'SPENT' +WHERE state IS NULL; + + +-- +goose Down +ALTER TABLE proofs DROP COLUMN state From 0797b078f98bfddc5b2b3b9b2de95103b21bc6e9 Mon Sep 17 00:00:00 2001 From: leonardo Date: Sat, 2 Nov 2024 14:45:01 +0000 Subject: [PATCH 07/13] test on payments --- cmd/nutmix/main_test.go | 76 +++- cmd/nutmix/payment_error_handling_test.go | 435 ++++++++++++++++++++++ internal/database/mock_db/main.go | 2 +- internal/lightning/fake_wallet.go | 6 +- 4 files changed, 514 insertions(+), 5 deletions(-) create mode 100644 cmd/nutmix/payment_error_handling_test.go diff --git a/cmd/nutmix/main_test.go b/cmd/nutmix/main_test.go index 24a6968..4ef4ba2 100644 --- a/cmd/nutmix/main_test.go +++ b/cmd/nutmix/main_test.go @@ -19,6 +19,7 @@ import ( "github.com/gin-gonic/gin" "github.com/lescuer97/nutmix/api/cashu" "github.com/lescuer97/nutmix/internal/database" + mockdb "github.com/lescuer97/nutmix/internal/database/mock_db" pq "github.com/lescuer97/nutmix/internal/database/postgresql" "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/routes" @@ -660,7 +661,6 @@ func TestMintBolt11FakeWallet(t *testing.T) { func SetupRoutingForTesting(ctx context.Context, adminRoute bool) (*gin.Engine, *mint.Mint) { db, err := pq.DatabaseSetup(ctx, "../../migrations/") - if err != nil { log.Fatal("Error conecting to db", err) } @@ -737,6 +737,80 @@ func SetupRoutingForTesting(ctx context.Context, adminRoute bool) (*gin.Engine, return r, mint } +func SetupRoutingForTestingMockDb(ctx context.Context, adminRoute bool) (*gin.Engine, *mint.Mint) { + db := mockdb.MockDB{} + seeds, err := db.GetAllSeeds() + + if err != nil { + log.Fatalf("Could not keysets: %v", err) + } + + mint_privkey := os.Getenv("MINT_PRIVATE_KEY") + decodedPrivKey, err := hex.DecodeString(mint_privkey) + if err != nil { + log.Fatalf("hex.DecodeString(mint_privkey): %+v ", err) + } + + parsedPrivateKey := secp256k1.PrivKeyFromBytes(decodedPrivKey) + masterKey, err := mint.MintPrivateKeyToBip32(parsedPrivateKey) + if err != nil { + log.Fatalf("mint.MintPrivateKeyToBip32(parsedPrivateKey): %+v ", err) + } + // incase there are no seeds in the db we create a new one + if len(seeds) == 0 { + + if mint_privkey == "" { + log.Fatalf("No mint private key found in env") + } + seed, err := mint.CreateNewSeed(masterKey, 1, 0) + if err != nil { + log.Fatalf("mint.CreateNewSeed(masterKey, 1, 0) %+v ", err) + } + + err = db.SaveNewSeeds([]cashu.Seed{seed}) + + seeds = append(seeds, seed) + + if err != nil { + log.Fatalf("SaveNewSeed: %+v ", err) + } + + } + + config, err := mint.SetUpConfigFile() + + config.MINT_LIGHTNING_BACKEND = mint.StringToLightningBackend(os.Getenv(mint.MINT_LIGHTNING_BACKEND_ENV)) + + config.DATABASE_URL = os.Getenv(database.DATABASE_URL_ENV) + config.NETWORK = os.Getenv(mint.NETWORK_ENV) + config.LND_GRPC_HOST = os.Getenv(utils.LND_HOST) + config.LND_TLS_CERT = os.Getenv(utils.LND_TLS_CERT) + config.LND_MACAROON = os.Getenv(utils.LND_MACAROON) + config.MINT_LNBITS_KEY = os.Getenv(utils.MINT_LNBITS_KEY) + config.MINT_LNBITS_ENDPOINT = os.Getenv(utils.MINT_LNBITS_ENDPOINT) + + if err != nil { + log.Fatalf("could not setup config file: %+v ", err) + } + + mint, err := mint.SetUpMint(ctx, parsedPrivateKey, seeds, config, &db) + + if err != nil { + log.Fatalf("SetUpMint: %+v ", err) + } + + r := gin.Default() + + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + routes.V1Routes(r, mint, logger) + + if adminRoute { + admin.AdminRoutes(ctx, r, mint, logger) + } + + return r, mint +} func newBlindedMessage(id string, amount uint64, B_ *secp256k1.PublicKey) cashu.BlindedMessage { B_str := hex.EncodeToString(B_.SerializeCompressed()) diff --git a/cmd/nutmix/payment_error_handling_test.go b/cmd/nutmix/payment_error_handling_test.go new file mode 100644 index 0000000..287c9a2 --- /dev/null +++ b/cmd/nutmix/payment_error_handling_test.go @@ -0,0 +1,435 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/lightning" + "github.com/lescuer97/nutmix/internal/mint" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestPaymentFailureButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { + ctx := context.Background() + t.Setenv("MINT_PRIVATE_KEY", MintPrivateKey) + t.Setenv("MINT_LIGHTNING_BACKEND", string(mint.FAKE_WALLET)) + t.Setenv(mint.NETWORK_ENV, "regtest") + + router, mint := SetupRoutingForTestingMockDb(ctx, false) + + w := httptest.NewRecorder() + + mintQuoteRequest := cashu.PostMintQuoteBolt11Request{ + Amount: 10000, + Unit: cashu.Sat.String(), + } + jsonRequestBody, _ := json.Marshal(mintQuoteRequest) + + req := httptest.NewRequest("POST", "/v1/mint/quote/bolt11", strings.NewReader(string(jsonRequestBody))) + + router.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("Expected status code 200, got %d", w.Code) + } + + var postMintQuoteResponse cashu.MintRequestDB + err := json.Unmarshal(w.Body.Bytes(), &postMintQuoteResponse) + + if err != nil { + t.Errorf("Error unmarshalling response: %v", err) + } + + referenceKeyset := mint.ActiveKeysets[cashu.Sat.String()][1] + + // ASK FOR SUCCESSFUL MINTING + blindedMessages, mintingSecrets, mintingSecretKeys, err := CreateBlindedMessages(10000, referenceKeyset) + if err != nil { + t.Fatalf("could not createBlind message: %v", err) + } + + mintRequest := cashu.PostMintBolt11Request{ + Quote: postMintQuoteResponse.Quote, + Outputs: blindedMessages, + } + + jsonRequestBody, _ = json.Marshal(mintRequest) + + req = httptest.NewRequest("POST", "/v1/mint/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + + router.ServeHTTP(w, req) + + var postMintResponse cashu.PostMintBolt11Response + + if w.Code != 200 { + t.Fatalf("Expected status code 200, got %d", w.Code) + } + + err = json.Unmarshal(w.Body.Bytes(), &postMintResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + /// start doing melt quote + meltQuoteRequest := cashu.PostMeltQuoteBolt11Request{ + Unit: cashu.Sat.String(), + Request: RegtestRequest, + } + + jsonRequestBody, _ = json.Marshal(meltQuoteRequest) + + req = httptest.NewRequest("POST", "/v1/melt/quote/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + var postMeltQuoteResponse cashu.PostMeltQuoteBolt11Response + err = json.Unmarshal(w.Body.Bytes(), &postMeltQuoteResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + // try melting + + w.Flush() + // errors to lightning to force payment checking + fakeWallet := lightning.FakeWallet{ + Network: *mint.LightningBackend.GetNetwork(), + UnpurposeErrors: []lightning.FakeWalletError{ + lightning.FailPaymentFailed, lightning.FailQueryPending, + }, + } + + mint.LightningBackend = &fakeWallet + + meltProofs, err := GenerateProofs(postMintResponse.Signatures, mint.ActiveKeysets, mintingSecrets, mintingSecretKeys) + + // test melt tokens + meltRequest := cashu.PostMeltBolt11Request{ + Quote: postMeltQuoteResponse.Quote, + Inputs: meltProofs, + } + + jsonRequestBody, _ = json.Marshal(meltRequest) + + req = httptest.NewRequest("POST", "/v1/melt/bolt11", strings.NewReader(string(jsonRequestBody))) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + var postMeltResponse cashu.PostMeltQuoteBolt11Response + fmt.Printf("\n response: %v", w.Body.String()) + + err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + if !postMeltResponse.Paid { + t.Errorf("Expected paid to be true because it's a fake wallet, got %v", postMeltResponse.Paid) + } + + proofs, _ := mint.MintDB.GetProofsFromSecret([]string{meltProofs[0].Secret}) + + if proofs[0].State != cashu.PROOF_PENDING { + t.Errorf("Proof should be pending. it is now: %v", proofs[0].State) + } +} + +func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { + const posgrespassword = "password" + const postgresuser = "user" + ctx := context.Background() + + postgresContainer, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:16.2"), + postgres.WithDatabase("postgres"), + postgres.WithUsername(postgresuser), + postgres.WithPassword(posgrespassword), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(5*time.Second)), + ) + if err != nil { + t.Fatal(err) + } + + connUri, err := postgresContainer.ConnectionString(ctx) + + if err != nil { + t.Fatal(fmt.Errorf("failed to get connection string: %w", err)) + } + t.Setenv("MINT_PRIVATE_KEY", MintPrivateKey) + t.Setenv("MINT_LIGHTNING_BACKEND", string(mint.FAKE_WALLET)) + t.Setenv(mint.NETWORK_ENV, "regtest") + t.Setenv("DATABASE_URL", connUri) + + router, mint := SetupRoutingForTesting(ctx, false) + + w := httptest.NewRecorder() + + mintQuoteRequest := cashu.PostMintQuoteBolt11Request{ + Amount: 10000, + Unit: cashu.Sat.String(), + } + jsonRequestBody, _ := json.Marshal(mintQuoteRequest) + + req := httptest.NewRequest("POST", "/v1/mint/quote/bolt11", strings.NewReader(string(jsonRequestBody))) + + router.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("Expected status code 200, got %d", w.Code) + } + + var postMintQuoteResponse cashu.MintRequestDB + err = json.Unmarshal(w.Body.Bytes(), &postMintQuoteResponse) + + if err != nil { + t.Errorf("Error unmarshalling response: %v", err) + } + + referenceKeyset := mint.ActiveKeysets[cashu.Sat.String()][1] + + // ASK FOR SUCCESSFUL MINTING + blindedMessages, mintingSecrets, mintingSecretKeys, err := CreateBlindedMessages(10000, referenceKeyset) + if err != nil { + t.Fatalf("could not createBlind message: %v", err) + } + + mintRequest := cashu.PostMintBolt11Request{ + Quote: postMintQuoteResponse.Quote, + Outputs: blindedMessages, + } + + jsonRequestBody, _ = json.Marshal(mintRequest) + + req = httptest.NewRequest("POST", "/v1/mint/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + + router.ServeHTTP(w, req) + + var postMintResponse cashu.PostMintBolt11Response + + if w.Code != 200 { + t.Fatalf("Expected status code 200, got %d", w.Code) + } + + err = json.Unmarshal(w.Body.Bytes(), &postMintResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + /// start doing melt quote + meltQuoteRequest := cashu.PostMeltQuoteBolt11Request{ + Unit: cashu.Sat.String(), + Request: RegtestRequest, + } + + jsonRequestBody, _ = json.Marshal(meltQuoteRequest) + + req = httptest.NewRequest("POST", "/v1/melt/quote/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + var postMeltQuoteResponse cashu.PostMeltQuoteBolt11Response + err = json.Unmarshal(w.Body.Bytes(), &postMeltQuoteResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + // try melting + + w.Flush() + // errors to lightning to force payment checking + fakeWallet := lightning.FakeWallet{ + Network: *mint.LightningBackend.GetNetwork(), + UnpurposeErrors: []lightning.FakeWalletError{ + lightning.FailPaymentFailed, lightning.FailQueryPending, + }, + } + + mint.LightningBackend = &fakeWallet + + meltProofs, err := GenerateProofs(postMintResponse.Signatures, mint.ActiveKeysets, mintingSecrets, mintingSecretKeys) + + // test melt tokens + meltRequest := cashu.PostMeltBolt11Request{ + Quote: postMeltQuoteResponse.Quote, + Inputs: meltProofs, + } + + jsonRequestBody, _ = json.Marshal(meltRequest) + + req = httptest.NewRequest("POST", "/v1/melt/bolt11", strings.NewReader(string(jsonRequestBody))) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + var postMeltResponse cashu.PostMeltQuoteBolt11Response + fmt.Printf("\n response: %v", w.Body.String()) + + err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + if !postMeltResponse.Paid { + t.Errorf("Expected paid to be true because it's a fake wallet, got %v", postMeltResponse.Paid) + } + + proofs, _ := mint.MintDB.GetProofsFromSecret([]string{meltProofs[0].Secret}) + + if proofs[0].State != cashu.PROOF_PENDING { + t.Errorf("Proof should be pending. it is now: %v", proofs[0].State) + } +} + +func TestPaymentPendingButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { + ctx := context.Background() + t.Setenv("MINT_PRIVATE_KEY", MintPrivateKey) + t.Setenv("MINT_LIGHTNING_BACKEND", string(mint.FAKE_WALLET)) + t.Setenv(mint.NETWORK_ENV, "regtest") + + router, mint := SetupRoutingForTestingMockDb(ctx, false) + + w := httptest.NewRecorder() + + mintQuoteRequest := cashu.PostMintQuoteBolt11Request{ + Amount: 10000, + Unit: cashu.Sat.String(), + } + jsonRequestBody, _ := json.Marshal(mintQuoteRequest) + + req := httptest.NewRequest("POST", "/v1/mint/quote/bolt11", strings.NewReader(string(jsonRequestBody))) + + router.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("Expected status code 200, got %d", w.Code) + } + + var postMintQuoteResponse cashu.MintRequestDB + err := json.Unmarshal(w.Body.Bytes(), &postMintQuoteResponse) + + if err != nil { + t.Errorf("Error unmarshalling response: %v", err) + } + + referenceKeyset := mint.ActiveKeysets[cashu.Sat.String()][1] + + // ASK FOR SUCCESSFUL MINTING + blindedMessages, mintingSecrets, mintingSecretKeys, err := CreateBlindedMessages(10000, referenceKeyset) + if err != nil { + t.Fatalf("could not createBlind message: %v", err) + } + + mintRequest := cashu.PostMintBolt11Request{ + Quote: postMintQuoteResponse.Quote, + Outputs: blindedMessages, + } + + jsonRequestBody, _ = json.Marshal(mintRequest) + + req = httptest.NewRequest("POST", "/v1/mint/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + + router.ServeHTTP(w, req) + + var postMintResponse cashu.PostMintBolt11Response + + if w.Code != 200 { + t.Fatalf("Expected status code 200, got %d", w.Code) + } + + err = json.Unmarshal(w.Body.Bytes(), &postMintResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + /// start doing melt quote + meltQuoteRequest := cashu.PostMeltQuoteBolt11Request{ + Unit: cashu.Sat.String(), + Request: RegtestRequest, + } + + jsonRequestBody, _ = json.Marshal(meltQuoteRequest) + + req = httptest.NewRequest("POST", "/v1/melt/quote/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + var postMeltQuoteResponse cashu.PostMeltQuoteBolt11Response + err = json.Unmarshal(w.Body.Bytes(), &postMeltQuoteResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + // try melting + + w.Flush() + // errors to lightning to force payment checking + fakeWallet := lightning.FakeWallet{ + Network: *mint.LightningBackend.GetNetwork(), + UnpurposeErrors: []lightning.FakeWalletError{ + lightning.FailPaymentFailed, lightning.FailQueryPending, + }, + } + + mint.LightningBackend = &fakeWallet + + meltProofs, err := GenerateProofs(postMintResponse.Signatures, mint.ActiveKeysets, mintingSecrets, mintingSecretKeys) + + // test melt tokens + meltRequest := cashu.PostMeltBolt11Request{ + Quote: postMeltQuoteResponse.Quote, + Inputs: meltProofs, + } + + jsonRequestBody, _ = json.Marshal(meltRequest) + + req = httptest.NewRequest("POST", "/v1/melt/bolt11", strings.NewReader(string(jsonRequestBody))) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + + var postMeltResponse cashu.PostMeltQuoteBolt11Response + fmt.Printf("\n response: %v", w.Body.String()) + + err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) + + if err != nil { + t.Fatalf("Error unmarshalling response: %v", err) + } + + if !postMeltResponse.Paid { + t.Errorf("Expected paid to be true because it's a fake wallet, got %v", postMeltResponse.Paid) + } + + proofs, _ := mint.MintDB.GetProofsFromSecret([]string{meltProofs[0].Secret}) + + if proofs[0].State != cashu.PROOF_PENDING { + t.Errorf("Proof should be pending. it is now: %v", proofs[0].State) + } +} diff --git a/internal/database/mock_db/main.go b/internal/database/mock_db/main.go index ba92507..26295b4 100644 --- a/internal/database/mock_db/main.go +++ b/internal/database/mock_db/main.go @@ -101,7 +101,7 @@ func (m *MockDB) GetMeltRequestById(id string) (cashu.MeltRequestDB, error) { var meltRequests []cashu.MeltRequestDB for i := 0; i < len(m.MeltRequest); i++ { - if m.MintRequest[i].Quote == id { + if m.MeltRequest[i].Quote == id { meltRequests = append(meltRequests, m.MeltRequest[i]) } diff --git a/internal/lightning/fake_wallet.go b/internal/lightning/fake_wallet.go index e107b61..bf52065 100644 --- a/internal/lightning/fake_wallet.go +++ b/internal/lightning/fake_wallet.go @@ -71,11 +71,11 @@ func (f FakeWallet) PayInvoice(invoice string, zpayInvoice *zpay32.Invoice, feeR func (f FakeWallet) CheckPayed(quote string) (PaymentStatus, string, error) { switch { - case slices.Contains(f.UnpurposeErrors, FailPaymentUnknown): + case slices.Contains(f.UnpurposeErrors, FailQueryUnknown): return UNKNOWN, "", nil - case slices.Contains(f.UnpurposeErrors, FailPaymentFailed): + case slices.Contains(f.UnpurposeErrors, FailQueryFailed): return FAILED, "", nil - case slices.Contains(f.UnpurposeErrors, FailPaymentPending): + case slices.Contains(f.UnpurposeErrors, FailQueryPending): return PENDING, "", nil } From 2988de000372f93a0b14e0c6991ea44bea1ddb9f Mon Sep 17 00:00:00 2001 From: leonardo Date: Sat, 2 Nov 2024 20:52:50 +0000 Subject: [PATCH 08/13] changing state with special type add quote ref --- api/cashu/keys_test.go | 24 +++++++++++++++++++ api/cashu/types.go | 13 ++++++++-- cmd/nutmix/payment_error_handling_test.go | 3 --- internal/database/postgresql/main.go | 10 ++++---- internal/routes/bolt11.go | 16 +++++++++---- internal/routes/mint.go | 2 ++ internal/utils/proofs.go | 3 +-- internal/utils/proofs_test.go | 2 +- .../18_add_quote_reference_to_proofs.sql | 9 +++++++ 9 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 migrations/18_add_quote_reference_to_proofs.sql diff --git a/api/cashu/keys_test.go b/api/cashu/keys_test.go index b074f5a..bc0f532 100644 --- a/api/cashu/keys_test.go +++ b/api/cashu/keys_test.go @@ -47,3 +47,27 @@ func TestGenerateKeysetsAndIdGeneration(t *testing.T) { } } + +func TestChangeProofsStateToPending(t *testing.T) { + + proofs := Proofs{ + Proof{ + Amount: 1, + State: PROOF_UNSPENT, + }, + Proof{ + Amount: 2, + State: PROOF_UNSPENT, + }, + } + proofs.SetProofsState(PROOF_PENDING) + + if proofs[0].State != PROOF_PENDING { + t.Errorf("proof transformation not working, should be: %v ", proofs[1].State) + } + if proofs[1].State != PROOF_PENDING { + t.Errorf("proof transformation not working, should be: %v ", proofs[1].State) + + } + +} diff --git a/api/cashu/types.go b/api/cashu/types.go index f15a55d..87f0611 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -144,6 +144,14 @@ const PROOF_UNSPENT ProofState = "UNSPENT" const PROOF_SPENT ProofState = "SPENT" const PROOF_PENDING ProofState = "PENDING" +type Proofs []Proof + +func (p *Proofs) SetProofsState(state ProofState) { + for i := 0; i < len(*p); i++ { + (*p)[i].State = state + } +} + type Proof struct { Amount uint64 `json:"amount"` Id string `json:"id"` @@ -153,6 +161,7 @@ type Proof struct { Witness string `json:"witness" db:"witness"` SeenAt int64 `json:"seen_at"` State ProofState `json:"state"` + Quote string `json:"quote" db:"quote"` } func (p Proof) VerifyWitness(spendCondition *SpendCondition, witness *Witness, pubkeysFromProofs *map[*btcec.PublicKey]bool) (bool, error) { @@ -485,7 +494,7 @@ type PostMeltQuoteBolt11Response struct { } type PostSwapRequest struct { - Inputs []Proof `json:"inputs"` + Inputs Proofs `json:"inputs"` Outputs []BlindedMessage `json:"outputs"` } @@ -495,7 +504,7 @@ type PostSwapResponse struct { type PostMeltBolt11Request struct { Quote string `json:"quote"` - Inputs []Proof `json:"inputs"` + Inputs Proofs `json:"inputs"` Outputs []BlindedMessage `json:"outputs"` } diff --git a/cmd/nutmix/payment_error_handling_test.go b/cmd/nutmix/payment_error_handling_test.go index 287c9a2..9112a3a 100644 --- a/cmd/nutmix/payment_error_handling_test.go +++ b/cmd/nutmix/payment_error_handling_test.go @@ -130,7 +130,6 @@ func TestPaymentFailureButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { router.ServeHTTP(w, req) var postMeltResponse cashu.PostMeltQuoteBolt11Response - fmt.Printf("\n response: %v", w.Body.String()) err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) @@ -284,7 +283,6 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { router.ServeHTTP(w, req) var postMeltResponse cashu.PostMeltQuoteBolt11Response - fmt.Printf("\n response: %v", w.Body.String()) err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) @@ -415,7 +413,6 @@ func TestPaymentPendingButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { router.ServeHTTP(w, req) var postMeltResponse cashu.PostMeltQuoteBolt11Response - fmt.Printf("\n response: %v", w.Body.String()) err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) diff --git a/internal/database/postgresql/main.go b/internal/database/postgresql/main.go index 139a75f..5cc0392 100644 --- a/internal/database/postgresql/main.go +++ b/internal/database/postgresql/main.go @@ -246,7 +246,7 @@ func (pql Postgresql) SaveMeltRequest(request cashu.MeltRequestDB) error { return nil } -func (pql Postgresql) AddPreimageMeltRequest(preimage string, quote string) error { +func (pql Postgresql) AddPreimageMeltRequest(quote string, preimage string) error { // change the paid status of the quote _, err := pql.pool.Exec(context.Background(), "UPDATE melt_request SET payment_preimage = $1 WHERE quote = $2", preimage, quote) if err != nil { @@ -270,7 +270,7 @@ func (pql Postgresql) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, e var proofList []cashu.Proof ctx := context.Background() - rows, err := pql.pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at, state FROM proofs WHERE secret = ANY($1)", SecretList) + rows, err := pql.pool.Query(ctx, "SELECT amount, id, secret, c, y, witness, seen_at, state, quote FROM proofs WHERE secret = ANY($1)", SecretList) defer rows.Close() @@ -297,13 +297,13 @@ func (pql Postgresql) GetProofsFromSecret(SecretList []string) ([]cashu.Proof, e func (pql Postgresql) SaveProof(proofs []cashu.Proof) error { entries := [][]any{} - columns := []string{"c", "secret", "amount", "id", "y", "witness", "seen_at", "state"} + columns := []string{"c", "secret", "amount", "id", "y", "witness", "seen_at", "state", "quote"} tableName := "proofs" tries := 0 for _, proof := range proofs { - entries = append(entries, []any{proof.C, proof.Secret, proof.Amount, proof.Id, proof.Y, proof.Witness, proof.SeenAt, proof.State}) + entries = append(entries, []any{proof.C, proof.Secret, proof.Amount, proof.Id, proof.Y, proof.Witness, proof.SeenAt, proof.State, proof.Quote}) } for { @@ -327,7 +327,7 @@ func (pql Postgresql) GetProofsFromSecretCurve(Ys []string) ([]cashu.Proof, erro var proofList []cashu.Proof - rows, err := pql.pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at, state FROM proofs WHERE y = ANY($1)`, Ys) + rows, err := pql.pool.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at, state, quote FROM proofs WHERE y = ANY($1)`, Ys) defer rows.Close() if err != nil { diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index 414ab8f..a317164 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -475,6 +475,10 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { return } + // change state to pending + meltRequest.Inputs.SetProofsState(cashu.PROOF_PENDING) + quote.State = cashu.PENDING + if AmountProofs < (quote.Amount + quote.FeeReserve + uint64(fee)) { logger.Info(fmt.Sprintf("Not enought proofs to expend. Needs: %v", quote.Amount)) c.JSON(403, "Not enought proofs to expend. Needs: %v") @@ -523,7 +527,6 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { // if error on checking payement we will save as pending and returns status if err != nil { - quote.State = cashu.PENDING response := quote.GetPostMeltQuoteResponse() err = mint.MintDB.ChangeMeltRequestState(quote.Quote, quote.RequestPaid, quote.State, quote.Melted) @@ -622,11 +625,16 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { return } - // change proofs to spent - for i := 0; i < len(meltRequest.Inputs); i++ { - meltRequest.Inputs[i].State = cashu.PROOF_SPENT + err = mint.MintDB.AddPreimageMeltRequest(quote.Quote, quote.PaymentPreimage) + if err != nil { + logger.Error(fmt.Errorf("mint.MintDB.AddPreimageMeltRequest(quote.Quote, quote.PaymentPreimage) %+v", err).Error()) + c.JSON(200, response) + return } + // change proofs to spent + meltRequest.Inputs.SetProofsState(cashu.PROOF_SPENT) + // send proofs to database err = mint.MintDB.SaveProof(meltRequest.Inputs) diff --git a/internal/routes/mint.go b/internal/routes/mint.go index af9b1fe..a94f68e 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -282,6 +282,8 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { Signatures: blindedSignatures, } + swapRequest.Inputs.SetProofsState(cashu.PROOF_SPENT) + // send proofs to database err = mint.MintDB.SaveProof(swapRequest.Inputs) diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go index 1277b97..3c6c6af 100644 --- a/internal/utils/proofs.go +++ b/internal/utils/proofs.go @@ -52,7 +52,7 @@ func GetChangeOutput(overpaidFees uint64, outputs []cashu.BlindedMessage) []cash } // Sets some values being used by the mint like seen, secretY, seen, and pending state -func GetAndCalculateProofsValues(proofs *[]cashu.Proof) (uint64, []string, error) { +func GetAndCalculateProofsValues(proofs *cashu.Proofs) (uint64, []string, error) { now := time.Now().Unix() var totalAmount uint64 var SecretsList []string @@ -67,7 +67,6 @@ func GetAndCalculateProofsValues(proofs *[]cashu.Proof) (uint64, []string, error } (*proofs)[i] = p (*proofs)[i].SeenAt = now - (*proofs)[i].State = cashu.PROOF_PENDING } return totalAmount, SecretsList, nil diff --git a/internal/utils/proofs_test.go b/internal/utils/proofs_test.go index 4f99bb5..9856796 100644 --- a/internal/utils/proofs_test.go +++ b/internal/utils/proofs_test.go @@ -77,7 +77,7 @@ func MakeListofMockProofs(amounts int) []cashu.Proof { func TestGetValuesFromProofs(t *testing.T) { - listOfProofs := []cashu.Proof{ + listOfProofs := cashu.Proofs{ { Id: "mockid", Amount: 2, diff --git a/migrations/18_add_quote_reference_to_proofs.sql b/migrations/18_add_quote_reference_to_proofs.sql new file mode 100644 index 0000000..ea711c7 --- /dev/null +++ b/migrations/18_add_quote_reference_to_proofs.sql @@ -0,0 +1,9 @@ +-- +goose Up +ALTER TABLE proofs +ADD COLUMN quote text; + + +-- +goose Down +ALTER TABLE proofs +DROP COLUMN quote; + From 2d66020b2105d7b2235b55ad8b1d874405faa3bf Mon Sep 17 00:00:00 2001 From: leonardo Date: Sat, 2 Nov 2024 21:05:47 +0000 Subject: [PATCH 09/13] set quote reference and pending --- api/cashu/types.go | 13 +++++++++++++ internal/routes/bolt11.go | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/api/cashu/types.go b/api/cashu/types.go index 87f0611..ff60f7a 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -146,12 +146,25 @@ const PROOF_PENDING ProofState = "PENDING" type Proofs []Proof +func (p *Proofs) SetPendingAndQuoteRef(quote string) { + for i := 0; i < len(*p); i++ { + (*p)[i].State = PROOF_PENDING + (*p)[i].Quote = quote + } +} + func (p *Proofs) SetProofsState(state ProofState) { for i := 0; i < len(*p); i++ { (*p)[i].State = state } } +func (p *Proofs) SetQuoteReference(quote string) { + for i := 0; i < len(*p); i++ { + (*p)[i].Quote = quote + } +} + type Proof struct { Amount uint64 `json:"amount"` Id string `json:"id"` diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index a317164..1196bdb 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -476,7 +476,7 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { } // change state to pending - meltRequest.Inputs.SetProofsState(cashu.PROOF_PENDING) + meltRequest.Inputs.SetPendingAndQuoteRef(quote.Quote) quote.State = cashu.PENDING if AmountProofs < (quote.Amount + quote.FeeReserve + uint64(fee)) { From 95f96d86cd860cb59232528d4a146d2ff7632d12 Mon Sep 17 00:00:00 2001 From: leonardo Date: Sat, 2 Nov 2024 21:07:37 +0000 Subject: [PATCH 10/13] add small test --- api/cashu/keys_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/api/cashu/keys_test.go b/api/cashu/keys_test.go index bc0f532..5e16e5f 100644 --- a/api/cashu/keys_test.go +++ b/api/cashu/keys_test.go @@ -71,3 +71,32 @@ func TestChangeProofsStateToPending(t *testing.T) { } } +func TestChangeProofsStateToPendingAndQuoteSet(t *testing.T) { + + proofs := Proofs{ + Proof{ + Amount: 1, + State: PROOF_UNSPENT, + }, + Proof{ + Amount: 2, + State: PROOF_UNSPENT, + }, + } + proofs.SetPendingAndQuoteRef("123") + + if proofs[0].State != PROOF_PENDING { + t.Errorf("proof transformation not working, should be: %v ", proofs[1].State) + } + if proofs[0].Quote != "123" { + t.Errorf("proof transformation not working, should be: %v ", "123") + } + if proofs[1].State != PROOF_PENDING { + t.Errorf("proof transformation not working, should be: %v ", proofs[1].State) + + } + if proofs[1].Quote != "123" { + t.Errorf("proof transformation not working, should be: %v ", "123") + } + +} From 90a4db9f49aa2ee6442e6852d2a50b5ce510d562 Mon Sep 17 00:00:00 2001 From: leonardo Date: Sun, 3 Nov 2024 13:18:15 +0000 Subject: [PATCH 11/13] better error checking for error code --- api/cashu/types.go | 4 ++++ internal/mint/mint.go | 12 +++--------- internal/routes/bolt11.go | 16 ++++++---------- internal/routes/mint.go | 5 +++-- internal/utils/proofs.go | 4 +++- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/api/cashu/types.go b/api/cashu/types.go index ff60f7a..96e469c 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -20,6 +20,10 @@ var ( ErrCouldNotDecryptSeed = errors.New("Could not decrypt seed") ErrKeysetNotFound = errors.New("Keyset not found") ErrKeysetForProofNotFound = errors.New("Keyset for proof not found") + + AlreadyActiveProof = errors.New("Proof already being spent") + AlreadyActiveQuote = errors.New("Quote already being spent") + UsingInactiveKeyset = errors.New("Trying to use an inactive keyset") ) const ( diff --git a/internal/mint/mint.go b/internal/mint/mint.go index 52ff8b1..e6e6638 100644 --- a/internal/mint/mint.go +++ b/internal/mint/mint.go @@ -32,12 +32,6 @@ type Mint struct { MintDB database.MintDB } -var ( - AlreadyActiveProof = errors.New("Proof already being spent") - AlreadyActiveQuote = errors.New("Quote already being spent") - UsingInactiveKeyset = errors.New("Trying to use an inactive keyset") -) - type ActiveProofs struct { Proofs map[cashu.Proof]bool sync.Mutex @@ -50,7 +44,7 @@ func (a *ActiveProofs) AddProofs(proofs []cashu.Proof) error { for _, p := range proofs { if a.Proofs[p] { - return AlreadyActiveProof + return cashu.AlreadyActiveProof } a.Proofs[p] = true @@ -81,7 +75,7 @@ func (q *ActiveQuote) AddQuote(quote string) error { defer q.Unlock() if q.Quote[quote] { - return AlreadyActiveQuote + return cashu.AlreadyActiveQuote } q.Quote[quote] = true @@ -267,7 +261,7 @@ func (m *Mint) SignBlindedMessages(outputs []cashu.BlindedMessage, unit string) correctKeyset := m.ActiveKeysets[unit][output.Amount] if correctKeyset.PrivKey == nil || correctKeyset.Id != output.Id { - return nil, nil, UsingInactiveKeyset + return nil, nil, cashu.UsingInactiveKeyset } blindSignature, err := output.GenerateBlindSignature(correctKeyset.PrivKey) diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index 1196bdb..50cfb5a 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -8,7 +8,6 @@ import ( "github.com/lescuer97/nutmix/api/cashu" "github.com/lescuer97/nutmix/internal/lightning" "github.com/lescuer97/nutmix/internal/mint" - m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnrpc" @@ -47,7 +46,7 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { var mintRequestDB cashu.MintRequestDB if mint.Config.PEG_OUT_ONLY { logger.Info("Peg out only enables") - c.JSON(400, "Peg out only enabled") + c.JSON(400, cashu.ErrorCodeToResponse(cashu.MINTING_DISABLED, nil)) return } @@ -222,12 +221,8 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { if err != nil { logger.Error(fmt.Errorf("mint.SignBlindedMessages: %w", err).Error()) - if errors.Is(err, m.ErrInvalidBlindMessage) { - c.JSON(403, m.ErrInvalidBlindMessage.Error()) - return - } - - c.JSON(500, "Opps!, something went wrong") + errorCode, details := utils.ParseErrorToCashuErrorCode(err) + c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } @@ -504,7 +499,7 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { if err != nil { logger.Debug("Could not verify Proofs", slog.String(utils.LogExtraInfo, err.Error())) - errorCode, details := utils.ParseVerifyProofError(err) + errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) return } @@ -604,7 +599,8 @@ func v1bolt11Routes(r *gin.Engine, mint *mint.Mint, logger *slog.Logger) { if err != nil { logger.Info("mint.SignBlindedMessages", slog.String(utils.LogExtraInfo, err.Error())) - c.JSON(500, "Opps!, something went wrong") + errorCode, details := utils.ParseErrorToCashuErrorCode(err) + c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } diff --git a/internal/routes/mint.go b/internal/routes/mint.go index a94f68e..e5db823 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -264,7 +264,7 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { if err != nil { logger.Warn("mint.VerifyListOfProofs", slog.String(utils.LogExtraInfo, err.Error())) - errorCode, details := utils.ParseVerifyProofError(err) + errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) return } @@ -274,7 +274,8 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint, logger *slog.Logger) { if err != nil { logger.Error("mint.SignBlindedMessages", slog.String(utils.LogExtraInfo, err.Error())) - c.JSON(500, "Opps!, something went wrong") + errorCode, details := utils.ParseErrorToCashuErrorCode(err) + c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go index 3c6c6af..da88d52 100644 --- a/internal/utils/proofs.go +++ b/internal/utils/proofs.go @@ -8,7 +8,7 @@ import ( "github.com/lescuer97/nutmix/api/cashu" ) -func ParseVerifyProofError(proofError error) (cashu.ErrorCode, *string) { +func ParseErrorToCashuErrorCode(proofError error) (cashu.ErrorCode, *string) { switch { case errors.Is(proofError, cashu.ErrEmptyWitness): @@ -21,6 +21,8 @@ func ParseVerifyProofError(proofError error) (cashu.ErrorCode, *string) { case errors.Is(proofError, cashu.ErrLocktimePassed): message := cashu.ErrLocktimePassed.Error() return cashu.UNKNOWN, &message + case errors.Is(proofError, cashu.UsingInactiveKeyset): + return cashu.INACTIVE_KEYSET, nil case errors.Is(proofError, cashu.ErrInvalidPreimage): message := cashu.ErrInvalidPreimage.Error() return cashu.UNKNOWN, &message From fa4a6b5628993d399b19d0c5c70a932f911a3c3c Mon Sep 17 00:00:00 2001 From: leonardo Date: Sun, 3 Nov 2024 17:04:38 +0000 Subject: [PATCH 12/13] fix tests --- cmd/nutmix/main_test.go | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/cmd/nutmix/main_test.go b/cmd/nutmix/main_test.go index 4ef4ba2..8d52595 100644 --- a/cmd/nutmix/main_test.go +++ b/cmd/nutmix/main_test.go @@ -298,12 +298,18 @@ func TestMintBolt11FakeWallet(t *testing.T) { router.ServeHTTP(w, req) - if w.Code != 403 { - t.Errorf("Expected status code 403, got %d", w.Code) - } + errorResponse = cashu.ErrorResponse{} + + err = json.Unmarshal(w.Body.Bytes(), &errorResponse) - if w.Body.String() != `"Invalid blind message"` { - t.Errorf("Expected Invalid blind message, got %s", w.Body.String()) + if w.Code != 400 { + t.Errorf("Expected status code 400, got %d", w.Code) + } + if errorResponse.Code != cashu.TOKEN_NOT_VERIFIED { + t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Code) + } + if errorResponse.Error != "Proof could not be verified" { + t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Error) } // MINTING TESTING ENDS @@ -1142,13 +1148,15 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer w = httptest.NewRecorder() router.ServeHTTP(w, req) + errorResponse = cashu.ErrorResponse{} - if w.Code != 403 { - t.Errorf("Expected status code 403, got %d", w.Code) - } + err = json.Unmarshal(w.Body.Bytes(), &errorResponse) - if w.Body.String() != `"Invalid blind message"` { - t.Errorf("Expected Invalid blind message, got %s", w.Body.String()) + if errorResponse.Code != cashu.TOKEN_NOT_VERIFIED { + t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Code) + } + if errorResponse.Error != "Proof could not be verified" { + t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Error) } // ASK FOR MINTING WITH TOO MANY BLINDED MESSAGES @@ -1809,8 +1817,15 @@ func TestConfigMeltMintLimit(t *testing.T) { if w.Code != 400 { t.Errorf("Expected status code 200, got %d", w.Code) } - if w.Body.String() != `"Peg out only enabled"` { - t.Errorf(`Expected body message to be: "Peg out only enabled". Got: %s`, w.Body.String()) + errorResponse := cashu.ErrorResponse{} + + err = json.Unmarshal(w.Body.Bytes(), &errorResponse) + + if errorResponse.Code != cashu.MINTING_DISABLED { + t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Code) + } + if errorResponse.Error != "Minting is disabled" { + t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Error) } } From f1685e471dd977eeec75853b9406760b689ad388 Mon Sep 17 00:00:00 2001 From: leonardo Date: Mon, 4 Nov 2024 10:19:59 +0000 Subject: [PATCH 13/13] pending quote check test --- cmd/nutmix/payment_error_handling_test.go | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cmd/nutmix/payment_error_handling_test.go b/cmd/nutmix/payment_error_handling_test.go index 9112a3a..655075c 100644 --- a/cmd/nutmix/payment_error_handling_test.go +++ b/cmd/nutmix/payment_error_handling_test.go @@ -146,6 +146,23 @@ func TestPaymentFailureButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { if proofs[0].State != cashu.PROOF_PENDING { t.Errorf("Proof should be pending. it is now: %v", proofs[0].State) } + + req = httptest.NewRequest("POST", "/v1/melt/bolt11", strings.NewReader(string(jsonRequestBody))) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + var errorResponse cashu.ErrorResponse + + err = json.Unmarshal(w.Body.Bytes(), &errorResponse) + + if err != nil { + t.Fatalf("Could not parse error response %s", w.Body.String()) + } + + if errorResponse.Code != cashu.QUOTE_PENDING { + t.Errorf("Incorrect error code, got %v", errorResponse.Code) + + } + } func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { @@ -299,6 +316,22 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { if proofs[0].State != cashu.PROOF_PENDING { t.Errorf("Proof should be pending. it is now: %v", proofs[0].State) } + + req = httptest.NewRequest("POST", "/v1/melt/bolt11", strings.NewReader(string(jsonRequestBody))) + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + var errorResponse cashu.ErrorResponse + + err = json.Unmarshal(w.Body.Bytes(), &errorResponse) + + if err != nil { + t.Fatalf("Could not parse error response %s", w.Body.String()) + } + + if errorResponse.Code != cashu.QUOTE_PENDING { + t.Errorf("Incorrect error code, got %v", errorResponse.Code) + + } } func TestPaymentPendingButPendingCheckPaymentMockDbFakeWallet(t *testing.T) {